88 Commits

Author SHA1 Message Date
snark13 1214785a56 PoP roomtest: дверь уровня — непрозрачные куски + финальный кадр на 2-ю страницу
Два дефекта открытой двери:

1. Слева от лестницы оставалась поднявшаяся решётка.  Оригинал рисует ВСЕ
   куски двери blitters_0_no_transp, а наш упаковщик по умолчанию гонит
   пиксель 0 в 0xFF (ключ прозрачности) — марш лестницы 144 переставал
   закрашивать створку под собой.  Добавил LEVELDOOR_ENV_IDS в
   NO_TRANSP_ENV_IDS (тот же приём, что для 43/73/74/96/149).

2. Дверь дрожала через кадр: створка анимируется в back-страницу, и
   ПОСЛЕДНИЙ кадр анимации ложился только на одну из двух страниц, вторая
   застревала на шаг раньше.  Добавлен отложенный редрой (ldoor_rest) —
   повтор финального кадра на второй странице, как spike_rest/button_rest.

Проверено в MAME с заморозкой кадра: обе страницы в области двери
побайтово одинаковы, слева от лестницы чистый чёрный фон.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:18:01 +03:00
snark13 63a25530f5 PoP roomtest: дверь уровня — створка, лестница и анимация открытия
Комната 9: портал на уровень 2 рисовался чёрным проёмом — не был
портирован draw_leveldoor (seg008:1D29).  Дверь рисуется при обработке
ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8 px:
  99  низ лестницы (всегда),
  144 марш лестницы за створкой (когда створка тронулась),
  33  слайс створки — повторяется вниз с шагом 4 px до y = ybottom-modif
      (modif 0 = закрыто на всю высоту, 43 = открыто, остаётся кромка),
  34  верх коробки.

Анимация: animate_leveldoor (seg007:05F1) — type 0..2 открытие (modif++
до 43), type>=3 быстрое закрытие со скоростями {0,5,17,99}.  Кнопка
заводит trob через trigger_1 (seg007:0999): дверь открывается ОДИН раз,
при modif != 0 кнопка уже ничего не делает.

Перерисовка створки — pop_leveldoor_redraw: draw_tile правой половины С
ЗАПЕЧКОЙ в ОЗУ-копию (банк TRANSPARENT), иначе kid_heal возвращал бы из
фона закрытую створку.  Куски двери непрозрачные, wipe не нужен.

Спрайты 33/34/99/144 не попадали в атлас: сбор идёт прогоном
render_room.py, а он draw_leveldoor не реализует — добавлены явным
набором LEVELDOOR_ENV_IDS (как STUCK_ENV_IDS для нажатой кнопки).

Проверено в MAME (комната 9): закрытая дверь = решётка как в оригинале;
после нажатия кнопки (0,0) створка едет вверх, открывая лестницу.
Вход в дверь (кадры 217..228 + переход на уровень 2) НЕ делался.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:05:57 +03:00
snark13 fb495ace42 PoP roomtest: вспышка фона стробом + красная вспышка урона (+ урон падения)
1. Жёлтая вспышка подъёма меча была ОДНОЙ длинной заливкой.  В оригинале
   (seg003:0AFC flash_if_hurt / remove_flash_if_hurt) цвет ставится и
   СНИМАЕТСЯ в том же кадре — пока идёт flash_time, видно быстрый строб
   «жёлтый/чёрный».  У нас теперь так же: цвет ставится в кадре и
   снимается после ПЕРВОГО gfx_wait_vsync из трёх (≈20 мс жёлтого,
   40 мс чёрного — близко к оригинальным 2 тикам таймера).

2. Красной вспышки при потере HP не было вовсе.  Порт второй ветки
   flash_if_hurt: если flash_time не активен, но в этом кадре hitp_delta<0
   — do_flash(color_12_brightred) ровно на кадр.  У нас триггер —
   pop_kid_hurt (он же рисует «брызги»).

3. Чтобы вспышке было от чего срабатывать, портирован урон падения из
   land() (seg005), которого у нас не было: <22 — мягко, <33 — −1 HP и
   seq_20 (2 этажа), иначе take_hp(100) + seq_22_crushed (насмерть).
   ВНИМАНИЕ: это меняет геймплей — падение с 2 этажей теперь отнимает HP,
   с 3+ убивает (как в оригинале).

Проверено в MAME: pop_flash_time тикает 5→3→1→0 (строб), падение с ряда 0
на ряд 2 даёт hitp_curr 3→2 и кадр 109 (medium land).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:40:52 +03:00
snark13 e1846369ad PoP roomtest: блеск лежащего меча, клинок в руке, вспышка фона
Три расхождения с оригиналом на сцене подъёма меча (комната 15):

1. Лежащий меч не блестел.  Порт animate_sword (seg007:0425) +
   start_anim_sword (seg007:087C): при входе в комнату тайлу даётся
   случайная фаза (prandom & 0x1F), каждый кадр счётчик вниз, на 0 —
   новый период 0x28..0x67.  Кадр блеска рисуется РОВНО на modif==1
   ((modif==1)+10 в draw_tile), т.е. одиночная вспышка раз в 40..103
   тика.  Перерисовка тайла — по смене видимого кадра, схемой кнопки
   (текущая страница сразу, вторая через rest-цикл).

2. В кадрах «нашёл меч» клинка не было видно.  Порт
   add_sword_to_objtable (seg006:1798): клинок — ОТДЕЛЬНЫЙ спрайт
   chtab_0 поверх Kid со смещением из sword_tbl.  Смещения в ЭКРАННЫХ
   пикселях (оригинал применяет их после calc_screen_x_coord), поэтому
   берём уже масштабированный obj_x.  Из таблицы взяты только строки
   35..42 (кадры 229..236) — бой не портирован.  Новый атлас sword.atl
   (8 спрайтов, 1.6 КБ) + палитра chtab_0 в слотах 0x80..0x8F.
   Прямоугольник heal расширяется объединением с клинком, иначе он
   оставлял след за габаритом Kid.

3. Не было вспышки фона.  do_flash = set_bg_attr(0, color) — оригинал
   подменяет НУЛЕВУЮ запись палитры, вспыхивает всё чёрное поле экрана;
   proc_get_object ставит flash_color=14, flash_time=8.  У нас gfx_pal_set
   на обе страницы.  ВАЖНО: вызов gfx_pal_set(0,0,0,0,0) пятью литералами
   ломает SDCC 4.5 (эмитит невалидный `ld hl, a`) — обёрнуто в функцию с
   параметрами.

Попутно: TROBS_MAX 24 -> 30 (как в оригинале) + при входе в комнату из
списка выбрасываются «декоративные» trob ДРУГИХ комнат (факелы/зелья/
меч анимируются только в отрисованной).  Без этого отладочный обход всех
24 комнат забивал список, и новые анимации молча не заводились.

Проверено в MAME: блеск (брейк на редрое тайла срабатывает), клинок в
руке виден, фон вспыхивает жёлтым.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:22:22 +03:00
snark13 1c91c5f92a PoP roomtest: меч — отрисовка на полу, подъём по Shift, статус have_sword
Комната 15, тайл (2,2): меч не рисовался вовсе — в draw_tile не было
ветки tiles_22_sword (seg008 draw_tile_anim):

    add_midtable(chtab_1, (modifier == 1) + 10, draw_xh, 0, draw_main_y - 3, ...)

Спрайты уже лежали в нашем pot-атласе (chtab_1 пакуется целиком, id 1..23),
так что понадобился только вызов.  У нас предмет рисуется статикой в фоне,
а не в midtable: пока меч лежит, он не анимируется, а Kid и так поверх фона.

Подъём — порт цепочки оригинала:
- check_get_item/get_item (seg005:061F/073E) -> pop_get_item_action() в
  pop_map (тайлы): стоя НА предмете отступить на тайл назад, подровняться
  по кромке и присесть; из приседа над мечом — поднять;
- do_pickup (seg006:1671): тайл -> пол, pop_item_taken = tilepos+1;
  roomtest запекает пол на ОБЕИХ страницах (pop_floor_bake) и пишет
  per-room override, чтобы меч не воскресал при возврате в комнату;
- триггер — Shift (control_shift2) в стойке и в приседе, как в
  control_standing/control_crouched;
- seq_91 pickupsword: опкод SEQ_GET_ITEM с аргументом 1 в play_seq теперь
  зовёт pop_proc_get_object (seg006:16CB) -> pop_have_sword = 1.

Статус: pop_have_sword.  Боевой режим (стойка с мечом, бой) НЕ делаем и
он тут не нужен: seq_91 сам заканчивается убиранием меча в ножны
(кадры 230..240) и возвратом в обычную стойку — как в оригинале до
встречи со стражем.  Питьё зелий не портировано: над зельем Kid только
приседает (get_item возвращает «не обработано»).

Проверено в MAME (комната 15): меч виден на полу, Shift над ним даёт
присед -> кадр 229 «нашёл меч» -> ножны -> стойка; меч с пола исчезает,
pop_have_sword = 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:46:09 +03:00
snark13 6769b6a01b PoP roomtest: loose-плита впереди = КРОМКА в get_edge_distance
Осторожный шаг к проваливающемуся полу проваливал Kid с первого же шага:
в pop_edge_distance не было ветки оригинала (seg004:067C)

    if (tiletype == tiles_11_loose) goto loc_59FB;  // CLOSER + до кромки

— плита читалась как обычный пол (EDGE_FLOOR, 11), и весь механизм
«проверки ногой» пролетал мимо.  С веткой safe_step (seg005) отрабатывает
три фазы, как в оригинале: (1) укороченный шаг РОВНО до кромки плиты
(seq 29..42 по distance), (2) seq_44 testfoot — щуп ногой, плита трясётся
по SEQ_KNOCK_DOWN, Kid остаётся на месте (Char.repeat), (3) только третье
нажатие — шаг на плиту и падение вместе с ней.

Заодно портированы соседние ветки той же функции: верх двери лицом вправо
(проверяется ДО wall_type) и closer/меч/зелье (кромка, пока есть зазор).

Проверено вручную в MAME: все три фазы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:29:01 +03:00
snark13 5f5eefc9fc PoP roomtest: loose-плита поверх Кида (порт draw_loose + draw_tile_base)
Комната 12, вис и подтягивание на кромке loose-плиты над дырой от
соседней упавшей: плита рисовалась ПОД Кидом — он лез на неё «с
переднего края» вместо проёма.  Не хватало двух кусков draw_tile:

1. draw_loose (seg008:0A38) кладёт нижнюю грань плиты (loose_fram_bottom)
   В ОБЕ таблицы — backtable И foretable, безусловно.  Это единственный
   кусок тайла с таким поведением: у обычного пола bottom идёт только в
   backtable, а fore_id = 0.  Добавлено в fore_tile (наш проход foretable
   по тайлам футпринта Кида); ceiling-случай это уже делал отдельно.
2. draw_tile_base (seg008:0A8E) подставляет id: у loose верх плиты берётся
   из loose_fram_left, у opener'а без пола слева — 148.  В нашем
   midtable-оверлее (overlay_mid_tile) стоял голый tile_table.base_id, а у
   loose он 0 — верх плиты в оверлей не попадал, и поверх Кида ложилась
   только передняя грань.  Перенесён draw_tile_base целиком.

Проверено в MAME: кадр виса на кромке целой плиты (frame 89, x=95,
col 2) — плита закрывает Кида, наружу торчат только пальцы, как в SDLPoP.

Голова стоящего Кида поверх падающей НА НЕГО плиты — артефакт САМОГО
оригинала (сверено с SDLPoP v1.24), не чинить: записано в bug_list.md
(раздел «НЕ БАГИ») и в memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:14:23 +03:00
snark13 517d225091 PoP roomtest: полный порт bumped (seg004) — прижатие Y к полу + hardbump
Комната 5, прыжок в решётку: Kid оставался стоять на 6 пикселей выше
пола и без приземления-приседания.  Шесть пикселей — это dy(-6) кадра
frame_25_standing_jump_10: удар обрывал standjump ровно между кадрами
25 и 26, парный dy(+6) не выполнялся.

От оригинального bumped() у нас был портирован только хвост (выровнять
X к грани + seq_47_bump).  Добавлены недостающие ветки:

- bumped()      — исход удара выбирается по тайлу, НА КОТОРОМ персонаж
                  оказался после отжатия (сквозь стену/верх двери это
                  соседняя клетка, для ворот/зеркала — сама клетка);
- bumped_floor  — прижимает Y к y_land, ветка fall_y>=22 (только отжать
                  на 5) и выбор сиквенса по кадру: 24/25/40..42/102..106
                  -> seq_46_hardbump (отскок с приседанием), иначе 47;
- bumped_fall   — удар выше пола на 15+ px (беззнаковое сравнение
                  оригинала) или не над полом: seq_45, в свободном
                  падении — только гашение fall_x.

Гард «уже в отскоке — не рестартить» (наш, от edge-триггера коллизий)
перенесён внутрь bumped_floor: выравнивание X/Y идёт всегда, рестарта
анимации нет.  Проверено в MAME.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 14:23:10 +03:00
snark13 fc69316c2c PoP roomtest: окклюзия падающей плиты соседним полом — ряд из координаты
Комната 1, плита (2,6): её правая грань (env 42, лежит целиком в ячейке 7)
оставалась ПОВЕРХ пола (2,7) и перекрывала его переднюю грань.

Причина: mob_render перерисовывал соседний тайл по m->row — а это
ЛОГИЧЕСКИЙ счётчик «сквозь какой ряд летим», который mob_down_a_row уводит
на ряд вперёд.  Для плиты НИЖНЕГО ряда он сразу становится 3, и окклюзия
звала draw_tile(3, col+1) — ряда 3 нет, тайл рисовался за нижним краем
экрана, то есть пол соседа не перерисовывался никогда.

Оригинал берёт ряд ИЗ КООРДИНАТЫ куска, draw_mob (seg007:13E5):
    tile_row = y_to_row_mod4(ypos);      set_redraw2(tilepos справа)
    top_row  = y_to_row_mod4(ypos - 18); если отличается — ТОЖЕ пометить
То есть помечаются ДВА тайла справа: под низом куска и под его верхом, пока
кусок висит на границе рядов.  Второго у нас не было вовсе — и именно он тут
решающий: при y=194 нижний ряд даёт -1 (полоса у потолка), а верхний — 2,
то есть настоящий пол (2,7).

Проверено в MAME покадрово (bp на pop_loose_mob_spawn + cmd gv; NB: главный
цикл ждёт ТРИ vsync на итерацию, один gv = треть игрового кадра): следов
плиты на полу (2,7) нет, передняя грань цела, потолочная полоса не тронута.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:28:50 +03:00
snark13 08ebf09d4d PoP roomtest: пометить недостижимые комнаты уровня 1 (13, 18, 24)
Обход графа связей res2001.bin (links @1952) от стартовой комнаты 1: 13, 18
и 24 недостижимы — ссылки наружу у них есть, на них не ссылается никто
(24: L->9, но у 9 R=0).  Несимметричные ссылки ровно у этих трёх, у прочих
21 симметрия полная — признак выкинутых из компоновки комнат.  В таблицу
обхода добавлена пометка, чтобы не гоняться за призраками: рендер кромки
читает колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
считает, так что странный шов там — свойство данных.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:06:47 +03:00
snark13 50aa652dbe sprinter-cc: убрать устаревший комментарий «--w3 несовместим с huge»
Комментарий у блока разбора --w3 утверждал, что режим подразумевает small и
несовместим с huge, хотя код тремя строками ниже huge как раз поддерживает:
резидент делит окно W3 с трамплин-банками (порт 0xE2), crt0_banked запоминает
резидентную страницу и возвращает её дефолтом после загрузки банков, а
трамплин bank.s сохраняет/восстанавливает W3 на каждый __banked вызов.
Следствие, которое теперь тоже записано: резидент -> __banked работает,
обратное невозможно (из банка резидентной страницы в W3 просто нет).

В справке по --w3 дополнено правило «W3-код не переключает страницу W3»:
сюда же относится собственная графическая скобка _bgi_begin/_bgi_end — она
маппит видеобанк ЧЕРЕЗ ТОТ ЖЕ порт 0xE2, и открытая из W3-кода означает
исполнение из видео-ОЗУ (белый экран).  Звать графические примитивы HOME
можно: скобку они открывают и закрывают внутри себя.

Только комментарии, поведение не менялось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 12:04:18 +03:00
snark13 08c6a8504f PoP roomtest: отладочный обход комнат (+/-) + TODO по редрою пик и idle-skip
ROOMNAV (одна строка #define в roomtest.c): '+'/'-' — следующая/предыдущая
комната уровня ПО НОМЕРУ (1..24, с обёрткой), Kid ставится на первый пол,
pop_trob_reset() возвращает пики/ворота в исходное.  Нужен для обхода всех
24 комнат в поиске багов отрисовки, включая недостижимые обычным путём.
Цена — 505 Б кода W1 (22816 -> 23321), они же вычитаются из кучи (4178 ->
3673 Б); W3 не тронут.  Переводить приложение в huge ради этого не стали:
смена модели памяти (банкованный W1 + трамплины) ради полукилобайта —
плохой размен прямо перед прогоном комнат.

Номер комнаты — ПОЛОСКАМИ в верхнем борте (слева десятки, справа единицы),
а не outtextxy: текст тянет системный знакогенератор (_gfx_font_buf, 2 КБ
статики) и сажает кучу до 576 Б.  Цвет полосок 0x57, а не WHITE: палитра
игровая, запись 15 в ней ЧЁРНАЯ (kid.pal 0x0F = 0,0,0) — из-за этого
закомментированный ранее лейбл был бы невидим в любом случае.

bug_list.md: раздел TODO (T-1 редрой пик по причине, T-2 idle-skip) +
пустая таблица на 24 комнаты под результаты обхода.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:58:06 +03:00
snark13 723da3c5c1 PoP roomtest: пики — редрой каждый кадр, полный draw_tile; пламя без heal
Выдвинутая пика живёт ТОЛЬКО в видео-ОЗУ (банк SPRITE, в запечённый фон не
входит), а kid_heal каждый кадр возвращает под спрайтом Кида печёный фон —
то есть стирает попавшие под него части остриёв.  Редрой «только при смене
видимой сигнатуры» их не возвращал: hold держится 15 кадров (modif
0x8F..0x81) с неизменным кадром 5.  Итог по дампу VRAM (комната 14, Kid на
(2,7), modif 0x8E): у спрайта 132 стёрто третье остриё (x 245..247), у 138 —
прямое целиком (x 258..260) и низ наклонного, страницы дабл-буфера
расходились на 8 пикселей.  SDLPoP: animate_spike (seg007:0353) зовёт
redraw_21h БЕЗУСЛОВНО, вне всяких if — редрой каждый кадр, пока trob жив.

pop_spike_redraw переписан на честный redraw_tile_height (seg007:0218):
heal + ПОЛНЫЙ draw_tile своего тайла и правого соседа.  Рисовать только два
спрайта остриёв нельзя — теряется порядок слоёв внутри тайла
(draw_tile_anim_right идёт ПЕРВЫМ, база и fore соседа ложатся поверх), и
остриё лезло на колонну (2,9).  draw_tile заодно сам восстанавливает вклад
соседних пик (ветка lcode==2), так что две пики подряд больше не гасят
друг друга.

Пламя факела: heal убран.  Оригинал рисует его blitters_0_no_transp (seg008
draw_tile_anim_right), все 9 кадров лежат на общем канвасе 16x18 и бокс
центрирован в ячейке — непрозрачный блит сам полностью накрывает предыдущий
кадр.  Кадры пакуются с opaque=True (POT_FLAME_IDS).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:34:07 +03:00
snark13 e143ee010a PoP roomtest: нажатая кнопка-closer, таблица падающих кусков, фон-покой при запечке
Кнопка-closer (tiles_6) в нажатом состоянии рисуется как tiles_5_stuck
(get_tile_to_draw seg008:2FE), но её грани — env id 35 (правая) и 36
(нижняя) — не попадали в атлас: pop_pack_bg собирает набор спрайтов
прогоном render_room по статике уровней, а тайла 5 статически в уровнях
нет.  Итог — чёрный провал на месте нижней грани.  Добавлены явным
набором STUCK_ENV_IDS (как loose/пики/ворота).  Заодно OPENER_NOFLOOR_ENV_IDS
= {148}: левая половина кнопки-opener, когда слева пусто (draw_tile_base
seg008:0A8E) — ветка была не реализована ни в pop_bg, ни в render_room.

bake_rest: при ЗАПЕЧКЕ статического фона (запись идёт в ОЗУ-копию
акселератора, из неё восстанавливает heal) get_loose_frame возвращает
кадр ПОКОЯ.  Иначе транзиентный кадр тряски соседней плиты консервируется
в копии, а так как запечка идёт двумя кадрами (по одному на страницу
дабл-буфера) — на страницах застывают РАЗНЫЕ кадры, вечное мерцание.

mobs[MOB_MAX] вместо одиночных статиков: две плиты, упавшие подряд,
теряли первый кусок (застывал в воздухе) и оставляли щебень только от
одной.  Плюс отложенная допечка прошлого приземления ПЕРЕД разбором
нового и ограничение чёрного бара в pop_loose_bake_empty низом тайла.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 12:07:29 +03:00
snark13 b106aba59b PoP roomtest: подъём/спуск на ОТКРЫТЫХ воротах (потерянная проверка openness)
Kid не мог залезть на тайл поднятой решётки: срывался обратно.  can_climb_up
(seg005:09DF) выбирает seq_73 («влез под решётку и съехал») только для
ЗАКРЫТЫХ ворот — условие включает curr_room_modif[curr_tilepos] >> 2 < 6, а у
нас его не было, и seq_73 играл при любой решётке над головой при взгляде
влево.

Та же дыра нашлась в спуске: down_pressed (seg005:482) запрещает climb-down с
тайла ворот лицом влево тоже ТОЛЬКО пока они закрыты
(|| curr_room_modif[curr_tilepos] >> 2 >= 6).  Без этого Kid приседал вместо
спуска даже под поднятой решёткой.

Openness обеих проверок берётся общим хелпером gate_modif(col,row) (вынесен из
gate_passable — умеет свою комнату и соседнюю через шов).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:59:30 +03:00
snark13 5e09bd9d5a PoP roomtest: непрозрачные нижние грани + edge-триггер бампа + 2 колонки шва
Три фикса, найденные разбором в MAME.

1. Просвечивание Кида сквозь торец кнопки (подъём/спуск у кромки).
   draw_tile_bottom (seg008:570) рисует нижнюю грань блиттером
   blitters_0_no_transp: пиксель индекса 0 заливается ЧЁРНЫМ, а не
   пропускается.  У граней кнопок (env 96/149) и нижних кадров loose (73/74)
   последняя строка целиком нулевая — наш упаковщик переводил 0 в 0xFF для
   ВСЕХ спрайтов, и она становилась дырой, сквозь которую был виден Kid за
   тайлом (на запечённом фоне не видно — там и так чёрное).
   pop_pack_bg.py: NO_TRANSP_ENV_IDS пакуется с сохранением индекса 0 как
   ЦВЕТА (0x50 = чёрный в палитре env).  Ноль тактов в рантайме.

2. Разворот в проёме опущенной решётки выкидывал Кида на другую её сторону.
   check_bumped проверял столкновение УРОВНЕМ («край зашёл за грань»), поэтому
   разворот на месте внутри тайла-стены менял, какую грань мы меряем, и это
   читалось как новое столкновение → Kid.x = char_dx_forward(d) швырял его
   сквозь решётку.  Оригинал (check_collisions + get_row_collision_data,
   seg004:0004) считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ
   направления, и зовёт bumped() только на переходе 0 -> 1 (вправо смотрит на
   0x0F, влево на 0xF0).  Порт этого edge-триггера: флаги от габарита + память
   прошлого кадра по колонке, сброс при смене комнаты (в оригинале это
   несовпадение prev_coll_room/curr_row_coll_room).  Проверено: теперь
   разворот даёт уход в комнату 8 (leave_room, взгляд влево, char_x_left<=54)
   — то есть туда, где Kid физически и стоит.

3. Снимок шва расширен до ДВУХ колонок с каждой стороны (lcol_fg[6]/
   rcol_fg[6]: col9+col8 и col0+col1).  Kid, стоящий В шве (curr_col=-1),
   смотрит вперёд на колонку -2, где раньше была мнимая стена (get_tile ->
   TILE_WALL).  Оригинал резолвит любую колонку через find_room_of_tile
   (seg006:005D).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:47:58 +03:00
snark13 6582154381 PoP roomtest: clip_char (верхняя обрезка спрайта) + подстановка нажатой кнопки
Спуск Кида с кнопки (room8, кромка (0,6)) рисовался неверно: Кид просвечивал
в щель между кнопкой и ближним столбом, а ближняя рука была срезана до одного
оторванного пикселя.  Две независимые причины.

1. Не был портирован clip_char() (seg006:1749) — оригинал перед add_objtable
   обрезает спрайт персонажа сверху по y_clip[curr_row+1], когда тайл над
   головой стена или пол.  Порт: pop_clip_char_top() (pop_map.c, метрики по
   set_char_collision) + новый примитив gfx_blit_cols_part() в libbgi (блит
   column-major с пропуском skip верхних строк; обрезка сверху бесплатна —
   колонка непрерывна в ОЗУ, сдвигается только старт).  gfx_blit_cols стал
   тонкой обёрткой над ним.  kid_heal чистит уже ОБРЕЗАННЫЙ прямоугольник,
   иначе стирается кромка пола над срезом.

2. climb_overlay_tile (порт draw_floor_overlay, seg008:1E3A) выбирал ветку по
   СЫРОМУ коду тайла, а get_tile_to_draw (seg008:240) подменяет нажатую кнопку
   на floor/stuck.  Тайл-кнопка не проходил тест floor → уходил в
   draw_other_overlay, который кладёт поверх Кида ВЕСЬ тайл вместо узкой
   кромки floor_left_overlay[frame-137].  Фикс — tile_code_drawn(): одна
   подстановка на все слои (fore_only_tile/overlay_mid_tile перестали её
   дублировать, W3 −293 Б).

Проверено покадрово в MAME (шаг gv + снимок): кадры спуска 148..138 чистые,
после приземления на кнопке мусора нет.

Заодно: gfx_heal_noclip() (пара к gfx_blit_noclip; heal 11658 -> 8982 тактов)
и rest_pending в pop_trob — холостой проход по 30 тайлам только когда есть
отложенные редрои пик/кнопок (pop_process_trobs 89-111К -> 70-92К тактов).

START_ROOM временно = 8 (отладка спуска с кнопки), вернуть на 6/старт уровня.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 09:50:47 +03:00
snark13 c127a4b0d2 PoP: факелы и зелья (chtab_1) + быстрый блит фона gfx_blit_noclip
Графика chtab_1 (flame/sword/potion) — новый атлас pop_pot.atl:
- склянка зелья берётся из chtab_1 (seg008 draw_tile_fore: id 12 малая /
  13 большая при типах 2..4), а НЕ chtab_6 id 12 — res212 в VDUNGEON нет,
  каскад падал на VPALACE и рисовал кусок дворцовой декорации («мусорный
  объект» в комнате 5);
- пакуем chtab_1 целиком (id 1..23): пламя, склянки, кадры пузырька;
- MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из
  ОБЩЕЙ 16-цветной палитры экрана — она добавлена в 0x30..0x3F, палитра
  chtab_1 в 0x40..0x4F; пузырёк пакуется силуэтом цвета 12 (красный),
  его маска — цвета 0;
- в env-атлас добавлено основание факела (env 146, seg008:489 — рисует
  правый сосед); в статический render_room оно не попадало.

Анимация (seg007 animate_torch/animate_potion + seg000:0B12
anim_tile_modif): при входе в комнату факелам и зельям задаётся случайная
стартовая фаза и заводится trob; факел — get_torch_frame, пламя в ячейке
ПРАВОГО соседа (xh = draw_xh+1, y = draw_main_y−40); зелье —
bubble_next_frame по младшим 3 битам модификатора (старшие 5 = тип, при
загрузке уровня modif <<= 3, seg009).  Пламя и пузырёк не запекаются в
фон: heal своей области + кадр поверх.  Скорость пламени /2 — наш
логический кадр короче игрового тика оригинала.

Скорость отрисовки (профиль в MAME, кадр = 430 080 тактов):
- замер показал, что цена блита почти НЕ зависит от размера — 13 288
  тактов на спрайт 32×3 против 4 617 у линейного спрайтового ядра без
  клипа; платим за проход gfx_blit → gfx_blit_part → _gfx_blit_full;
- в libbgi добавлен gfx_blit_noclip() — блит без клипа в ТЕКУЩЕМ банке
  (putsprite не годится: навязывает GFX_BANK_SPRITE, фону нужен
  TRANSPARENT ради ОЗУ-копии); pop_bg.blit_b уходит на него, когда
  спрайт целиком на экране и не нужен g_clip_top → ~2.9× на блит;
- W3-скобку ставит САМА libbgi: из модуля с --w3 её вызывать нельзя —
  после _bgi_begin окно W3 занято видеобанком и код вызывающего исчезает
  (проявлялось белым экраном);
- редрой шва больше не перерисовывает полосу потолка (bar начинается с
  POP_YOFF+3) — это удваивало стоимость блока при анимации решётки;
- docs/size_optimization_plan.md §7–§8: замеры, сделанное и запас
  (батчинг W3-скобки, решётка одним спрайтом, лишние блиты).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:53:04 +03:00
snark13 63a3b60524 PoP roomtest: плита-потолок, порядок слоёв по SDLPoP, фиксы окклюзии и Y
Фича — плита-ПОТОЛОК (loose ряда 2 комнаты сверху, комн.5 (2,5) над комн.6):
- check_press (seg006:1683) портирован целиком: кадры виса 87..99 и подъёма
  135..140 «нажимают» тайл, за который Kid держится; кадр 79 — удар снизу
  (с проверкой action, как в оригинале); стояние — тайл под ним;
- ряд −1 в pop_map.get_tile = ряд 2 комнаты сверху (порт find_room_of_tile),
  состояние тряски/падения — pop_ceil_modif[10]; do_knock(-1) трясёт потолок;
- падение: mob переведён на координаты оригинала (y_loose_land/y_something),
  спавнится в ряду −1, по move_loose находит пол → loose_land: debris +
  do_knock; heal коридора — по прошлой позиции ДЛЯ КАЖДОЙ страницы;
- урон: check_loose_fall_on_kid + fell_on_your_head (seq_52/seq_22, −1 HP)
  и «брызги» draw_hurt_splash (kid image 218);
- колодец наверх: leave_room «вверх» проверяется ПЕРВЫМ (иначе y≈248 при
  подтягивании ловил check_leave_below и ронял Kid из уровня);
- персистентность: общий tile-override (щебень внизу, пустота в комнате
  сверху) вместо debris-only.

Окклюзия — приведена к оригиналу (найдено трассировкой собранного SDLPoP):
- draw_tile_right/anim_right/draw_loose кладут спрайты в add_backtable
  НАПРЯМУЮ, минуя ptr_add_table → правая грань соседа («шахматка» столба 93),
  blueline и грани loose/пик соседа ВСЕГДА под персонажем.  draw_other_overlay
  поверх Kid даёт только: 42 при левом соседе-поле, base, свои пики, bottom
  (overlay_mid_tile);
- порядок объектов: тайл Kid (set_objtile_at_char) и порядок обхода
  (ряды 2→0, колонки 0→9), внутри тайла — сортировка по y (sort_curr_objs);
  то же правило для падающей плиты (pop_loose_mob_draw_over);
- fore-слой рисуется ПОСЛЕ оверлеев (foretable после midtable), иначе тёмный
  скос floor_left_overlay ложился поверх колонны;
- fore_tile больше не рисует bottom_id (в оригинале он в backtable) — минус
  2..4 блита за кадр;
- полоса потолка поверх Kid = draw_tile_bottom(1)+draw_loose(1)+draw_tile_fore
  (arg_0=1 добавляет в foretable), остальное — под ним;
- следы оверлея восстанавливаются на ОБЕИХ страницах (pop_fore_heal).

Прочее:
- control(): при action bumped/in_freefall управление игнорируется целиком
  (seg005) — иначе прерванный присед терял dy(1)+dy(1) из medland и Kid
  навсегда оставался на 2px выше пола;
- check_bumped: стена ищется по колонке переднего края (а не curr_col) +
  гард разворота — Kid больше не проходит сквозь опущенную решётку шва;
- pop_floor_bake для смены floor→debris (bake_empty стирал верх стены ряда
  ниже — чёрный прямоугольник);
- в pop_room_redraw_seam_left восстанавливается полоса потолка над решёткой.

Проверено в MAME (bridge); эталон — собранный SDLPoP из applications/PoP/SDLPoP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:35:25 +03:00
snark13 86d7615841 PoP: roomtest — объекты/обломки/переходы комнат + арт
Порт Prince of Persia (applications/PoP/roomtest): развитие уровня,
объекты (loose-полы/обломки), переходы между комнатами, фон-упаковка;
планы (room_model/size_optimization), bug_list, pop_trob.
Арт third_party/16x16-RPG-characters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:23 +03:00
snark13 3d586af031 libc/kbd: raw-клавиатура — вычерпывание FIFO + селективный wipe модификаторов
- kbd_raw_sync: цикл вычерпывания SIO FIFO (не 1 байт/прерывание) —
  фикс залипания клавиш; overrun-wipe сбрасывает только пострадавшие
  клавиши, не модификаторы (typematic их не перечитывает).
- Гайд docs/kbd-games.md; заметка о Rx-overrun в docs/TODO.md; справочник
  скан-кодов в docs/libc-reference.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:12 +03:00
snark13 95c22be9bd libbgi: скролл-примитивы + --w3 + отчёт раскладки памяти
Скролл региона video->video (неактивная страница -> активная, банк 0x50:
копия = скролл + heal цели):
- gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без
  страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256;
- gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход
  без буфера (колонка = accel-burst LD A,A, STOP между read/write делает
  промежуточный OUT Port_Y безопасным), банды по 16 колонок;
- _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t.
Пример examples/scroll.

check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели
(W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank),
не только при --bank.  Док docs/memory-management.md §10.

--w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT,
mkexe -W, tests/w3probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:00 +03:00
snark13 d1bc97c589 roomtest: обломки упавшего loose в комнате снизу (debris)
Loose-пол room1(2,6) при падении даёт кусок, который в оригинале улетает в
комнату снизу (links.down) и приземляется на первый floor, превращая его в
debris (порт move_loose/loose_land seg007).  Для room1(2,6): room2(0,6)=empty
пролёт → room2(1,6)=floor приземление → debris.

Минимальная реализация (без анимации полёта куска, только персистентный
результат — мини-версия P0 из docs/gates_spikes_plan.md):
- pop_map: сигнал pop_loose_fell (tilepos+1 упавшего loose).
- pop_level: pop_room_col_landing(room,col) — ряд первого floor сверху вниз
  (пустые ряды пролетаются), иначе -1.
- roomtest: при pop_loose_fell вычисляет debris-приземление в комнате снизу →
  персистентный override (dbr_room/dbr_pos, живёт между переходами) →
  enter_room применяет поверх загруженных тайлов (floor→debris 0x0E).

Проверено в MAME: провал loose room1(2,6) → debris в room2(1,6), сохраняется
при повторных входах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:26:21 +03:00
snark13 bf7ddfd4da roomtest: L3 переходы вбок (право+лево) + план объектов (кнопки/гейты/пики)
Переходы в соседние комнаты по горизонтали (порт SDLPoP leave_room/
goto_other_room/find_room_of_tile), проверено в MAME (room2↔room3↔room9,
room1↔room5 и т.д.):

- pop_level: pop_room_load теперь извлекает и rightcol (col0 правого соседа),
  как leftcol — для коллизии/рендера правого шва.
- pop_map: pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg) — связи + кромки швов.
  get_tile(col=-1)=lcol (col9 левого), get_tile(col=10)=rcol (col0 правого),
  с гейтом по связи (нет соседа → стена) — порт find_room_of_tile.  check_leave
  (порт leave_room): передний край char_x_left<=54 / char_x_right>=201 (и
  обратные <=57 / >=198) → x∓140, сигнал pop_leave_dir (1=left,2=right).
- roomtest: rcol-массивы; enter_room задаёт edges; обработчик pop_leave_dir
  переключает комнату по pop_room_link.
- Фикс двойного перехода: без коллизии ЛЕВОГО шва Kid, войдя справа в соседа
  (curr_col=-1), стоял «в стене» → in_wall выбрасывал его на второй переход.
  Левый шов (get_tile(-1)=lcol) это чинит.

docs/gates_spikes_plan.md — ПОДРОБНЫЙ самодостаточный план на след. сессию:
интерактивные объекты (RAISE/DROP-кнопки, гейты, пики) + HP/смерть.  Контекст
текущего roomtest, форматы уровня (LINKLOC/LINKMAP @1440/1696), данные room6,
декод связи кнопка(0,2)→гейт room8(0,9), триггер пик (check_spike_below),
точные ссылки SDLPoP, фазы P0(персистентное per-room состояние+trob)/
S(пики+HP/смерть)/B(кнопки+гейты).

NB: L3-вверх (climb-up в комнату сверху) ещё не сделан.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:14:03 +03:00
snark13 7f3e32ddd6 roomtest: L2 — переход в комнату снизу (провал/спуск) + фиксы окклюзии
При пересечении нижней границы комнаты Kid переходит в комнату links.down,
продолжая падать/спускаться (порт SDLPoP goto_other_room/leave_room).

- pop_map check_leave_below (порт leave_room, seg002): триггер по Kid.y>=211
  (не curr_row) — единый для провала И спуска-зацепа/climbdown; репроекция
  координат в кадр комнаты снизу (y-=189, curr_row=y_to_row_mod4) + сигнал
  pop_fell_out.  Убран прежний преждевременный триггер y>=189 из do_fall
  (из-за него climbdown с curr_row=3 ждал y_land[4]=244 → пролёт через комнату).
- roomtest: enter_room(room) — загрузка комнаты в рабочие массивы + отрисовка
  фона в обе страницы; обработчик pop_fell_out переключает комнату по
  pop_room_link(down) или респавнит (нет комнаты снизу).  cur_room-трекинг.
- pop_loose_reset (pop_map) + pop_loose_mob_reset (pop_bg): сброс loose-состояния
  (индексировано позицией тайла) при смене комнаты — иначе течёт в новую.

Фиксы окклюзии при висе/спуске (сверено с SDLPoP):
- other_overlay_tile: пустой тайл НЕ окклюдирует — draw_tile для него рисует
  лишь фоновую сетку (BLUELINE «силуэт кладки»), которая в оригинале уходит в
  y-сортируемый midtable ПОЗАДИ Kid; у нас клалась поверх.  Пропускаем.
- pop_fore_over_kid: порт set_char_collision (seg006:0723) — char_bottom_row =
  y_to_row_mod4(obj_y), обёртка -1 → ряд 3 (Kid НИЖЕ комнаты при спуске), а не
  ряд 0.  Прежний кламп в 0 давал rT>rB → цикл окклюзии не выполнялся, нога
  рисовалась поверх пола.  single-room: ряд 3 клампится в 2 (передняя грань пола).

Проверено в MAME: провал вниз, спуск-зацеп/climbdown с кромки, окклюзия ноги.
Известно (позже): осколки упавшего loose в комнате снизу (перенос mob),
per-room modified-tile tracking (re-entry восстанавливает исходные тайлы), HP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:22:52 +03:00
snark13 21f978d441 roomtest: L1 — данные уровня из сырого res2001.bin вместо хардкода
Отказ от room1_data.h: уровень читается из сырого blueprnt DAT 1.0
(applications/PoP/SDLPoP/data/LEVELS/res2001.bin, формат — Table 6 POP-DAT).

- pop_level.c/.h: уровень грузится ОДИН раз в отдельную EMM-страницу (данные
  с offset 0x100, ISR-стаб в первых байтах — страница безопасна для W0-маппинга,
  как атласы).  pop_room_load(room) извлекает комнату в массивы приложения (W2):
  fg[30] (тайл-код = байт & 0x1F, верхние биты-модификаторы пока отброшены,
  как делал render_room.py), bg[30] (raw) + срезы соседей для кромок — правый
  столбец left-комнаты (leftcol), верхний ряд down-комнаты (belowrow).
  pop_room_link() — связи для будущих переходов (L2/L3).
- Рабочая копия ТЕКУЩЕЙ комнаты — в обычной памяти (W2, мутабельная: loose→
  empty); страница уровня маппится в W0 только на время извлечения.
- Проверено в MAME: комната 1 рисуется идентично.  Данные сверены байт-в-байт
  (fg&0x1F, bg raw, leftcol из room5 совпали); belowrow теперь из реального
  room2 (links.down=2), а не из прежнего частично-выдуманного хардкода.
- Память (small): DATA до 0xA260, стек от 0xBFFF — запас ~7.5КБ; EMM-страница
  уровня вне W1/W2.

NB: res2001.bin берётся из клона SDLPoP (data/ в .gitignore) — как и генерация
атласов через pop_pack_bg.py; в наш репозиторий сырой уровень не тащим.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:52:31 +03:00
snark13 8b30dc20c8 roomtest: loose-полы — тряска (knock), падение с окклюзией, фикс дабл-буфера
Проваливающиеся полы в roomtest (порт SDLPoP seg007/seg008), проверено в MAME:

- Падающий кусок (mob): правый край env-42 (w=26, доходит до mob_x+57)
  теперь полностью покрыт heal-коридором (MOB_W 56->64) — убран тёмный
  хвост-тень на полу 2,7 ПОСЛЕ падения.
- Окклюзия правого куска во ВРЕМЯ падения: после отрисовки плиты
  перерисовывается сосед-пол draw_tile(row,col+1) в том же кадре — край
  прячется за полом 2,7 (порт redraw_at_cur_mob: set_redraw_full+1).
- Тряска от сотрясения (knock): seqtbl-команды KNOCK_DOWN/UP в play_seq ->
  флаг knock -> do_knock(ряд) трясёт loose ряда из покоя (modif=0x80).
  KNOCK_DOWN в land-seq (приземление) и runcyc (footstep).
- Фикс «бесконечной тряски» (дабл-буфер): при завершении тряски (0x84->0)
  перерисовать покойный кадр плиты на ОБЕИХ страницах (loose_rest, 2 кадра) —
  иначе на одной странице застревает дрожащий кадр правой грани (живёт в
  тайлах col и col+1) -> мерцание через флип.

Инфраструктура/документация:
- app.mk: цель `make hdd` (упаковка в D: для MCP-моста MAME).
- docs: POP-DAT-FormatSpecifications.pdf/.txt как каноническая спецификация
  форматов ресурсов; ссылки в README/MSDOS_RESOURCE_FORMAT.
- README.md/CLAUDE.md для applications/PoP и roomtest (правило «SDLPoP —
  источник истины», порядок слоёв, режим отладки freeze 1/2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:27:47 +03:00
snark13 5ef084eca4 docs: loose_floors_plan — конкретика SDLPoP + подход интеграции
Дополнен точными данными из SDLPoP (сверено): кадровые таблицы
loose_fram_left/right/bottom, get_loose_frame, y_loose_land, delay=11;
триггеры с call-sites (check_press->make_loose_fall при стоянии на 11,
frame79-сверху; do_knock на жёстком приземлении; animate каждый кадр);
tile_is_floor(11)=1.  Плюс подход интеграции в наш движок: общая
мутабельная копия комнаты pop_map<->pop_bg + per-page запекание пустоты
после падения; тонкое место — перерисовка динамического тайла в дабл-буфере.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:43:43 +03:00
snark13 36a5a5e194 roomtest: фикс провала сквозь пол при беге с кромки (порт start_fall/in_wall)
Тап-бег с кромки [1,3] вправо -> Kid проваливался в стену col2/3 ->
респавн, вместо посадки на [2,4].  Сверено с SDLPoP seg006:

- start_fall: frame 9 -> seq_7_fall (был ошибочно seq_19 как у 13);
  frame 13 -> seq_19 (лишний dx(1)).  + хвост seg006:1099: тайл ПЕРЕД
  персонажем — стена -> Char.x = char_dx_forward(-1);
- in_wall переписан аутентично (seg006:1292): выталкивает ВПЕРЁД из стены
  (delta+4) на соседний тайл, а не назад вглубь (наш старый через
  dist_from_wall_forward давал отрицательный сдвиг -> Kid уходил в стену
  col2 -> проваливался).  distance_to_edge_weight + 6-delta/+4.

Проверено в MAME (пользователь): бег с кромки [1,3] -> посадка на [2,4].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:23:09 +03:00
snark13 237f780e0e roomtest: fore-окклюзия пола/стены над Kid (порт redraw_at_char/char2)
Kid — высокий спрайт: голова/руки торчат в ряд выше опорного, floor/wall
там должны перекрывать его.  Сверено с SDLPoP (seg003 redraw_at_char2 +
seg008 draw_tile_fore/draw_other_overlay/draw_floor_overlay):

- fore_tile (draw_tile_fore): стена рисует и WALL_FRAM_BOTTOM (нижняя
  грань-решётка), не только MAIN — руки при прыжке в потолок [2,7]->[1,7]
  уходят за стену;
- диапазон рядов форсит включение ряда над опорным (y_to_row верха спрайта
  мог схлопнуться из-за +60-сдвига);
- other_overlay_tile (draw_other_overlay): на КРОМКЕ пола (сосед слева пуст)
  перерисовать весь тайл поверх Kid — но ТОЛЬКО в позах захвата (78-79),
  виса (action 2/6), полёта (3/4), старта падения (bumped 102-106) и начала
  подъёма (135/136, до SEQ_UP на 141 Kid ещё в ряду ПОД полом).  Гейтинг
  как в redraw_at_char2 — при стоянии/беге ложной окклюзии нет;
- отладочный стоп-кадр: '1' заморозить / '2' продолжить (для разбора поз).

Проверено в MAME (пользователь): прыжок-в-потолок, короткий прыжок,
спрыгивание, подъём на [0,3] — окклюзия корректна.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:41:55 +03:00
snark13 023b45eb85 roomtest: двойная буферизация (два экрана + флип), тумблер SPACE
Убирает мерцание/тиринг при перерисовке слоёв (Kid/fore/пол-оверлей):
рендер всего кадра в скрытую страницу, tear-free флип на vblank.
Инфра libbgi (gfx_set_draw_page/visible_page/wait_vsync) уже была.

- фон комнаты рисуется в ОБЕ графические страницы (у каждой своя
  ОЗУ-копия — источник heal); палитра 0->1 уже синкалась gfx_pal_sync;
- kid_heal/kid_draw: прямоугольник Kid запоминается ПО СТРАНИЦЕ
  (kid_l*[2]) — при чередовании страниц heal стирает пиксели своей
  страницы (прошлый Kid там был 2 логических кадра назад);
- цикл: draw в back, 3x wait_vsync (пейсинг), gfx_set_visible_page(back);
- SPACE (edge) — тумблер: off = однобуфер (draw==visible==0) для отладки.

Мерцание при подъёме подтверждено устранённым в MAME (пользователь).
План: applications/PoP/docs/double_buffer_plan.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:07:03 +03:00
snark13 fce3e58830 applications/PoP/docs: план порта loose floors (проваливающиеся полы)
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose): хранение
состояния (tile 11 + curr_room_modif: 0 покой / 0x80.. тряска / 1..11
отсчёт падения), триггеры тряски (do_knock на приземлении → shake ряда)
и падения (make_loose_fall при стойке/зацепе на loose), падающий кусок
(mob → debris снизу, empty сверху), отрисовка по статусу (loose_fram_*
через get_loose_frame).  Порядок реализации L1..L5 + что нужно в нашем
движке (динамический тайловый слой: room_modif[], trob-очередь, mob).

ПЛАН — не реализация.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:31:23 +03:00
snark13 3c0baacbf6 libc/kbd: recovery по Rx-overrun SIO (залипание клавиш) + kbd_raw_sync
Симптом (интермиттентный): при отпускании shift+стрелка иногда стрелка
залипает.  Диагностика: на чистом одновременном release break-коды
обрабатываются верно (проверено MCP) → drain-логика ISR корректна.
Остаточное залипание = переполнение 3-байтного аппаратного FIFO SIO при
пачке скан-кодов (F0 12 E0 F0 74 = 5 байт) во время длинных DI-окон →
потерян break → залипание.

Фикс: трамплин после drain читает RR1 SIO (бит5 = Rx Overrun), при
overrun делает Error Reset (WR0=0x30) и взводит _kbdraw_overrun.
Новый kbd_raw_sync() (звать раз в кадр) по флагу сбрасывает всё
held-состояние _kbdraw_down (какой break потерян — неизвестно; реально
зажатые перечитаются).  pop_ctrl_tick зовёт kbd_raw_sync().  Буфер W2-
трамплина 288→320 (трамплин 244 Б).

ВНИМАНИЕ: путь overrun НЕ проверен детерминированно (баг интермиттентный,
зависит от тайминга DI) — ТРЕБУЕТ ПРОВЕРКИ на железе/в длинной сессии.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:26:42 +03:00
snark13 cbae48dbc4 applications/PoP/roomtest: пол-оверлей при подъёме (draw_floor_overlay)
Проблема из динамики: при подъёме [1,2]→[0,3] нижняя часть Kid, которая
физически за полом назначения, просвечивала.

Порт SDLPoP draw_floor_overlay (seg008:1457): на кадрах подъёма 137..144,
если тайл-назначения floor-подобный (floor/pillar/stuck/torch) И тайл СЛЕВА
пуст (кромка), передний край пола (floor_left_overlay[frame-137] =
{32,151,151,150,150,151,32,32}) + низ пола рисуются ПОВЕРХ нижней части Kid.

- pop_bg: climb_overlay_tile + доп-проход в pop_fore_over_kid (новый параметр
  frame) на кадрах 137..144.
- pop_pack_bg.py: CLIMB_OVERLAY_ENV_IDS={32,150,151} явно добавлены в env-фон
  (не попадают в used render_room — рантайм-анимация); env4 count 16→24.
- roomtest: передача Kid.frame в pop_fore_over_kid.

Проверено в MAME (кадр 137): при подтягивании видна только голова/плечи Kid
над кромкой [0,3], нижняя часть скрыта за полом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:37:56 +03:00
snark13 419d5e4eff applications/PoP/roomtest: фикс ухода Kid в стену при прыжке с кромки
Баг (из динамики): стоя на кромке [1,3]/[1,4] лицом вправо + Up, Kid
телепортировался/застревал в стене col8.

КОРЕНЬ (трасса MCP): jumpup(seq_14) на кромке → приземление над ямой →
падение; на кадрах падения action=3 (midair) check_bumped был НЕ заглушён
(guard покрывал только freefall/hang/climb-кадры).  Kid дрейфовал к стене,
get_tile вне рядов давал WALL, dist_from_wall_forward/x_bump[] — мусор с
большим отрицательным сдвигом → Kid.x=char_dx_forward((int8_t)d) → underflow
uint8 X (0→255) → долёт до col8 и застревание в стене.

Фикс check_bumped: заглушить и в action MIDAIR (кадры падения), и при
curr_row вне [0..2] (падение мимо пола — тайлы вне комнаты = WALL, bump-мусор;
fell_out ловит do_fall).  Проверено в MAME: на кромке Kid больше НЕ уходит
в стену — чисто падает (в тесте сброс на старт).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:20:48 +03:00
snark13 5935724897 applications/PoP/roomtest: стены в fore-проходе поверх Kid
Проблема из динамики: за стенами Kid не прятался (стена [2,9] должна
перекрывать).  В PoP основная грань стены (wall_fram_main) добавляется в
FORETABLE (seg008:712) — перекрывает персонажа.  У нас fore-проход
рисовал только fore_id, а у стены (0x14) fore_id=0.

fore_tile: для стены (code==20) рисуем WALL_FRAM_MAIN + wall_pattern(,,1)
поверх Kid (как draw_tile_fore), а не только fore_id.  Проверено в MAME:
Kid прячется за правой стеной col9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:47:10 +03:00
snark13 78f2aaec1a applications/PoP/roomtest: fore-слой поверх Kid (передние тайлы)
Порт SDLPoP seg003 redraw_at_char + seg008 set_char_collision: после
kid_draw передний слой (fore_id = foretable-кусок) тайлов ФУТПРИНТА
спрайта Kid перерисовывается ПОВЕРХ него — то, что по изометрии перед
персонажем (передние грани колонн/ворот/большой колонны/щебня).  Стены и
факелы имеют fore_id=0 → остаются сзади (в статическом фоне).

- pop_bg: pop_fore_over_kid(obj_x,obj_y,w,h,dir) — считает футпринт
  (char_x_left/right, col_from_x, y_to_row_mod4) рядов top..bottom ×
  колонок left..right (≤2×2=4 тайла) и рисует fore_id каждого в
  GFX_BANK_SPRITE (видео-ОЗУ; kid_heal восстановит из теневого фона,
  fore в нём запечён pop_room_draw).
- pop_kid: kid_fp_obj_x/y/width/height — метрики последнего кадра
  (obj_x ЛОГИЧЕСКАЯ, до ×8/7) для футпринта.
- roomtest: вызов pop_fore_over_kid после kid_draw.

Проверено в MAME: Kid, идя влево мимо колонны под навесом (row1 col3),
корректно уходит ЗА её переднюю грань (скрывается) и выходит с другой
стороны; задние колонны/факелы остаются сзади.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:34:32 +03:00
snark13 ea57a03543 applications/PoP/roomtest: K4 — зацеп/вис/подтягивание/спуск
Порт SDLPoP seg004/005/006 «hang state»:

- pop_map: check_grab (зацеп за уступ в падении по Shift → seq_15 → вис),
  can_grab/can_grab_front_above + tile-запросы над/за персонажем;
  pop_jump_up_seq (check_jump_up: ↑ в стойке = чистый прыжок seq_28/14
  ЛИБО прыжок-с-зацепом seq_8/24/16 за уступ выше → запрыгнуть на этаж);
  pop_hang_* (climb_up seq_10/73, hang_fall seq_23/11, hang-у-стены);
  pop_down_action (спуск seq_68 у края лицом от края / отступ / присед).
- pop_ctrl: control_hanging/can_climb_up/hang_fall, control_jumpup,
  jump_up через pop_jump_up_seq, down_pressed через pop_down_action;
  pop_ctrl_shift_held() для check_grab.

Ключевой фикс check_bumped (seg004 guard'ы): не бампить при action
hang_climb/hang_straight, на кадрах подъёма/спуска 135..148 И на кадрах
виса 87..99.  Без последнего спуск (seq_68) на кадре frame_91 (action
ещё midair, act(hang_climb) идёт следующим опкодом) ловил отскок у стены
и рвал цепочку hang→hang_fall→seq_11, приземляя не туда.

Проверено в MAME (HDD-тест, покадровая трасса через мост): прыжок-с-
зацепом [row2 col4]→вис→подтягивание→[row1 col3]; спуск [row1 col3]→
[row2 col4] с корректной позицией у стены (x=122, совпадает с SDLPoP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:07:20 +03:00
snark13 cd8d566d82 applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн
Порт PoP на Sprinter.  Текущий PoC — applications/PoP/roomtest/:
комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре
и коллизией с картой.

- roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип,
  seqtbl-анимация), pop_ctrl (порт control() PoP на held-state
  kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены,
  падение/приземление, отскок seq_47, вертикальный прыжок K4.1).
  MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge).
- toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract —
  распаковка res-графики MSDOS в атласы + композиция комнат.
- toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD),
  png_strip.py / room_compose.py (ассет-пайплайн).
- docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT).
- bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип).

.gitignore: build-артефакты applications/*/*/*; исключены внешние
референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и
оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:51:08 +03:00
snark13 484b18d10c libc+libbgi: raw-клавиатура (held-state) + column-major блит спрайтов
libc/kbd: kbd_raw_open/close/down — сырой PS/2-канал клавиатуры с
held-state (битовая карта _kbdraw_down[512], EXT-клавиши +256).  Пока
raw открыт, кадровый IRQ-трамплин перехватывает байт SIO у DSS и
декодирует make/break (0xF0/0xE0-префиксы) сам.  FIFO вычерпывается В
ЦИКЛЕ (приёмный буфер SIO 3 байта; пачка break-кодов при одновременном
отпускании иначе теряется → залипание клавиши).  Буфер W2-трамплина
поднят 224→288 Б под выросший обработчик.

libc/conio: kbd_mod_state() — live-состояние модификаторов (ESTEX
CTRLKEY $33h), Shift/Ctrl/Alt/Lock прямо сейчас, KBD_MOD_* маска.

libbgi: gfx_blit_cols(x,y,img,flip) + _bgi_blit_cols_raw — блит
column-major спрайта вертикальным accel-проходом, бесплатный
горизонтальный флип (sstride<0), клип по экрану.  Для персонажей.

libbgi/atlas_load: восстанавливать W3 ДО записи a->count (atlas_t в
--bank памяти резолвится через W3; count оставался мусором).

tests/kbdraw — тест raw-клавиатуры; size-baseline +kbdraw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:48:32 +03:00
snark13 e14b6745f7 examples/rpgwalk: переключатель FPS-делителя (1/2/3) — проверка пейсинга
Клавиши 1/2/3 зовут gfx_set_fps_div(n) на лету (дефолт n=1 не меняет
поведение).  Проверено в MAME: при n=2 FPS-метр стоит РОВНО на 024
(48.83/2) все 8 секунд без плавания — логический кадр = ровно 2 vsync'а,
скорость персонажей (пиксель/сек) постоянна.  +808 Б (демо тянет код
делителя).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:42:28 +03:00
snark13 1b4fbeaa6b libbgi: FPS-делитель gfx_set_fps_div(n) поверх цепочки кадровых IRQ
Логический кадр = ровно n кадровых интервалов (1=50/2=25/3=~16.7 fps);
при переполнении слота — выравнивание на ближайший фронт (без дрейфа
фазы, в отличие от наивного «жди n фронтов»).

Механика: фоновый счётчик _gfx_frame_tick инкрементит _gfx_frame_isr,
поставленный в СВОЙ слот цепи (irq_chain_add); gfx_set_fps_div(1) снимает
только этот слот (irq_chain_remove), не трогая хендлер приложения.
gfx_wait_vsync: ветка n>=2 (счётчик + halt) перед лучевым поллингом;
поллинг вынесен в static gfx_wait_vsync_beam (функция с хвостовым __asm
не должна иметь переходов через asm — SDCC не эмитит эпилог-метку;
ранний return делителя в чистом-C gfx_wait_vsync).

Файлы: common/_gfx_fps_state.c (данные), _gfx_frame_isr.c (ISR),
gfx_set_fps_div.c (сеттер, единственная ссылка на irq-механику → DCE).
Работает tiny/big/huge (цепочка all-modes); small для мелких программ
= EINVAL.

Проверено MAME (tests/fpsdiv): n=1/2/3 → 20/40/60 кадров на 20 wait'ов
(drift=0); n=2 с рендер-заглушкой ~1 кадр → период держится 2
(поглощение перерасхода, наивный путь дал бы ~60); huge идентично;
small = EINVAL graceful.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:36:37 +03:00
snark13 2c6f4e33c3 libc/irq: цепочка кадровых обработчиков + all-modes W1-remap
_irq_user → _irq_chain[4]+_irq_chain_n (W2-BSS); трамплин tr_frame
проходит слоты (один тяжёлый сейв на всю цепь, пустой слот пропуск).
API irq_chain_add (0/-1+ENOMEM) / irq_chain_remove(h); irq_install →
обёртка chain_add (EBUSY исчез), irq_remove() рвёт всю цепь; refcount
таблицы на первом/последнем слоте, мутации под IRQ_DISABLE. Лимит
4 кадровых + 1 CTC.

All-modes: трамплин copy-safe (только jr/djnz + литерал jp 0x0038),
_irq_table_ref копирует его в _irq_tramp_w2buf (W2) когда оригинал в W1
(small/huge), вектор → на копию; вокруг вызова хендлеров восстанавливает
базовую W1-страницу (_irq_app_w1_page = IN 0xA2). Хендлер может лежать
где угодно в плоском 0x4000-0xBFFF.

Проверено в MAME (tests/irqtest, 2 хендлера): tiny/big/huge — chain
h1=h2 → remove h2 → h1 жив/h2=0; huge = код в W1, remap работает.
Follow-up: CTC в small/huge = EINVAL (нужна W2-копия _irq_ctc_tramp);
small для мелких программ (BSS в W1) = irq_install EINVAL, safe.

Доки: im2_isr_design (цепочка), sprite-api §9е (делитель поверх цепи),
fast_ram §8. rpgprof: gfx_sprite_ysort в профиль (+230 Б baseline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:23:39 +03:00
snark13 6b6986d9b6 docs: W1-перемапы DSS при EI подтверждены артефактом; дизайн цепочки irq
wpiset на порт W1 (0xA2) + iff1 в dev-MAME: DSS перемапливает W1 при
ВКЛЮЧЁННЫХ прерываниях во время системных вызовов (файловые, загрузка
exe, видео; страницы 0xFE/FF/F3/0x50, PC ядра 0x15xx-0x2Exx) —
ограничение irq_install «только tiny/big» обосновано, handler в
W1-коде небезопасен принципиально.  §9е дополнен фактом; в TODO —
дизайн цепочки irq-обработчиков (массив слотов в W2, chain_add/remove,
irq_install как обёртка; реализация по потребности).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:34:21 +03:00
snark13 b9ddce8d34 libbgi: спрайты — кэш адреса кадра, DDA+asm тик, Y-сортировка со слоями
Оптимизации A+B (профиль rpgwalk-15: активная часть кадра 410К → 307К
тактов из 430080; лимит спрайтов 16×16 на стабильные 48.8 fps: 14 → ~21):

- (B) sprite_t.src/stride — готовый адрес кадра: считают только
  sprite_frame (теперь функция, одно умножение на СМЕНУ кадра) и тикер
  (±an_step БАЙТ инкрементально); блит-ядра принимают src+stride,
  img/sx/sy из сигнатуры ушли.  Блит 177К → 146К на кадр.
- (A) тик 100К → 37.7К: tween переформулирован Брезенхэм → беззнаковый
  DDA (mv_rem/mv_acc, «приехали» = rem==0 — без знаковых 16-бит
  сравнений), tick_move и tick_anim — ручной asm (SDCC спиллит такие
  функции в IX-фрейм ~100 обращений; C-реструктуризации не помогали —
  проверено кодогеном).  Биты an_flags переименованы по категориям
  (_SPR_STRIP_HORZ, _SPR_PP_BACK).

Y-сортировка (gfx_sprite_ysort, идеи пользователя — 8-бит ключ,
персистентность):

- painter's algorithm по ключу {layer:8, clamp_y:8}; поле
  sprite_t.layer (в КОНЦЕ структуры — asm-офсеты не сдвигает): слои
  сцены в одном массиве/одном sprite_update;
- ПЕРСИСТЕНТНАЯ asm-таблица {key16, ptr16}: resort порядка прошлого
  кадра (почти линейно), rebuild при смене arr/count; массив
  приложения не трогается; ~28К/15 спрайтов (с нуля было 44К);
- компоненты YSORT_Y/YSORT_LAYER отключаемы независимо масками ключа
  (без ветвлений в сортировщике); ВНИМАНИЕ: mode=1 значит Y-only,
  полный порядок = YSORT_Y|YSORT_LAYER;
- funcptr-DCE: выключено = код и таблица не линкуются (rpgwalk −190 Б);
- ПРАВИЛО в sprite.h: два sprite_update на страницу запрещены (heal
  второй группы стирает спрайты первой — ОЗУ-копия чистая).

Попутные фиксы:

- libbgi/Makefile: .rel зависят от заголовков (HDRS) — stale .rel со
  старой раскладкой sprite_t молча ломал рантайм;
- rpgprof: --memory small (перерос tiny: BSS вылезал за W2 → мгновенный
  «Unexpected application termination»; mkexe это пока не ловит);
- tests/spranim: проверки переведены на кэш src, добавлены T6 (reframe
  после тикера) и T7 (Y-сортировка: порядок, слои, персистентный
  resort, LAYER-only) — 7/7 PASS в MAME.

Доки: §9д — новый бюджет (19.5К/спрайт), §9е — ПЛАН FPS-делителя
(frame pacing, gfx_set_fps_div); TODO — дизайн цепочки irq-обработчиков.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:19:24 +03:00
snark13 0eec977630 libbgi: sy*img_w умножением вместо O(sy)-цикла — rpgwalk 24 → стабильные 48 fps
Профилирование rpgwalk в dev-MAME (watchpoint на OUT-маркеры +
totalcycles) показало: blit 8 спрайтов ел 17 мс из 20.5 — 85% в цикле
`while (sy--) src += img_w;` blit-обёрток (писался под «обычно sy==0»,
а вертикальные ленты атласов дают sy до 176 → до 64К тактов на кадр).

Фикс: src += sy*img_w через __mulint (O(1); __mul16 адаптивен — при
sy < 256 крутит 8 итераций, ~500Т) + if (sy): горизонтальные ленты и
одиночные спрайты не платят и за умножение.  Blit: 45К → 11.8К
тактов/спрайт.

Замерен бюджет кадра (docs/sprite-api-design.md §9д): кадр 48.83 Гц =
430080 тактов @21МГц; спрайт 16×16 ≈ 26К (тик 7.3К + heal 6.4К +
blit 11.8К) → лимит стабильных 48 fps = 14 спрайтов (15 — 94% кадров,
16 — на грани).  FPS-плашка bar+outtextxy стоит ~210К (полкадра!) —
HUD рисовать putimage-заготовкой.

examples/rpgwalk/rpgprof.c — профилировочная копия демо: визуальный
профайлер (цвет бордера по секциям кадра) + маркеры для тактового
профайла через wpiset дебаггера (рецепт в шапке и §9д).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:54:37 +03:00
snark13 02f7afe765 examples/rpgwalk: FPS-метр
Плашка слева-сверху (значение раз в секунду, рисуется на обеих
страницах — fps_draw=2, как в examples/space).  ~39 fps на 8 ходящих
персонажах (кадр на грани 20 мс — частично двухvsync'овые кадры).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:38:04 +03:00
snark13 b010d24792 examples/rpgwalk: 8 RPG-персонажей ходят по травяному полю
Демо на реальном арте (third_party/16x16-RPG-characters, bard):
conv_sprites.py режет PNG 192×128 (8 персонажей = блоки 3×4 кадров:
ряды вниз/влево/вправо/вверх × кадры маятника 0/1/2) в 8 вертикальных
лент по 12 кадров и строит bard.pal (слоты 0-15 EGA + цвета PNG с 16,
прозрачность → 0xFF).  8×12 кадров не лезут в одну EMM-страницу —
ДВА атласа по 4 персонажа (движок сам переключает страницы W0).

Палитра из файла: gfx_pal_fload + gfx_pal_sync.  Смена направления =
sprite_anim(dir*3, dir*3+2, PINGPONG).  Правила хождения (двухфазная
машина на sprite_moveto): до края → разворот 180° → случайная точка
(не меньше четверти экрана) → поворот ±90° → снова до края.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:27:00 +03:00
snark13 6dea6955c2 libbgi: gfx_pal_sync + палитра ↔ файл (gfx_pal_fsave/fload); все PASS
- gfx_pal_sync(): палитра страницы 1 := палитре 0 (все 256 записей,
  чанками по 64 через общий буфер _gfx_pal_buf в W2) — обязательный
  шаг дабл-буфера (грабли examples/space: без него флип на страницу 1
  чёрный).  examples/space переведён на хелпер.
- gfx_pal_fsave(pal, path): 256 записей × 4 Б (B,G,R,0 — родной формат
  BIOS $A4) = 1024 Б.
- gfx_pal_fload(pal, path): принимает и усечённый файл (64 Б = палитра
  16 цветов) — грузит сколько есть, остальные слоты не трогает;
  возвращает число записей.

tests/palfile (MAME dev, все PASS): fsave; fload восстанавливает
испорченные слоты (n=256); усечённый файл на 2 записи чинит только их
(n=2, слот 2 не тронут); sync чинит испорченный слот палитры 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:51:49 +03:00
snark13 8792594c5a examples/space: демо полного спрайтового стека (атлас W0 + авто-анимации)
Всё сразу: .atl с диска (mkatlas.py из генерённых лент) → atlas_load в
EMM-страницу (W0, ISR-стаб) → движок sprite_t.page → авто-анимации:
5 астероидов (ANIM_LOOP вращение + sprite_moveto к случайным целям
разных скоростей; по прибытии — взрыв ANIM_ONCE в точке + новая цель,
по SPR_ANIM_DONE взрыв прячется), маяк ANIM_PINGPONG; дабл-буфер,
FPS-метр, ESC.  ~48 fps (vsync-кап).

Грабли по дороге (оба — прикладные, не библиотека):
- фон рисовался rand()'ом с разными последовательностями на страницах
  → мерцание звёзд; фикс — фиксированный seed на draw_space;
- НЕ была скопирована палитра страницы 0 → 1 (у каждой страницы своя,
  см. gfx.h) — страница 1 показывалась чёрной, флип мигал
  «сцена/чёрный»; fps-плашка теперь рисуется на ОБЕИХ страницах
  (fps_draw=2 кадра при смене секунды).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:40:48 +03:00
snark13 eb9ca0179d libbgi: авто-анимация спрайтов — кадровая + tween (tests/spranim все PASS)
Реализация §9г по требованиям пользователя:
- sprite_anim(first,last,speed,mode): ANIM_LOOP / ANIM_PINGPONG /
  ANIM_ONCE (one-shot замирает на last), | ANIM_HORIZ для
  горизонтальных лент.  Смена кадра в тике = ±an_step к оси ленты —
  без умножений (осевое смещение first считается в setup циклом).
- sprite_moveto(tx,ty,max_step,interval): Брезенхэм порциями
  ≤max_step вдоль большей оси (меньшая — err-аккумулятором,
  нелинейные шаги Y сами собой), деления нет.
- Одновременность кадровой и tween — независимые поля/биты.
- Статус: sprite_anim_status() — битовое поле SPR_ANIM_ON/DONE +
  SPR_MOVE_ON/DONE; sprite_anim_frame() — текущий индекс кадра;
  sprite_moving().
- Стопы: sprite_anim_stop(frame | -1 = текущий);
  sprite_move_stop(0 = замереть / 1 = прыжок в цель + DONE).
- Тикер _sprite_tick — проход 0 sprite_update через funcptr
  _spr_tick_fn (DCE: без sprite_anim/moveto код не линкуется,
  цена — один if на кадр; +40 Б программе с движком, sprite_t +18 Б).

tests/spranim (MAME dev, все PASS): LOOP/PINGPONG (разворот на границе
без удвоения краёв)/ONCE+DONE; пример (0,0)→(100,50), шаг 5,
интервал 4 → ровно 80 кадров, Y идёт 3/2/3/2; стопы обоих видов.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:14:31 +03:00
snark13 0e935343d9 libbgi: W0-атласы спрайтов — загрузчик, движок, упаковщик (все проверки PASS)
Реализация §3.1/§9в: атлас = один .atl-файл = одна EMM-страница,
подключаемая в W0 на время блита.

- atlas_load: read() файла целиком в страницу через W3 + патч ISR-стаба
  (0x38: JP _gfx_w0_isr; 0x66: RETN); atlas_free/atlas_image/
  atlas_sprite_init; gfx_w0_map/unmap для ручных вызовов.
- _gfx_w0_isr (стаб из tests/w0page): свап на ядро DSS → честный 0x38 →
  restore спрайт-страницы; покрывает IM1 и IM2-чейн.
- sprite_t.page (0 = обычная память); sprite_update в блит-проходе
  мапит страницу по смене (один OUT на атлас), эпилог возвращает DSS
  (+52 Б на движок — цена фичи).
- toolchain/mkatlas.py: PNG (indexed) / raw → .atl; заголовок 0x100,
  каталог 0x68 (19 лент), файл-офсет == офсет страницы == W0-адрес.
- Сплит _gfx_sprite_fns → _gfx_blit_fn.c + _gfx_heal_fn.c (1 указатель
  = 1 модуль): putsprite-only программа не тянет heal-ядро (spriteclip
  ловил +292 Б; теперь −68 Б от эталона).

tests/atlas (MAME dev, PASS): загрузка (count/страница), каталог и
данные по W0-адресам (заголовок ленты через gfx_w0_map), 3 спрайта из
двух лент через движок — пиксели проверены read_vram побайтно
(0x10/0x12/0x21, прозрачный угол = фон).

docs: §9г — предложение авто-анимации (кадровая ±step без умножений,
tween-Брезенхэм порциями, тикер через funcptr) — ОБСУЖДАЕТСЯ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:52:00 +03:00
snark13 c4a512200f tests/w0page + docs §9в: атласы в страницах W0 — probe-тест (все PASS)
Схема: атлас в EMM-странице, на время sprite_update страница
подключается в W0.  Все прерывания исполняют 0x38 текущей страницы W0
(IM1 напрямую, IM2-трамплин чейнит jp 0x0038) — защита: стаб в первых
0x100 байтах страницы (0x38: JP на W2-хелпер: свап на страницу DSS →
честный 0x38 → restore спрайт-страницы; 0x66: RETN).

Проверено в dev-MAME (tests/w0page, все PASS):
  P1 ESTEX READ в W3-замапленную страницу;
  P3 IM1: ~5 c busy-цикла с EI при странице в W0 — жив, сентинел цел;
  P4 IM2: 100 кадровых прерываний посчитаны при подключенной странице
     (тракт трамплин→чейн→стаб→DSS→restore);
  P5 accel-блит src=0x0100 (W0) — пиксели верны по ОЗУ-копии.

Формат .atl в §3.1 дополнен: атлас ≤ 16К−0x100 (одна страница, один
файл), вариант II — 0x100-заголовок, файл-офсет == офсет страницы ==
W0-адрес, загрузка = read() целиком + патч стаба.  На железе
перепроверить.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:30:24 +03:00
snark13 88a00bb4ec docs: атласы — рекомендация по компоновке + предложение файл-формата .atl
Решение: in-memory формат остаётся (getimage + sx/sy, ленты/сетки).
Рекомендация: кадры одного спрайта — одного размера с полной сеткой;
для спрайтов разных размеров — контейнерный формат .atl (каталог +
независимые getimage-ленты, паддинг невозможен по построению).
Формат — предложение, реализация по потребности первого приложения.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:44:49 +03:00
snark13 ee1ca00c6f libbgi: funcptr-диспетч clip/noclip спрайтовых ядер + снятие src[0]-фикса
Диспетчеризация clip/noclip через указатели _gfx_blit_fn/_gfx_heal_fn
(common/_gfx_sprite_fns.c, дефолт clip): gfx_sprite_clip() — теперь
модуль, переключает указатели один раз; sprite_update/putsprite/
movesprite зовут через указатель — ветка if(clip) из горячего цикла
убрана (съедала половину выигрыша noclip). Программа без вызова
gfx_sprite_clip() noclip-ядра не линкует.

Замер dev-MAME (16 шаров, uncapped): clip 50 → noclip 60-61 fps
(+20-22%). Регресс tests/sprites (A/B PASS), size-check OK
(balls −237 Б, sprites −3177 Б — отвязались лишние ядра).

Фикс CPU-байта write-триггера (preread + EX AF,AF') снят: точная
dev-MAME эмулирует ПЛМ, подавляющую CPU-байт при активном burst'е —
подтверждено по байтам VRAM (tests/blitw col0 = GREEN через
read_vram MCP-моста). Для heal фикс был избыточен всегда (банк 0x50
перезаписывает dst[0]). Строки фикса оставлены закомментированными
в трёх leaf'ах на случай отличий реального железа; шапки и §9а/§9б
дизайна обновлены. НА ЖЕЛЕЗЕ ПЕРЕПРОВЕРИТЬ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:37:54 +03:00
snark13 72ce66275e libbgi: спрайтовый движок v2 + accel-блит/heal leaf'ы + noclip-путь
Спрайтовая графика поверх accel block-copy (docs/sprite-api-design.md):

- Ядро блиттинга: leaf'ы _bgi_blit_rows_raw (dst фикс, только src-страйд) /
  _bgi_copy_rows_raw (getimage) / _bgi_heal_rows_raw (src==dst). DI один на
  спрайт (санкция: малый спрайт под одним DI аудио не рвёт); src[0]-фикс
  снят (точная MAME подавляет CPU-байт триггера записи — на железе
  перепроверить; для heal был избыточен и снят безусловно).
- Общие bracket-free ядра _gfx_blit_full/_gfx_heal_full (полная ширина:
  клип по экрану + split >256 для putimage) + лин _gfx_blit_sprite/
  _gfx_heal_sprite (кадр ≤64, без split, 8-бит w/h) + noclip-варианты
  (клип-кода нет → полный codegen-win).  Имя *_full (не *_clip) — «clip»
  двусмысленно (sprite-ядра тоже клипуют; различитель — ширина/split).
- Движок retained-модели <sprite.h>: sprite_init/update/flip + inline
  move/frame/show/hide/touch; drawn[2] per-page внутри структуры; кадр —
  двухпроходно heal ВСЕ -> блит ВСЕ под одной W3-скобкой/банком на проход.
- Флаг gfx_sprite_clip(on/off): приложение, само следящее за границами,
  отключает клип (~+19% на анимации; диспетч пока через if — funcptr далее).
- putsprite/movesprite/gfx_blit/putimage(COPY)/getimage переведены на ядро.

Тесты: examples/balls (движок, дабл-буфер, boundary-тест клипа),
tests/sprites (RAM PASS, клип 4 края, атлас), tests/blitw (trig-leak),
tests/spriteclip (hardware-probe: железо НЕ режет за краем -> клип нужен),
tests/blitperf, tests/gfxbanks. size-baseline обновлён (53 программы).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:51:52 +03:00
snark13 78161561e7 sprinter-cc: --max-allocs 100000 по умолчанию для пользовательского кода
Тот же приём, что во fast-библиотеках (786836e): агрессивная
регистровая аллокация SDCC.  --max-allocs N переопределяет (меньшее
значение = быстрее компиляция).

Замер (47 программ): mdview -235, banklocl -149, fbench -110,
filetest -90, ls -81, solidt -69 и т.д.; 4 микро-роста (+1..+12 —
другие развязки аллокатора, шум).  Полная сборка ~2 мин -> ~4:15.
MAME: filetest/bgitest/seek (с big.txt, скриншот пользователя) зелёные.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:35:44 +03:00
snark13 786836e2e7 libc+libbgi: fast-версии собираются с --max-allocs-per-node 100000
Идея из mdview2 (memory/mdview2_size_budget): большее время компиляции
покупает более агрессивную регистровую аллокацию SDCC.  Применено к
fast-вариантам обеих библиотек (sprinter.lib, bgi256.lib); safe-версии
остаются на дефолте — быстрая пересборка для отладки.

Замер (47 программ, роста нет): solidt -507, filetest -506, fbench
-461, bgi_img -339, bgitest -316, gfx_dbuf -316, fdmax -278, errno
-179, gfx_demo -173, accfill -143, ptime -128, stattest -117, cbl* -53,
остальные до -28.  Время сборки: libc fast 17с -> 68с, libbgi 43с
(обе) — терпимо.  MAME: filetest + bgitest зелёные.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:24:50 +03:00
snark13 9f8aa6fc28 libc: две версии библиотеки — sprinter.lib (fast) / sprinter_safe.lib
Симметрично libbgi (bgi256/bgi256_safe): fast = -DLIBC_NOCHECK,
дефолт sprinter-cc; safe линкуется по --safe (флаг уже существовал).

Под LIBC_NOCHECK вырезаны ТОЛЬКО параметр-валидации:
- fgetc/fputc: NULL-check в горячей asm-обёртке (~11Т на каждый байт);
- fgets/fputs/fread/fwrite: NULL ptr/fp и EBADF на неверное направление
  потока; ftell/fseek/ungetc/fclose: NULL fp; cputs: NULL s.
НЕ тронуты: критичный _fd_guard (9-й OPEN вешает DSS — в обеих
версиях), функциональная маршрутизация (консоль/направление/hold),
cold-path валидации (fopen/cbl_open/irq/settextmode — экономии ноль).

libc/Makefile — dual-build по образцу libbgi (build/fast + build/safe,
общий стейл-контроль).  Корневой Makefile: в TESTS добавлены bgitest,
bgi_img, accfill, cblstream — раньше не собирались корневым make и
выпадали из size-check при чистой пересборке.

Дельты fast vs старая (safe-семантика): filetest -481, fbench -384,
solidt -180, errno -71, остальные -3..-9; роста нет.  Проверено в MAME:
filetest fast и safe (--safe, sprinter_safe.lib) — прогоны идентичны.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:11:01 +03:00
snark13 c9ac0999fd libc: тонкие аксессоры → inline в заголовках (по размерному критерию)
Inline (чистый C99 `inline` без static, паттерн из libbgi/5de2f06):
textattr, get_text_attr, get/set_putch_raw_mode (conio.h; g_text_attr
и pc_raw_mode объявлены публично), isatty (unistd.h — сворачивается в
константу при константном fd).  Модули удалены (5 шт).

«Толстые» кандидаты НЕ инлайнены — критерий проверен замером на 47
программах: feof/ferror/clearerr (тело ~12-15 байт с NULL-проверкой,
filetest +56 Б при инлайне) и textcolor/textbackground/set_text_attr
(RMW-маски, conio2 +13 Б) остаются модулями — при 2+ сайтах вызова
инлайн крупнее call+общее тело.  Правило: инлайнить только тела
<= ~6 байт на сайте или сворачиваемые константами.

Дельты: solidt -94, hello -24, mouse -7, bios_text -7; conio2 +11
(textattr×5 — паритет, принято за скорость).  z80.lib SDCC не содержит
feof/isatty — маскировки удалённых модулей нет, провал инлайна = ошибка
линковки.  Проверено в MAME: conio2 (атрибутная матрица), filetest
(полный прогон).  В size_baseline также вошли gfx_dbuf +608/gfx_demo
+25 — это первый чистый релинк run-рендера текста (ba09c0b), не inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:53:37 +03:00
snark13 5de2f06cfb libbgi: тривиальные аксессоры → inline в заголовках (17 модулей удалено)
setcolor/getcolor/setbkcolor/getbkcolor/getmaxx/getmaxy/getmaxcolor/
getx/gety/moveto/moverel/setfillstyle/graphresult (graphics.h) и
gfx_get_bank/gfx_set_bank/gfx_get_draw_page/gfx_get_visible_page
(gfx.h) определены inline в публичных заголовках; state-переменные
объявлены там же (хранилище прежнее — _bgi_state.c/_gfx_state.c).

Именно `inline` БЕЗ static: проверено артефактами (.asm) — SDCC 4.5
инлайнит вызов при всех наших флагах (--opt-code-size/--opt-code-speed/
--max-allocs) и не эмитит standalone-тело; `static inline` эмитил бы
мёртвую копию каждого аксессора в КАЖДЫЙ включивший модуль.  Отказ
инлайнить = громкая ошибка линковки (все 47 программ слинковались).

Экономия ~30-40Т на вызов, минус 17 .rel; по _CODE размер-нейтрально
(сайт вызова ≈ телу).  size_baseline: bgitest +267/accfill +528 — это
НЕ inline, а run-рендер текста из ba09c0b (draw_scaled потянул
vspan_raw+vfill256 и сам вырос) — цена за ~2-6× скорость текста.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:21:26 +03:00
snark13 ba09c0bd05 libbgi: _bgi_draw_scaled — рендер текста run'ами вместо поштучных плотов
Строка глифа сканируется на прогоны единичных битов; прогон n битов =
прямоугольник n*size×size → hspan/vspan-примитивы (одиночный пиксель —
plot_raw).  Координаты инкрементальные (+= size за бит) — умножений на
пиксель нет.  Прогон клипится до span'а — клип теперь работает и в
fast-сборке (span-raw диапазон не клипят).  ~2× на size=1, ~6× на
size=4.  Проверено в MAME (bgitest: масштабы 1..4 + VERT — пиксель в
пиксель).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:36:53 +03:00
snark13 38bfabb2ca libbgi: __preserves_regs(d,e) на raw-примитивы (контракт, без эффекта сейчас)
plot/read/hspan/vspan_raw читают D/E (y в DE), но не пишут — аннотация
задокументирована в _bgi.h.  Честное измерение (полная пересборка с/без,
diff всех .asm в common/ и bgi256/): кодогенерация SDCC 4.5 НЕ меняется —
вызывающие держат локали в IX-фрейме и перезагружают DE перед каждым
вызовом, спасений DE вокруг вызовов не было.  Оставлено как контракт на
будущее (изменение вызывающих/компилятора); при правке asm сверять
клоббер-лист.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:35:32 +03:00
snark13 40e896a73d libbgi: фикс утечки W3-скобки в bar() + удалить дубликат _bgi_read
bar(): rectfill-ветки выходили ранним return без _bgi_end() — W3
оставался замаплен на видеобанк после каждого bar() со сплошной
заливкой.

_bgi_read дублировал getpixel (та же композиция begin+read_raw+end);
единственный потребитель floodfill переведён на getpixel, модуль
удалён.  getpixel.c: убраны мёртвые статики _gp_* (остались от
до-register версии).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:35:32 +03:00
snark13 5f0c46f0ea docs: идея span-примитивов для узких прямоугольников (TODO + fill-budget)
При узкой стороне <= 8 линий chunked-rectfill проигрывает циклу
_bgi_hspan_raw/_bgi_vspan_raw (подготовка ~340Т впустую, break-even
n~9): либо fast-path в диспетчере, либо рецепт для пользователя —
решать по профилю.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:02:16 +03:00
snark13 580837d2ee docs: бюджет тактов/размеров chunked-заливок + идеи дальнейших упрощений
docs/accel-fill-budget.md: полный разбор v3 (подготовка ~340Т на
прямоугольник, 46Т на чанк 16 линий, 53Т/51Т на линию, ~107 байт/leaf)
против per-line di/ei вариантов (djnz 72Т/линию ~63 байта; 16-бит IX
206Т/линию); break-even n≈9 линий, тотализатор, решение остаться на v3.
Краткие выжимки — в шапки _gfx_rect*fill256.

Идеи на будущее (там же): ограничить контракт стороной <=256 (широкие
прямоугольники пользователь выводит в два приёма); реализовать оба
варианта (поблочный/построчный) с переключением опцией сборки в духе
GFX_NOCHECK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:58:17 +03:00
snark13 2c414da465 libbgi: chunked-заливки на мульти-триггере акселератора + сплит rectfill
Семантика FSM акселератора вскрыта по драйверу MAME (sprinter.cpp) и
подтверждена экспериментами в MAME (tests/accfill): армирование живёт до
следующего accel-опкода (мульти-триггер работает); под армированием
триггерит ЛЮБОЙ non-M1 доступ к памяти (fetch операнда djnz/out!);
вертикальный Fill двигает Port_Y и не возвращает; размер блока переживает
LD B,B.  Детали: memory/accel_multitrigger_fill.

- _bgi_clear_raw: 20 DI-скобок по 16 колонок вместо полного брекета на
  каждую из 320 колонок; армирование размера один раз на скобку.
- _gfx_rectfill256 разделён: диспетчер (проверка ориентации ~160-245Т,
  break-even |w-h| >= ~4) + leaf'ы _gfx_recthfill256/_gfx_rectvfill256
  для прямого вызова, когда форма известна заранее.
- Leaf'ы: чанки <=16 линий одной скобкой (~54-56Т/линию против ~250Т у
  per-line варианта); счётчик чанков precompute'ится в байт-регистр
  (dec e/jr nz ~46Т/чанк вместо 16-бит арифметики в IX-слотах
  ~173Т/чанк); хвост — отдельная скобка со своим армированием (CBL-ISR
  в окне EI армирует акселератор своим размером — не выносить).
- getpixel/putpixel/_bgi_read: уборка мёртвого закомментированного кода.
- tests/accfill: регресс chunked-заливок (B0 clear, B1 vert 1+3 чанка,
  B2 horz чанк+хвост, полноширинный bar 320x8 — путь w>=256).

Прежние реализации сохранены под #if 0 для отката/сравнения.
Проверено в MAME (все PASS); семантика эмуляции — перепроверить на
реальном Sprinter.  size-baseline: accfill добавлен, gfx_demo +54 Б
(обвязка диспетчера), остальные без роста.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:41:49 +03:00
snark13 e553e5e6c9 docs: обновить size_baseline после bgi256 register-ABI рефактора
Эталон отставал от коммита 0296079.  Дельты объяснены: bgitest/bgi_img
уменьшились (убран скретч _gfx_acc256), gfx_demo вырос (демо rectfill в
исходнике), gfx_dbuf вырос (vsync-wait тянет CBL через общий порт
0x004E — см. _cbl_port_ref/unref).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:17:04 +03:00
snark13 f0f0ab9276 docs: TODO — пакетное чтение/запись массива байт через акселератор
Задел для будущих leaf'ов, читающих/пишущих строку или столбец пикселей
одним burst'ом (как _bgi_hspan_raw/_bgi_vspan_raw), чтобы ускорить блит
getimage/putimage (сейчас per-pixel _bgi_read_raw повторяет Port_Y +
addr + bounds-check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:17:04 +03:00
snark13 9b1ce71130 libbgi: упростить safe-контроль span'ов + отсекать len==0
_bgi_vspan_raw: safe-версия (без GFX_NOCHECK) больше НЕ клиппит диапазон
y+len и не обрабатывает y<0 — проверяется только валидность x/y (как в
_bgi_hspan_raw).  Сознательный компромисс: safe ловит грубый выход за
экран по координате, но не частичный отрезок; y+len<=256 — обязанность
вызывающего.

Оба span'а: в safe добавлена проверка len==0 → return (иначе B=0 по
конвенции акселератора рисует «256»).  В fast (GFX_NOCHECK) проверка
вырезается — поведение прежнее (256 точек), задокументировано в шапках.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:16:53 +03:00
snark13 029607971f libbgi: bgi256 fill-примитивы на register-ABI + typedef color_t
- удалён глобальный скретч акселератора (_gfx_acc256.c); fill-сегменты
  (_gfx_hfill256/_gfx_vfill256) принимают аргументы в регистрах HL/C/B/E,
  вызываются только из asm raw-примитивов
- новый _gfx_rectfill256: заливка прямоугольника через Horizontal_Size
- raw-примитивы (plot/read/hspan/vspan/clear) переписаны под новый ABI
- graphics.h: typedef color_t (uint8_t) для всех public color-функций
- sprinter-cc: флаг --safe (линковка *_safe.lib при наличии)
- CLAUDE.md/mame_interactive: авто-прогон тестов в MAME
- gfx_demo: демонстрация rectfill

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:32:21 +03:00
snark13 52636a5d6e libbgi: выделить графику BGI в отдельную библиотеку + тест спрайтов
Графика вынесена из libc/ в новую библиотеку libbgi/:
  - common/  — mode-agnostic математика и состояние (один исходник,
    .rel попадает в оба driver-архива);
  - bgi256/ + bgi16/ — mode-specific leaf'ы (raw-плот/чтение/спаны);
  - include/ — graphics.h + gfx.h; _bgi.h — внутренний заголовок.
Собираются lib/bgi256.lib (и bgi16.lib в Фазе 2); выбор режима
линковкой через sprinter-cc --gfx 256|16.  libc/ теперь без графики.

tests/bgi_img — тест спрайтов getimage/putimage/imagesize (5 операций
COPY/XOR/OR/AND/NOT + XOR-round-trip + self-check imagesize).
Проверен автотестом в MAME.

Примечание: make size-check пока красный (gfx_dbuf/gfx_demo выросли
после реорга) — закрыть по завершении миграции libbgi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:18:33 +03:00
snark13 3bf50f7ff7 docs: единый справочник по автотестам в MAME; убрать метод AUTORUN.BAT
- docs/mame-autotest.md — исчерпывающий документ: запуск, ввод команд,
  скриншоты, завершение сессий, анализ, раскладка клавиатуры, все квирки.
  Одного этого документа достаточно, чтобы работать с MAME в режиме
  автотестирования.
- mame_interactive.py теперь единственный инструмент: авто-запускает exe
  вводом пути (a:\<exe>+Enter), --step опционален (доп. ввод в программу),
  умные дефолты снимков/таймаута.
- удалён mame_auto_test.py (старый метод через AUTORUN.BAT chainload) и
  все упоминания AUTORUN.BAT в доках; интерактивный ввод его заменил.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:41:45 +03:00
snark13 657c6d2955 libc: BGI Фаза 2d-3 — settextstyle (масштаб и направление текста)
settextstyle/gettextsettings/textwidth/textheight: DEFAULT_FONT 8×8,
целочисленный масштаб 1..10, HORIZ/VERT (поворот 90° CCW), прозрачный
фон. Свой scaled-рендер (_bgi_draw_scaled) читает глиф через leaf
_bgi_font_rows (interleaved системный шрифт) и рисует блоки size×size
raw в одной W3-скобке. outtext/outtextxy переведены на него. Проверено
в MAME (tests/bgitest): размеры 1..4 + вертикальный текст.

Доки обновлены (Ф2d-1/2/3 готовы; осталось viewport/клиппинг).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:18:48 +03:00
snark13 b26560c409 libc: BGI Фаза 2d-2 — setlinestyle (стили и толщина линий)
setlinestyle/getlinesettings: SOLID/DOTTED/CENTER/DASHED/USERBIT +
NORM/THICK. _bgi_styled_line — Брезенхэм с 16-битной маской (пропуск
пикселя по биту) и дублированием ±1 перпендикулярно оси для THICK;
SOLID+NORM идёт быстрым путём (accel _bgi_lineseg). line/lineto/linerel/
rectangle/drawpoly переведены на него. Проверено в MAME (tests/bgitest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:14:03 +03:00
snark13 25cb7ba554 libc: BGI Фаза 2d-1 — getimage/putimage/imagesize (спрайты)
Растровые образы: imagesize (4 байта заголовка w,h + w*h пикселей),
getimage (захват прямоугольника), putimage с COPY/XOR/OR/AND/NOT_PUT.
Блит идёт raw в одной W3-скобке — добавлен _gfx_getpixel256_raw в gfx +
leaf _bgi_read_raw в drv256. Проверено в MAME (tests/bgitest): захват
спрайта, 3 COPY-копии, XOR/OR/COPY поверх фона.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:09:51 +03:00
snark13 dc37a14010 libc: BGI Фаза 2c — floodfill/pieslice/sector
floodfill — скан-строчная заливка области до границы (self-bracket
чтение: корректно, но медленно; raw-оптимизация в TODO).
pieslice/sector — залитые сектора круга/эллипса: границу (центр→дуга→
центр) прогоняем через _bgi_poly_edge и заливаем min/max по строкам,
как fillpoly (для >180° возможен перелив — упрощение). Проверено в
MAME (tests/bgitest): floodfill круга, круговая диаграмма, штрих-сектор.

Доки/справочник/память обновлены (Ф2a-c готовы; Ф2d = images/viewport/
text-style/line-style — осталось).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:04:04 +03:00
snark13 ecb419efda libc: BGI Фаза 2b — заливки (setfillstyle/bar/bar3d/fillpoly/fillellipse)
setfillstyle/getfillsettings + 10 стандартных 8×8 паттернов Borland.
bar теперь честно учитывает стиль заливки; bar3d (3D-брусок), fillpoly
(scanline min/max по строкам через брезенхэмовский проход рёбер),
fillellipse (полуширина строки через целочисленный _bgi_isqrt — без
32-бит). Общий _bgi_fill_span (SOLID/EMPTY/паттерн) с клипом, поверх
raw-hline в одной W3-скобке. Проверено в MAME (tests/bgitest): solid/
hatch бары, bar3d со slash, синий fillellipse, xhatch-треугольник.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:58:06 +03:00
snark13 5a48f7fafb libc: BGI Фаза 2a — arc/ellipse/drawpoly
Дуги/эллипсы через целочисленную тригонометрию Q7 (_bgi_trig.c, ×128) +
общий рисователь полилинией (_bgi_arc_draw.c). arc(x,y,st,end,r),
ellipse(x,y,st,end,xr,yr), drawpoly(n,pts). Проверено в MAME (tests/
bgitest): окружность/эллипс/дуга/полигон рисуются корректно.

ВАЖНО: тригонометрию считаем в int (Q7), НЕ через (long)…>>8 — первая
версия на 32-бит арифметике рисовала эллипс прямоугольником (SDCC/z80
криво собирает 32-бит; см. memory/avoid_32bit_arith_z80). Q7 даёт
радиус×значение ≤ 255×128 < 32767 — всё влезает в 16 бит.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:50:00 +03:00
snark13 e761d21505 libc: graphics.h — Turbo-C BGI-слой, Фаза 1 (режим 256)
Функционально-совместимый с Turbo-C <graphics.h> поверх gfx.h.
Архитектура: mode-agnostic математика (libc/bgi/*.c → sprinter.lib) +
driver-leaf'ы per-режим (libc/bgi/drv256/*.c → sprinter_gfx256.lib).
Режим выбирается линковкой: sprinter-cc --gfx 256 (16 — позже, тем же
leaf-split'ом). Один код работает в любом режиме без правок.

API: initgraph/closegraph/graphresult/cleardevice, set/get color+bkcolor,
getmaxx/y/color, put/getpixel, moveto/moverel/getx/gety, line/lineto/
linerel, rectangle, bar, circle, outtext/outtextxy. initgraph грузит
EGA-палитру 0..15. Пакетные примитивы (circle) — одна W3-скобка на
примитив (иначе на порядок медленнее). Проверено в MAME (tests/bgitest).

Попутно: gfx_getpixel256 в libc/gfx. size-check без регресса, baseline
обновлён. Детали: memory/bgi_two_lib_design, docs/TODO.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:40:49 +03:00
snark13 72466f7cad toolchain: скриптовый интерактивный ввод в DSS через MAME
mame_interactive.py печатает произвольный текст в командную строку DSS,
дёргая поля AT/PS-2-клавиатуры :kbd:ms_naturl через Lua set_value
(at_keyboard сам генерит scancode'ы → SIO Z84C015 → DSS). Раньше
инъекция шла в ZX-матрицу :IO_LINE*, которую DSS не читает — отсюда
«нет эффекта». Полная раскладка char→(port,mask,shift) с авто-Shift.

Квирки: attotime.seconds целое (субсекунды через attoseconds/1e18),
клавишу держать коротко (~0.06с, иначе автоповтор), дискета без
AUTORUN.BAT → приглашение C:\>. Проверено end-to-end: dir<Enter> и
запуск теста набором a:\rt_test.exe<Enter> (Shift для ':' и '\').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 12:18:37 +03:00
snark13 d652f89240 toolchain: автотест .exe в MAME без участия человека
AUTORUN.BAT chainload из system.bat (правится пользователем один раз)
+ mame_auto_test.py: кладёт .exe и сгенерированный AUTORUN.BAT на
дискету, гоняет MAME с Lua-таймингом (register_periodic +
manager.machine.time) для скриншотов и выхода по таймауту.

Natural keyboard (Lua natkeyboard:post/post_coded, -autoboot_command)
и прямая инъекция через ioport.fields[...]:set_value() не работают на
этом драйвере — перепробовано разными способами; AUTORUN.BAT chainload
оказался единственным надёжным путём запуска без участия человека.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 11:56:54 +03:00
snark13 46bd9ad0f1 gfx/time: vsync через polling бита кадра, sleep/delayms без halt-подсчёта
gfx_wait_vsync() и sleep() раньше предполагали, что КАЖДОЕ прерывание
на векторе 0xFF — кадровый тик; с CBL/клавиатурой на том же векторе
это уже не так.

gfx_wait_vsync(): вместо halt — polling бита 5 порта 0xFE (реальная
позиция луча, см. MAME kbd_fe_r), доступного пока включён CBL bit7
порта 0x004E. Разделяемое владение портом с CBL через
_cbl_port_ref/unref (тот же ref-counting паттерн, что у IM2-таблицы) —
cbl_close() возвращает "немой" режим вместо полного выключения, если
gfx ещё держит ссылку. Fallback на halt при таймауте.

sleep()/delayms(): калиброванный busy-wait по духу delayms.asm вместо
подсчёта halt-пробуждений. Калибровка одна на кадр (не на секунду —
не переполняет uint16_t и не требует умножения/32-бит арифметики),
общий движок libc/time/_sleep_calib.c для обеих функций. Fallback на
старое поведение при EBUSY (фрейм-хук занят другим irq_install()).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 10:47:57 +03:00
snark13 5086c47f0f libc: IM2 Phase 2b — звук CBL/COVOX через callback fill(), без кольца libc
CBL уже имеет аппаратный буфер 256 Б (2×128, двойная буферизация на
стороне железа) — держать поверх него ещё одно кольцо в libc было бы
лишней копией. cbl_open(freq, fmt, pump_mode, underrun_mode, fill)
регистрирует callback, вызываемый из ISR за очередным блоком; он сам
пропихивает данные приложения (откуда угодно) через cbl_push_otir()/
cbl_push_accel() — без промежуточного буфера.

- два насоса: OTIR (порт 0x4F) и ACCEL (акселератор, спец-страница
  EMM 0xFD@0xC000); OTIR+16-бит запрещён (EINVAL) — по исходнику MAME
  порт данных физически не может собрать 16-бит сэмпл из пары байт;
- форматы CBL_FMT_MONO8/16/STEREO8/16, частоты 7.8..109к;
- CBL_UNDERRUN_APP (по умолчанию, недолив не наша забота) /
  CBL_UNDERRUN_SILENCE (буфер тишины malloc'ится только в этом режиме);
- tests/cbltest: матрица 64 комбинации (2 насоса × 8 форматов × 4
  частоты); tests/cblwav: banked-стрим речи с дискеты (физстраницы
  кэшированы заранее — mem_get_page нельзя звать из fill()/ISR);
  tests/cblstream: единственный случай с собственным кольцом уровня
  приложения (диск нельзя читать из fill()).

Verified в MAME 2026-07-07 — все три теста работают.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:54 +03:00
snark13 8a952b99eb irq: Phase 2a — CTC-таймер на отдельном векторе 0x06 (Z84C015)
- irq_ctc_install(handler, div2, div3) / irq_ctc_remove: канал 2 CTC
  делит видеотакт 875 кГц (1 тик = 1 знакоместо), канал 3 считает от
  него и прерывает; f = 875000/(div2*div3), div 0 = 256. Пресет
  IRQ_CTC_VSYNC_DIV2/3 = 112*160 — точное начало кадра ~48.8 Гц БЕЗ
  примеси клавиатуры (вектор 0x06 отделён от общего 0xFF)
- CTC-трамплин: полный сейв -> handler -> EI/RETI; RETI обязателен
  (daisy chain Z84C015 снимает IUS только по опкоду RETI); к DSS не
  чейнится — личное прерывание
- общая IM2-таблица под счётчиком ссылок (_irq_table.c): кадровый и
  CTC-хендлеры независимы, последний unref возвращает I/IM 1;
  atexit-уборка глушит CTC обязательно (иначе кГц-прерывания душат
  шелл после выхода)
- порты/слова по docs/samples: CH0=0x10/CH2=0x12/CH3=0x13,
  0x57/0xD7/вектор в CH0, стоп 0x03
- irqtest: CTC-vsync параллельно с кадровым + произвольная частота;
  MAME: frame 48 Гц, ctc(vsync) 49 Гц (parallel frame жив),
  ctc(50x50) 350 Гц точно по формуле, remove/выход чистые

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:15:04 +03:00
snark13 5184415fc4 irq: фикс Phase 1 после отладки — DSS работает в IM 1, чейн всегда на 0x0038
Первая версия висла на первом прерывании. Две причины (verified по
docs/samples/sprinterIntLib.asm, SIO_CTC_KEY.asm и исходникам MAME):

- DSS работает в IM 1 (обработчик 0x0038); I=0x3F — наследие Spectrum
  ROM, НЕ таблица: чтение [I<<8|0xFF] давало мусор (0x00BF) и прыжок в
  никуда. Чейн из трамплина теперь ВСЕГДА jp 0x0038 (interrupted-PC на
  стеке = имитация RST 38); irq_remove безусловно восстанавливает IM 1
- CBL-фильтр по биту 7 порта 0xFE убран: при выключенном CBL бит
  подтянут к 1 (MAME kbd_fe_r: data |= 0xE0) — каждый кадр ложно
  уходил в чейн, user-handler не вызывался бы. Вернуть в Phase 2
  вместе с поддержкой CBL

Попутно подтверждено: порт 0x19 = SIO-A RR0 (Z84C015), бит 0 = Rx
Available; вектора встроенной периферии SIO 0x10..0x1E / CTC 0x06
(заливка 257×H ловит любые); внешний вектор 0xFF.

irqtest: диагностика I до установки + фаза без ESTEX; прогон в MAME:
~49 Гц, клавиатура жива, remove останавливает тики, чистый выход.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:05:28 +03:00
snark13 a6fe50e247 libc: IM2 user-ISR, Phase 1 — libc/irq (irq_install/irq_remove) + irqtest
Проще исходного плана (docs/im2_isr_design.md обновлён): отдельный
--memory im2 не понадобился.

- <irq.h>: irq_install(handler) — вызов ~50 Гц только на КАДРОВЫХ
  прерываниях (клавиатура bit0:0x19 и CBL bit7:0xFE отфильтровываются);
  штатный обработчик DSS чейнится ВСЕГДА (SMC-jp, адрес из старой
  IM2-таблицы по регистру I) — клавиатура/SYSTIME/мышь живут
- вектор-таблица: 513 Б BSS + runtime-выравнивание; Sprinter шлёт
  только вектор 0xFF, поэтому jp-заглушка лежит внутри самой таблицы
  по смещению H — без linker-областей и правок crt0
- трамплин: полный сейв обоих наборов+IX/IY вокруг user-handler'а
  (ex af,af' как .db 0x08 — апостроф ломает препроцессор SDCC)
- tiny/big: работает (код в W2); small/huge: EINVAL по проверке
  адресов; irq_remove идемпотентен и висит на atexit (выход без
  снятия = I в памяти умершего процесса = крах шелла); old_I==0 → IM1
- tests/irqtest: тики за 3 с против time() (~50 Гц), живая клавиатура
  под handler'ом, остановка после remove, чистый выход
- docs: im2_isr_design (статус+дельты), libc-reference (<irq.h>), TODO

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:21:16 +03:00
snark13 110f69fb2e docs: П6 (MAME-смоук) закрыт — все тесты зелёные
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:11:28 +03:00
641 changed files with 34410 additions and 1586 deletions
+13
View File
@@ -0,0 +1,13 @@
CompileFlags:
Add:
- -Ilibbgi/include
- -Ilibc/include
- -I.
- --target=z80-unknown-unknown
- -mz80
- -std=c99
- -D__SDCC # если нужно гасить SDCC-специфику через #ifdef
Remove:
- --target=z80-unknown-unknown
Diagnostics:
UnusedIncludes: None # опционально, чтобы не ругался на "лишние" инклюды под другой таргет
+26 -2
View File
@@ -3,7 +3,7 @@
# ===========================================================================
# `build/` directories anywhere in the tree
# (top-level build/, lib/build/, toolchain/*/build/, ...)
# (top-level build/, libc/build/, libbgi/build/, toolchain/*/build/, ...)
build/
# sprinter-cc per-example intermediate directory
@@ -40,7 +40,31 @@ tests/*/*.cdb
tests/*/*.mem
tests/*/*.rst
# libc archive (built from libc/, see lib/Makefile)
# applications/<app>/<prog>/ — реальные приложения (та же схема, что
# examples/ и tests/, но на уровень глубже: applications/PoP/roomtest/...)
applications/*/*/*.exe
applications/*/*/*.asm
applications/*/*/*.lst
applications/*/*/*.lk
applications/*/*/*.ihx
applications/*/*/*.noi
applications/*/*/*.sym
applications/*/*/*.map
applications/*/*/*.rel
applications/*/*/*.cdb
applications/*/*/*.mem
applications/*/*/*.rst
# PoP-порт: внешние референс-репозитории (собственные git-клоны — НЕ
# часть этого репозитория) + оригинальные game-данные (копирайт, только
# для реверса форматов на этой машине).
applications/PoP/SDLPoP/
applications/PoP/mininim/
applications/PoP/PR/
applications/PoP/Prince-of-Persia-Apple-II/
applications/PoP/MSDOS/
# libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
lib/*.lib
# Host-built mkexe binary + test outputs (input fixtures *.bin/*.ihx kept)
+26 -11
View File
@@ -6,20 +6,34 @@ Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0
## Сборка и проверка
```
make # tools + lib + все тесты (43) + examples
make -C lib # только libc → lib/sprinter.lib
make # tools + lib + libbgi + все тесты (45) + examples
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
make size-baseline # принять текущие размеры эталоном
```
Одиночный тест: `cd tests/<имя> && make run` (пакует ТОЛЬКО этот exe
+ EXTRA_DATA на дискету и запускает MAME). Тесты в MAME гоняет
пользователь — готовь дискету и проси прогнать.
Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK`
параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
После правок libc: пересборка от чистого листа (`make -C lib clean`)
не обязательна — stale .rel чистятся автоматически; `make size-check`
обязателен (рост _CODE без причины — регрессия).
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
дискету и запускает MAME автоматически через `toolchain/mame_interactive.py`,
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
шагов ввода) — прямой вызов:
`python3 toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (читаются инструментом Read).
Подробности: `docs/mame-autotest.md`.
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
автоматически; `make size-check` обязателен (рост _CODE без причины —
регрессия).
## Правила libc
@@ -29,7 +43,7 @@ make size-baseline # принять текущие размеры эталон
(`_`-префикс); общие статики — в отдельные data-модули
(`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) —
рядом с исходниками, НЕ в libc/include.
- Имя файла = имя функции. lib/Makefile собирает wildcard'ом —
- Имя файла = имя функции. libc/Makefile собирает wildcard'ом —
ничего регистрировать не надо.
- Комментарии — на русском; шапка модуля объясняет что/зачем + ABI.
- File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет
@@ -54,12 +68,13 @@ make size-baseline # принять текущие размеры эталон
лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard);
ENV $46: A=0 = NOT FOUND.
- Перед обвинением компилятора/железа — подтвердить артефактом
(сгенерированный .asm в lib/build/, дамп, репро) — см.
(сгенерированный .asm в libc/build/ или libbgi/build/, дамп, репро) — см.
memory/defer_unexplained_quirks.
## Структура
- `libc/<area>/*.c` — модули libc; `libc/include/` — публичные заголовки
- `libc/<area>/*.c` — модули libc (ядро, БЕЗ графики); `libc/include/` — публичные заголовки libc
- `libbgi/` — графика BGI (отдельная библиотека): `common/` — mode-agnostic (один исходник, .rel в обеих driver-библиотеках), `bgi256/` + `bgi16/` — mode-specific leaf'ы (реальные реализации, без обёрток); `include/` — graphics.h + gfx.h; `_bgi.h` — внутренний заголовок. Собирает `lib/bgi256.lib``bgi16.lib` в Фазе 2). Выбор режима линковкой: `--gfx 256` / `--gfx 16`.
- `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build)
- `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик
- `tests/` — по одному API/фиче; `examples/` — реальные приложения
+10 -6
View File
@@ -2,7 +2,7 @@
#
# make build host tools, libc archive, all tests, all apps
# make tools build only host tools (mkexe)
# make lib build lib/sprinter.lib (libc archive used by sprinter-cc)
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
# make tests build all libc feature tests under tests/
# make examples build all real applications under examples/
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
@@ -16,10 +16,11 @@
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
malloc mem_test argv errno rt_test openenv ls conio conio2 \
attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
filetest fdmax fbench solidt dec_test gets stest2 winrest \
filetest fdmax fbench solidt irqtest cbltest cblwav cblstream dec_test gets stest2 winrest \
bios_text text_palette \
gfx_demo gfx_d16 gfx_text gfx_mous gfx_dbuf
gfx_demo gfx_dbuf bgitest bgi_img accfill
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
# Larger end-user applications under examples/.
APPS := mdview mdview2
@@ -34,6 +35,7 @@ ALL_EXES := $(TEST_EXES) $(APP_EXES)
DATA_FILES := \
tests/cat/test.txt \
tests/seek/big.txt \
tests/cblwav/speech.pcm \
examples/mdview/SAMPLE.MD
.PHONY: all tools lib tests examples check clean sdcc floppy \
@@ -45,7 +47,8 @@ tools:
$(MAKE) -C toolchain/mkexe
lib:
$(MAKE) -C lib
$(MAKE) -C libc
$(MAKE) -C libbgi
check: tools
$(MAKE) -C toolchain/mkexe check
@@ -80,7 +83,8 @@ size-baseline:
clean:
$(MAKE) -C toolchain/mkexe clean
$(MAKE) -C lib clean
$(MAKE) -C libc clean
$(MAKE) -C libbgi clean
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
@for a in $(APPS); do $(MAKE) -C examples/$$a clean; done
+19 -2
View File
@@ -36,7 +36,9 @@ LIB := $(PROJ_ROOT)/lib/sprinter.lib
MAME_DIR := $(PROJ_ROOT)/mame/v306
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
MAKE_DISK := $(MAME_DIR)/make_disk.py
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
RUN_MAME := $(MAME_DIR)/run_mame.sh
# Optional knobs — see top of file.
@@ -62,8 +64,13 @@ $(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
$(MKEXE):
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
# $(LIB) = lib/sprinter.lib (libc). Графика (bgi256.lib) собирает
# libbgi/Makefile; гоним и его — иначе standalone `make` в тесте с
# --gfx 256 не найдёт bgi256.lib при линковке. Оба инкрементальные,
# на повторном запуске ничего не пересобирают.
$(LIB):
$(MAKE) -C $(PROJ_ROOT)/lib
$(MAKE) -C $(PROJ_ROOT)/libc
$(MAKE) -C $(PROJ_ROOT)/libbgi
clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe
@@ -80,4 +87,14 @@ floppy: $(EXAMPLE).exe
run: floppy
cd $(MAME_DIR) && ./run_mame.sh
.PHONY: all clean floppy run
# `make hdd` packs this program (+ optional EXTRA_DATA files) into the MAME
# HDD image mounted as disk D: (-hard2 test_hdd.chd). Гораздо быстрее FDD —
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
hdd: $(EXAMPLE).exe
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
@echo
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
.PHONY: all clean floppy run hdd
+84
View File
@@ -0,0 +1,84 @@
# Prince of Persia → ZX Sprinter — правила подпроекта
Порт Prince of Persia (DOS/Apple II) на Sprinter Sp2000 поверх нашего
sprinter-cc / libc / libbgi. Действуют правила корневого
`CLAUDE.md` (сборка, libc, ABI, MAME-автотест); ниже — только специфика PoP.
Общение и комментарии — на русском.
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
**`SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ авторитетный
источник того, как оригинальный движок это делает.** Правило без исключений:
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
константы) — СНАЧАЛА найди и прочитай соответствующий код в `SDLPoP/src/`,
и портируй по нему. Не пиши по памяти, не выводи логику «из общих
соображений», не угадывай значения — это источник багов, которые потом
ловятся в MAME часами.
2. **По любому вопросу «как в оригинале должно быть»** (что окклюдит что,
в каком порядке слои, когда меняется тайл, какая скорость/задержка,
что делает такой-то кадр анимации) — ответ ищи в `SDLPoP/src/`, а не
строй гипотезу. Если в SDLPoP не нашёл — это повод копать дальше в
исходнике, а не додумывать.
3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока
не доказано обратное (наша платформа/ABI требует отличия — тогда явно
зафиксировать почему в комментарии).
Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/
seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/
слои/окклюзия, `seg009` чтение ресурсов. Слои окклюзии у нас = слои SDLPoP.
См. memory `pop_check_sdlpop_first`.
Вторичные референсы (когда в SDLPoP непонятно/нужен другой ракурс):
- `Prince-of-Persia-Apple-II/` — оригинальный 6502-исходник 1989 (Мехнер).
- `PR/` (github.com/NagyD/PR, GPLv2) — Princed Resources.
- `mininim/` — независимая реализация.
Все эти папки — **справочник логики/структур/констант и источник ассетов**,
но НЕ код для копирования (лицензии несовместимы, наш ABI другой): читаем
и переписываем под наш движок, а не вставляем куски.
## Ассеты
Готовые распакованные VGA-256 ассеты (то, что нужно под 320×256×256) —
`SDLPoP/data/` (`res<id>.png`/`.pal`/`.bin`). Брать оттуда, а НЕ писать свой
декодер DOS `.DAT`. Локальные `.DAT` — в `MSDOS/`.
**Каноническая спецификация форматов `.DAT` — `docs/POP-DAT-FormatSpecifications.pdf`**
(грепаемая копия — `docs/POP-DAT-FormatSpecifications.txt`): первоисточник
Princed для DAT v1.0 (контейнер/индекс/чек-сумма, кодеки RLE/LZG, палитры,
формат уровней, звук), на нём построены и SDLPoP, и Princed Resources. Наши
разборы (`docs/MSDOS_RESOURCE_FORMAT.md` / `docs/APPLEII_RESOURCE_FORMAT.md` /
`docs/README.md`) — практические заметки/сверки; при расхождении источник
истины — спецификация. Формат уровня почти идентичен в Apple II и DOS.
Упаковка ассетов под Sprinter (атласы `.atl`, палитра) — python-скрипты в
`toolchain/` (`render_room.py`, `pop_pack_bg.py`, `pop_pack_kid.py`,
`pop_extract_kid_data.py`). Их дёргают Makefile'ы тестов.
## Структура папки
- `docs/` — планы и форматы: `PORT_PLAN.md` (общий план фаз),
`KID_PLAN.md`, `double_buffer_plan.md`, `loose_floors_plan.md`, форматы
ресурсов. Начинать чтение отсюда.
- `roomtest/`**активная разработка**: комната 1 + Kid (анимация, ввод,
коллизия, падение, зацеп, fore-окклюзия, loose-полы). Свой `CLAUDE.md`.
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
- `SDLPoP/`, `PR/`, `Prince-of-Persia-Apple-II/`, `mininim/`, `MSDOS/`
референсы/оригинальные данные (см. выше).
## Ключевые архитектурные решения (memory/)
- `pop_port_project` — общий статус порта.
- `pop_banking_architecture` — будущее: big+BANK_W1, графику нельзя в W3,
один файл = один банк = прямые вызовы, main резидентен.
- `pop_background_strategy` — фон = композиция тайлов в рантайме (вариант 3).
- `pop_kid_plan` / `pop_hang_state` / `pop_fore_layer` /
`pop_fall_debug_baseline` — этапы Kid.
- `kbd_raw_fifo_drain` — held-state клавиатуры (единственный принципиальный
пробел движка, закрыт `<kbd_raw.h>`): вычерпывать FIFO SIO циклом.
- `png_strip_padding_tradeoff`, `pop_tile_atlas_palette_merge` — квирки
упаковки ассетов.
+35
View File
@@ -0,0 +1,35 @@
# Prince of Persia на ZX Sprinter
Порт Prince of Persia на компьютер Sprinter Sp2000 поверх нашего
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
## Что где
| Папка | Назначение |
|-------|-----------|
| `roomtest/` | **Активная разработка.** Комната 1 уровня 1 живой композицией тайлов + Kid: анимация (seqtbl), управление с клавиатуры, коллизия, падение, зацеп/подтягивание, fore-окклюзия, проваливающиеся полы. Свой README/CLAUDE. |
| `docs/` | Планы (`PORT_PLAN`, `KID_PLAN`, `double_buffer_plan`, `loose_floors_plan`) и разбор форматов ресурсов Apple II / DOS. |
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
| `SDLPoP/`, `PR/`, `mininim/` | Референсные реализации движка (GPL) — читаем логику/константы, НЕ копируем код. `SDLPoP/data/` — источник распакованных VGA-ассетов. |
| `Prince-of-Persia-Apple-II/` | Оригинальный 6502-исходник 1989 г. |
| `MSDOS/` | Локальные `.DAT`-ресурсы DOS-версии. |
## Референсы = только справочник
`SDLPoP/`, `PR/`, `mininim/`, `Prince-of-Persia-Apple-II/` используются как
справочник структур/логики и как источник готовых ассетов — их код НЕ
копируется в наш порт (лицензии несовместимы, ABI другой). Любая механика
сверяется с `SDLPoP/src/` **до** реализации.
## Форматы ресурсов
Формат уровня почти идентичен в Apple II и DOS (2304 / 2305 байт,
`blueprnt`). Графика различается принципиально, но брать распакованные PNG
из `SDLPoP/data/` практичнее, чем декодировать сырой `.DAT`. Подробности —
[`docs/README.md`](docs/README.md).
+27
View File
@@ -0,0 +1,27 @@
# bgtest — мини-тест атласов статического фона PoP (Шаг 2, до порта room.c).
# Проверяет загрузку .atl + палитру + прямую адресацию + прозрачность.
#
# --memory huge (как poc): atlas_load/gfx_blit трогают W3; huge кладёт
# CODE в W1, DATA/BSS в W2 — без банков. --gfx 256 подлинкует bgi256.
#
# Данные фона генерит toolchain/pop_pack_bg.py в ../poc/res/bg/.
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := bgtest
MEMORY ?= huge
EXTRA_FLAGS ?= --gfx 256
BG_DIR := $(CURDIR)/../poc/res/bg
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \
$(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl $(BG_DIR)/pop_bg.pal
EXTRA_DATA := $(BG_DATA)
include $(PROJ_ROOT)/app.mk
# Ассеты фона: пересобрать пакером, если исходники поменялись.
$(BG_DATA): $(PROJ_ROOT)/applications/PoP/toolchain/pop_pack_bg.py \
$(PROJ_ROOT)/applications/PoP/toolchain/render_room.py
cd $(PROJ_ROOT)/applications/PoP/toolchain && python3 pop_pack_bg.py
$(EXAMPLE).exe: $(BG_DATA)
+112
View File
@@ -0,0 +1,112 @@
/*
* bgtest.c — мини-тест атласов статического фона PoP (Шаг 2 порта, ДО
* порта room.c). Проверяет на MAME: загрузку 7 .atl, палитру pop_bg.pal,
* ПРЯМУЮ адресацию (id -> страница/idx без remap-таблиц) и прозрачность
* (индекс 0xFF). Рисует сетку репрезентативных спрайтов на цветной
* заливке — прозрачные области должны показать фон.
*
* Раскладка из toolchain/pop_pack_bg.py (см. pop_bg_atlas.h):
* ENV фон id N -> env[N>>5], idx N&31 ; WALL/FORE idx = id.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
#include <stdio.h>
/* Имена файлов — плоская ФС диска (make_disk кладёт по basename). */
static const char *const ENV_ATL[5] = {
"pop_env0.atl", "pop_env1.atl", "pop_env2.atl",
"pop_env3.atl", "pop_env4.atl"
};
static atlas_t env[5];
static atlas_t wall_a;
static atlas_t fore_a;
/* Блит одного спрайта ленты idx атласа a в (x,y): страница атласа в W0,
* gfx_blit читает w/h из getimage-заголовка ленты (в W0). */
static void put(atlas_t *a, unsigned char idx, int x, int y)
{
const void *img = atlas_image(a, idx);
gfx_w0_map(a->page);
gfx_blit(x, y, img);
gfx_w0_unmap();
}
static void put_env(unsigned char id, int x, int y)
{
put(&env[id >> 5], (unsigned char)(id & 31), x, y);
}
int main(void)
{
int i;
/* Загрузка всех 7 атласов (env0..4 + wall + fore). */
for (i = 0; i < 5; i++) {
if (atlas_load(&env[i], ENV_ATL[i]) != 0) {
printf("atlas_load %s failed\n", ENV_ATL[i]);
return 1;
}
}
if (atlas_load(&wall_a, "pop_wall.atl") != 0) { puts("wall atl fail"); return 1; }
if (atlas_load(&fore_a, "pop_fore.atl") != 0) { puts("fore atl fail"); return 1; }
initgraph();
gfx_set_draw_page(0);
gfx_set_visible_page(0);
if (gfx_pal_fload(0, "pop_bg.pal") < 0)
gfx_pal_fload(0, "a:\\pop_bg.pal");
/* Свой яркий фон-индекс (вне 0x50..0x6F, занятых графикой) — чтобы
* прозрачные (0xFF) области спрайтов были ЯВНО видны. */
#define BG_IDX 0x20
gfx_pal_set(0, BG_IDX, 120, 0, 90); /* r,g,b — тёмно-пурпурный */
gfx_pal_sync();
setfillstyle(SOLID_FILL, BG_IDX);
bar(0, 0, 319, 255);
setcolor(WHITE);
outtextxy(2, 2, "PoP bg atlas test");
/* Прозрачный блит: банк не пишет 0xFF (фон проступает). */
gfx_set_bank(GFX_BANK_TRANSPARENT);
/* --- Стены (chtab_7): нижние грани 7/9/5/3, основные 8/10/6/4 --- */
put(&wall_a, 3, 4, 20); put(&wall_a, 5, 40, 20);
put(&wall_a, 7, 76, 20); put(&wall_a, 9, 112, 20);
put(&wall_a, 4, 4, 90); put(&wall_a, 6, 40, 90);
put(&wall_a, 8, 76, 90); put(&wall_a, 10, 112, 90);
/* декали-марки стен 14..17 */
put(&wall_a, 14, 150, 20); put(&wall_a, 15, 168, 20);
put(&wall_a, 16, 186, 20); put(&wall_a, 17, 204, 20);
/* --- Столб: база 92, боковая грань 93, фронт 95 (fore) --- */
put_env(92, 4, 160);
put_env(93, 40, 160);
put(&fore_a, 95, 76, 160);
/* --- Пол: база 41, правый треуг. 42, низ 43; силуэт 44/45 --- */
put_env(41, 150, 120);
put_env(42, 190, 120);
put_env(44, 230, 120);
put_env(45, 260, 120);
/* --- Решётка-окно 126, дебрис 97/98, слайсы ворот 52/53 --- */
put_env(126, 150, 160);
put_env(97, 190, 160);
put_env(52, 230, 160);
put_env(53, 250, 160);
gfx_set_bank(GFX_BANK_NORMAL);
/* Держим картинку на экране до нажатия (не полагаемся на блокирующий
* getch — в автотесте клавиш нет, крутимся до таймаута MAME). */
while (!kbhit())
gfx_wait_vsync();
closegraph();
for (i = 0; i < 5; i++) atlas_free(&env[i]);
atlas_free(&wall_a);
atlas_free(&fore_a);
return 0;
}
+6
View File
@@ -0,0 +1,6 @@
# coltest — прототип вертикального accel-copy (gfx_blit_cols) + флип.
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := coltest
MEMORY ?= huge
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+64
View File
@@ -0,0 +1,64 @@
/*
* coltest.c — прототип K2a: проверка вертикального accel-COPY
* (gfx_blit_cols) + прозрачности 0xFF + горизонтального флипа.
*
* Спрайт 16x24 column-major, АСИММЕТРИЧНЫЙ:
* левые 8 колонок: верх (row<12) = ПРОЗРАЧНО (0xFF), низ = БЕЛЫЙ (1)
* правые 8 колонок: КРАСНЫЙ (2)
* На синем фоне (3). Ожидаем на MAME:
* normal @ (50,100): слева бело-снизу+прозрачно-сверху, справа красный;
* flip @(120,100): ЗЕРКАЛО — слева красный, справа бело+прозрачно-сверху;
* в прозрачных местах виден СИНИЙ фон.
*/
#include <graphics.h>
#include <gfx.h>
#include <conio.h>
#define W 16
#define H 24
static unsigned char spr[4 + W * H];
int main(void)
{
int col, row;
spr[0] = W; spr[1] = 0;
spr[2] = H; spr[3] = 0;
for (col = 0; col < W; col++)
for (row = 0; row < H; row++) {
/* ФИНАЛ: АСИММЕТРИЧНЫЙ — лево верх прозрачно / низ белый,
* право красное. normal: лево бело+прозрач-верх, право красн;
* flip: ЗЕРКАЛО (лево красн, право бело+прозрач-верх). */
unsigned char v;
if (col < 8)
v = (row < 12) ? 0xFF : 1;
else
v = 2;
spr[4 + col * H + row] = v;
}
initgraph();
gfx_set_draw_page(0);
gfx_set_visible_page(0);
gfx_pal_set(0, 1, 255, 255, 255); /* белый */
gfx_pal_set(0, 2, 224, 32, 32); /* красный */
gfx_pal_set(0, 3, 32, 48, 200); /* синий фон */
gfx_pal_set(0, 255, 0, 224, 0); /* ДИАГ: 0xFF как ЗЕЛЁНЫЙ цвет (bank NORMAL) */
gfx_pal_sync();
setfillstyle(SOLID_FILL, 3);
bar(0, 0, 319, 255);
setcolor(1);
outtextxy(40, 80, "normal");
outtextxy(110, 80, "flip");
gfx_set_bank(GFX_BANK_TRANSPARENT); /* ДИАГ: 0x58 подавление 0xFF (с тенью) */
gfx_blit_cols(50, 100, spr, 0); /* обычный */
gfx_blit_cols(120, 100, spr, 1); /* зеркало */
gfx_set_bank(GFX_BANK_NORMAL);
while (!kbhit())
gfx_wait_vsync();
closegraph();
return 0;
}
@@ -0,0 +1,283 @@
# Формат ресурсов Prince of Persia (Apple II, оригинальные исходники 1989)
Источник — официально опубликованные Джорданом Мехнером исходники
(`Prince-of-Persia-Apple-II/`, 6502-ассемблер). В отличие от DOS-версии, здесь
формат восстановлен **напрямую по коду**, а не по догадкам о байтах —
уверенность высокая везде, где указана ссылка на конкретный файл/строки.
---
## 1. Формат уровня (`01 POP Source/Levels/LEVEL0`…`LEVEL14`, 2304 байта)
Файлы уровня — это побайтовый дамп структуры `blueprnt`, которая грузится по
фиксированному адресу `$b700` (`EQ.S:28`) и объявлена как `dum blueprnt` в
`EQ.S:258-266`. Никакого отдельного заголовка файла нет — это чистый образ
структуры в памяти:
| Поле | Размер | Смещение в файле | Описание |
|---|---|---|---|
| `BLUETYPE` | 720 Б | 0719 | 24 экрана × 30 тайлов: тип объекта/тайла |
| `BLUESPEC` | 720 Б | 7201439 | 24 экрана × 30 тайлов: доп. байт состояния объекта |
| `LINKLOC` | 256 Б | 14401695 | Таблица связей нажимных плит/дверей, часть 1 |
| `LINKMAP` | 256 Б | 16961951 | Таблица связей, часть 2 |
| `MAP` | 96 Б | 19522047 | 24 экрана × 4 байта: граф соседних экранов |
| `INFO` | 256 Б | 20482303 | Метаданные уровня: старт Кида, стражников и т.д. |
Сумма: 720+720+256+256+96+256 = **2304** — точно совпадает с размером файла,
что подтверждает: это чистый дамп структуры, без обёртки.
### 1.1 Сетка тайлов (`BLUETYPE` / `BLUESPEC`)
Каждый экран — ровно **30 тайлов** (10 столбцов × 3 ряда): подтверждено
таблицами `BlockTable`/`BlockEdge` (`TABLES.S:74-154`) и логикой перехода
между экранами в `CTRLSUBS.S:218-234` (при переходе через край экрана
`tempblockx` меняется на ±10, `tempblocky` — на ±3).
Функция `CALCBLUE` (`GRAFIX.S:1757-1784`) вычисляет для экрана 1–24:
`BlueType = blueprnt + (screen-1)*30`, `BlueSpec = BlueType + 24*30`,
используя таблицу `Mult30` (`TABLES.S:131-140`).
Байт `BLUETYPE` упакован битовыми полями (`EQ.S:484-486`):
```
бит 7-6: secmask (%11000000) — назначение не установлено по доступному коду
(возможно, служебное поле редактора)
бит 5: reqmask (%00100000) — флаг "необходимая опорная плитка"
(проверяется в BREAKLOOSE, MOVER.S:395-397)
бит 4-0: idmask (%00011111) — тип тайла/объекта, 0-29
```
Перечень 30 типов объектов (`MOVEDATA.S:8-37`):
```
0 space 8 pillarbottom 16 exit 24 window2
1 floor 9 pillartop 17 exit2 25 archbot
2 spikes 10 flask 18 slicer 26 archtop1
3 posts 11 loose 19 torch 27 archtop2
4 gate 12 panelwof 20 block 28 archtop3
5 dpressplate 13 mirror 21 bones 29 archtop4
6 pressplate 14 rubble 22 sword
7 panelwif 15 upressplate 23 window
```
Проверено вручную на дампе начала `LEVEL1` (`00 00 00 21 01 21 21 21 34 34
33 33 21 23 00 34 14 14 14 34 14 34 34 2e 23 0b 01 21 34`) — например,
`0x33 → id=0x13=19 (torch)`+reqmask, `0x34 → id=20 (block)`+reqmask —
декодирование по таблице сходится чисто.
`BLUESPEC` — доп. байт, чья семантика зависит от типа тайла (единой схемы
нет, разбирается объект-специфичным кодом):
- **gate** (дверь, `FRAMEADV.S:2222-2234`): на диске — маленький enum (1 =
начинает открытой сверху, 2 = снизу, …), который через `initsettings`
(`FRAMEADV.S:22-23`, диапазон `gminval=0`..`gmaxval=188`, из
`MOVEDATA.S:56-57`) при инициализации уровня превращается в живой счётчик
"высоты двери" 0–188.
- **loose** (шаткая плитка, `FRAMEADV.S:2224-2237`): при инициализации всегда
принудительно обнуляется, независимо от значения на диске.
- **flask** (зелье, `FRAMEADV.S:2226,2239-2246`): значение×32 выбирает
цвет/тип зелья.
- **spikes** (шипы, `MOVER.S:365-382`, константы `spikeExt=5, spikeRet=9` в
`MOVEDATA.S:45-46`): 0 = безопасно/убраны, 1–8 — кадр анимации
выдвижения/втягивания, `$FF` = навсегда заклинило (тело наколото).
- **pressplate/upressplate** (нажимные плиты, `MOVER.S:425-464`,
`FRAMEADV.S:2059-2098`): значение — это **индекс в цепочке связей**
`LINKLOC`/`LINKMAP` (см. ниже); младшие 5 бит `LINKMAP` по этому индексу
одновременно служат счётчиком таймера плиты (0–31), определяющим
состояние "поднято/опущено".
### 1.2 `LINKLOC` / `LINKMAP` — граф триггеров (нажимные плиты → двери и т.п.)
Два параллельных массива по 256 байт кодируют цепочки "нажатие плиты X →
сработать объект на экране S, блок B". Восстановлено из `MOVER.S:506-537`
(цикл `trigger`) и `MOVER.S:1549-1581` (`gettimer/chgtimer/getloc/
getlastflag/getscrn`):
```
LINKLOC[i]: бит 7 = флаг "последнее звено цепочки"
биты 6-5 = младшие 2 бита номера целевого экрана
биты 4-0 = номер целевого блока (0-29); $FF = "никуда не привязано"
LINKMAP[i]: биты 7-5 = старшие 3 бита номера целевого экрана
(вместе с LINKLOC биты 6-5 → полный номер экрана 0-31)
биты 4-0 = таймер обратного отсчёта плиты (0-31, значим только
по индексу самой плиты)
```
`BLUESPEC` плиты хранит индекс `i` её *первого* звена; `getlastflag` идёт
вперёд (`inc linkindex`), пока не встретит бит 7 в `LINKLOC`. Сверено на
`LEVEL1`: байт по смещению 1440 (`0x89 = 10001001` → флаг конца цепочки,
целевой блок 9) и параллельно байт по смещению 1696 (`0x60 = 01100000`
старшие биты номера экрана) — согласуется с этой раскладкой. Заполнены
реально используемые уровнем звенья, остальное — "мусорные" повторяющиеся
байты-заполнители.
### 1.3 `MAP` — граф соседних экранов
24 записи × 4 байта = 96 байт: для каждого экрана (1–24)
`MAP[(scrn-1)*4 + 0..3] = левый, правый, верхний, нижний соседние экраны`,
читается через `GETLEFT/GETRIGHT/GETUP/GETDOWN` (`CTRLSUBS.S:244-274`,
индексация `MAP-4..MAP-1,x` при `x = scrn*4`). Экран `0` зарезервирован как
"нет экрана" (проверка `beq ]rts` в этих же процедурах).
### 1.4 `INFO` — метаданные уровня (256 байт, база = смещение файла 2048)
Объявлено как `dum INFO` в `EQ.S:272-288`:
| Смещение (от начала INFO) | Поле | Размер |
|---|---|---|
| 0 | "число экранов + 1" (используется в `SETINITIALS`, `SUBS.S:1441-1445`) | 1 |
| 1–63 | резерв/не используется | 63 |
| 64 | `KidStartScrn` | 1 |
| 65 | `KidStartBlock` | 1 |
| 66 | `KidStartFace` (направление; при загрузке инвертируется XOR `$ff`, `SUBS.S:1516-1518`) | 1 |
| 67 | заполнитель | 1 |
| 68 | `SwStartScrn` (стартовый экран меча) | 1 |
| 69 | `SwStartBlock` | 1 |
| 70 | заполнитель | 1 |
| 7194 | `GdStartBlock[1..24]` — стартовый блок стражника на экране; **≥30 = "стражника нет"** (`AUTO.S:1832-1834`, `SUBS.S:1677-1679`) | 24 |
| 95118 | `GdStartFace[1..24]` (86 = "стражника нет", см. `ShadFace cmp #86` по всему `AUTO.S`) | 24 |
| 119142 | `GdStartX[1..24]` — пересчитывается заново из блока при старте уровня, значение на диске почти не используется (`SUBS.S:1674-1690`) | 24 |
| 143166 | `GdStartSeqL[1..24]` | 24 |
| 167190 | `GdStartProg[1..24]` — "программа"/поведение ИИ стражника | 24 |
| 191214 | `GdStartSeqH[1..24]` — обнуляется при старте (`SUBS.S:1685-1686`) | 24 |
| 215–255 | резерв/не используется | 41 |
Проверено на `LEVEL1`: байт по смещению файла 0x800 = `0x18`=24 (число
активных экранов = 23+1); по смещению 0x840 — `01 00 ff 00 00 00 ff 1e 1e
11 1e 1e ...``KidStartScrn=1, KidStartBlock=0, KidStartFace=$FF,
SwStartScrn=0, SwStartBlock=0`, далее 24 байта `GdStartBlock`, в основном
`0x1e`(30, "нет стражника"), с реальной расстановкой только на экране 3
(`0x11`=17) и экране 23 (`0x06`) — согласуется с уровнем, где всего два
стражника.
### 1.5 Как уровень попадает с диска (важно: имя файла — не игровой механизм)
В рантайме нет чтения "по имени файла LEVELn" — это чисто утилита для
экспорта в этом репозитории. Реально `LOADLEVELX` (`MISC.S:795-809`)
использует фиксированные таблицы по номеру уровня `bluepTRKlst`/
`bluepREGlst` (`MISC.S:776-787`), дающие физическую **дорожку (1-33)** и
**регион (0/1)**, затем `rdbluep` (`MASTER.S:598-616`) вызывает
низкоуровневое чтение `rw18` (`RdGrpErr`) 9 физических групп по 256 байт
(`$b7-$bf`) — 9×256=2304 байта — прямо в буфер blueprint; регион 0/1 выбирает
половину 18-секторной дорожки (два уровня делят одну дорожку). Файлы
`LEVELn` в этом репозитории — реконструкция этого сырого блока для удобства
работы с инструментами.
---
## 2. Формат изображений/спрайтов (`IMG.CHTAB1-7`, `IMG.BGTAB1/2.DUN/.PAL`)
Каждый такой файл грузится целиком по **фиксированному адресу**, заданному
константами `chtableN`/`bgtableN` (`GAMEEQ.S:9-18`):
```
chtable1=$6000 chtable2=$8400 chtable3=$0800 chtable4=$9600
chtable5=$a800 chtable6=$6000 chtable7=$9f00
bgtable1=$6000 bgtable2=$8400
```
— то есть смещения внутри файла один-в-один совпадают с адресами в памяти
после загрузки.
### 2.1 Раскладка контейнера
Восстановлено из заголовка-комментария "Image table format" в `HIRES.S:181-186`,
процедуры разрешения указателя `setimage` (`HIRES.S:263-277`) и
`GETWIDTH`/`PREPREP` (`HIRES.S:283-339`):
```
Смещение 0 : 1 байт — число изображений в таблице (максимум 127,
в образцах встречается 0x7f)
Смещение 1..254 : 127 × 2-байтных little-endian указателей
(указатель на изображение N — по смещению 1+(N-1)*2,
N=1..127) — АБСОЛЮТНЫЕ адреса в адресном пространстве
фиксированной загрузки этой таблицы, указывающие на
запись данных этого изображения
Смещение 255 : заполнитель (таблица указателей занимает ровно 256 байт)
Смещение 256 (база+0x100) и далее:
последовательно идущие записи данных изображений:
байт 0: ширина (в байтах на строку)
байт 1: высота (число строк)
байты 2..(2+ширина*высота-1): сырые байты пикселей,
слева направо, сверху вниз, БЕЗ сжатия
```
Проверено вручную на `IMG.BGTAB1.DUN`: с точной арифметикой индексов из
`setimage` (`Y = image*2 - 1`, `HIRES.S:264-267`) первые ~30 записей дают
строго возрастающую последовательность указателей `0x6101, 0x6133, 0x6159,
0x618b, 0x61c9, 0x61fb, 0x6221, 0x6313, 0x63c9, ...` — указатель
изображения #1 приходится ровно на `bgtable1 ($6000) + 0x100`, то есть точно
на конец 256-байтной таблицы указателей. Это независимо подтверждает и
размер таблицы, и семантику указателей.
**Важно: сжатия в этом формате нет.** RLE/дельта-упаковка (`SngExpand`/
`DblExpand`/`DeltaExpPop`/`DeltaExpWipe` в `01 POP Source/Source/UNPACK.S`)
применяется только к полноэкранным изображениям (титры/пролог/катсцены), но
не к CHTAB/BGTAB — спрайты и фоновые тайлы хранятся как чистые упакованные
байты hi-res/double-hi-res экрана Apple II, без какого-либо RLE или дельты.
### 2.2 Параметры отрисовки (не часть файла ресурса)
При выводе спрайта (`LAY`/`FASTLAY`/`PEEL` и т.д., `HIRES.S:658-1740`)
используются zero-page параметры `PAGE/XCO/YCO/OFFSET/IMAGE/OPACITY/TABLE/
BANK` (описаны в `HIRES.S:155-178`): `OFFSET` (0–6) — горизontальный сдвиг на
под-байтовый пиксель, `OPACITY` выбирает режим совмещения (AND/OR/STA/XOR/
маска-OR) плюс отдельный бит горизонтального зеркалирования (бит 7). Это
чисто рантайм-параметры отрисовки, не хранящиеся в файле ресурса. Точный
механизм барабанного сдвига для `OFFSET` (таблицы `HRTABLES.S`/`YLO`/`YHI`)
не прослежен до конца — при необходимости требует отдельного анализа.
### 2.3 Инструмент DRAZ (авторская утилита создания спрайтов)
В `04 Support/DRAZ` нет исходников самой утилиты DRAZ — только файлы данных
(`PAC.*` — позы персонажей, и уже скомпилированные `IMG.*`), поэтому
внутренний пайплайн DRAZ (как позы превращаются в CHTAB) напрямую не виден.
Формат контейнера выше выведен полностью из кода движка-потребителя, что
является надёжным, но косвенным источником.
Отдельно: в игровой логике списков объектов (`ADDBACK`, `GRAFIX.S:191-214`)
встречается **рантайм-упаковка ссылки на фоновую картинку** в один байт: бит
7 выбирает `bgtable1` или `bgtable2`, биты 0-6 — номер картинки в таблице
(0-63). Это соглашение для внутриигровых списков объектов (`bgIMG` и т.п.), а
не свойство самих файлов CHTAB/BGTAB на диске.
---
## 3. "Главного индекса ресурсов" не существует
В отличие от DOS-версии (см. `docs/MSDOS_RESOURCE_FORMAT.md`), в рантайм-коде
Apple II **нет обобщённого справочника "имя ресурса → расположение на
диске"**. Расположение каждого ресурса зашито напрямую как таблицы
дорожка/группа-секторов прямо в коде загрузчика:
- Уровни: `bluepTRKlst`/`bluepREGlst` (`MISC.S:776-787`), используются
`LOADLEVELX`/`LOADLEVEL` (`MISC.S:795-809`, `MASTER.S:467-481`).
- Альтернативные наборы фонов/персонажей: `bg1trk`/`bg2trk`/`ch4trk`/`ch4off`
(`MASTER.S:522-528`).
- Массовая загрузка при старте (chtable1-7, bgtable1-2, seqtable и т.д.):
прямые вызовы `rw18`/`RdGrp`/`RdSeq` с литеральными hex-списками
групп-секторов в `MASTER.S:1250-1360` и `BOOT.S:100-118`.
Весь дисковый ввод-вывод идёт через нестандартный низкоуровневый драйвер
`rw18` (`rw18 = $d000`, `EQ.S:11-12`; папка `02 POP Disk Routines/RW1835`),
реализующий нестандартный формат **18 секторов/дорожку** (вместо 16 у
стандартного DOS 3.3) — этим объясняется, почему регионы уровня (9×256Б)
идут парами на одной физической дорожке. Символические имена
`chtableN`/`bgtableN` в `GAMEEQ.S` — ближайший аналог "индекса ресурсов", но
они связывают ресурс с **фиксированным адресом в ОЗУ**, а не с положением на
диске; связь с диском — отдельная, вручную сопровождаемая таблица,
сопоставленная с ресурсом лишь порядком вызовов загрузчика.
---
## 4. Что ещё не восстановлено (открытые вопросы)
- Точное назначение бит `secmask` (%11000000) в `BLUETYPE` — не встречено
использование в доступном игровом коде (возможно, поле только для
редактора уровней, не читается движком).
- Механизм барабанного сдвига `OFFSET` для суб-байтового позиционирования
спрайта по X (`HRTABLES.S`) — не прослежен в деталях.
- Внутренний формат авторских файлов `PAC.*` инструмента DRAZ (как позы
скелетной анимации превращаются в растровые кадры CHTAB) — исходники DRAZ
отсутствуют в репозитории, можно только косвенно восстановить по
результату (уже скомпилированным `IMG.*`).
+199
View File
@@ -0,0 +1,199 @@
# Prince of Persia — Kid (персонаж): анализ и план
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
`memory/pop_background_strategy`) — Kid развиваем в том же `roomtest` как
новый PoC (решение пользователя: старый `poc/` не трогаем).
**Копирайт:** спрайты Kid (`SDLPoP/data/KID`) — Broderbund/Ubisoft.
Использование настоящей графики Kid — сознательное решение пользователя
(в отличие от плейсхолдера в старом `poc/`, см. PORT_PLAN §5.1).
---
## 1. Как устроен персонаж в оригинале (что портируем)
### 1.1 Состояние — `char_type` (14 полей, types.h)
```
frame текущий номер кадра (индекс во frame_table_kid)
x, y позиция (byte; x — с учётом direction)
direction -1 влево / 0 вправо
curr_col, логическая клетка (тайл), где персонаж
curr_row
action КАТЕГОРИЯ действия (actions_*, см. 1.2)
fall_x, скорость падения (fall_y<22 = 1 ряд, <33 = 2 ряда)
fall_y
room комната
repeat счётчик для удержания-ввода (напр. повторный прыжок)
sword есть ли меч (бой — вне Фазы 1)
alive жив/мёртв
curr_seq УКАЗАТЕЛЬ в seqtbl (байткод текущей последовательности)
```
Состояние крошечное — легко живёт в W2.
### 1.2 Категории действия — `actions_*` (9 шт)
`0 stand`, `1 run_jump`, `2 hang_climb`, `3 in_midair`, `4 in_freefall`,
`5 bumped`, `6 hang_straight`, `7 turn`, `99 hurt`. `action` определяет,
как `check_action()`/`play_kid()` реагируют на ввод и физику каждый тик.
### 1.3 Движок анимации/движения — ГЛАВНОЕ
**Движение НЕ физика, а байткод + per-frame смещения** (подтверждает
PORT_PLAN §6). Три уровня:
1. **`seqtbl`** — байткод-программа на действие. Опкоды (types.h):
`SEQ_DX`(0xFB) сдвиг x на amount×direction, `SEQ_DY`(0xFA) сдвиг y,
`SEQ_FLIP`(0xFE) разворот, `SEQ_JMP`(0xFF)/`SEQ_JMP_IF_FEATHER`(0xF7),
`SEQ_UP`/`SEQ_DOWN`(0xFD/0xFC) смена ряда, `SEQ_ACTION`(0xF9) задать
`Char.action`, `SEQ_SET_FALL`(0xF8), `SEQ_KNOCK_UP/DOWN`, `SEQ_SOUND`,
`SEQ_DIE`/`SEQ_END_LEVEL`/`SEQ_GET_ITEM`. **Байт < 0xF0 = НОМЕР КАДРА**
→ ставит `Char.frame` и play_seq возвращается (один кадр за тик).
2. **`play_seq()`** (seg006.c:570) — интерпретатор: крутит опкоды из
`seqtbl + Char.curr_seq`, пока не встретит кадр. ~15 case — портируется
1-в-1. **Квирк:** seqtbl использует АБСОЛЮТНЫЕ DOS-адреса в JMP;
`SEQTBL_0 = seqtbl - SEQTBL_BASE(0x196E)` — при порте пересчитать
базу (JMP-адреса в наших данных).
3. **`frame_table_kid[]`** (seg006.c:127, ~180 кадров) — на КАЖДЫЙ кадр:
`{image, sword_flags, dx, dy, flags}`. `image` — индекс спрайта Kid;
`dx/dy` — смещение позиции ЭТОГО кадра; `flags`: 0x1F weight_x, 0x20
thin, 0x40 needs_floor, 0x80 even/odd-pixel (влияет на x-рендер).
**Тик персонажа:** `play_kid()` (диспетчер по action+вводу) → `play_seq()`
(двигает curr_seq, ставит кадр, применяет seq-dx/dy) → `frame_table[frame]`
даёт image+собственные dx/dy → позиция и спрайт. У нас это ложится на
`sprite_frame`+`sprite_move` (НЕ `sprite_anim`/`sprite_moveto` — см.
PORT_PLAN §6: авторские таблицы, не автопрогрессия).
### 1.4 Управление — `control_kid()`/`read_user_control()` (seg006.c)
Читает ввод (у нас — held-state `kbd_raw`, уже готово, §2 PORT_PLAN) и по
`Char.action` выбирает последовательность (`seqtbl_offset_char(seq_id)`).
Логика «что можно из какого состояния» — ядро ощущения PoP.
### 1.5 Взаимодействие с картой — collision (seg006.c)
`check_on_floor()`/`start_fall()` — пол под ногами / падение в яму;
`in_wall()` — упор в стену (сдвиг наружу); `check_grab()`/
`can_grab_front_above()` — зацеп за уступ; `fell_out()` — вывалиться из
комнаты; `check_spiked()`/loose — ловушки; `fall_accel()`/`fall_speed()`
ускорение падения. Всё читает ТИП тайла (`get_tile`) — у нас это уже
разобранные `fg[]/bg[]` (level.h/room1_data.h).
---
## 2. Спрайты Kid (219 шт, 16 цветов, 177 КБ)
- 219 PNG (`data/KID`), 16-цветные (палитра `res400.pal`, 16×RGB как env/
wall), макс кадр **53×35** — влезает в лимит движка 64×64. 177 КБ в
8bpp.
- `frame_table_kid` отображает кадр→`image` (индекс спрайта). Число
РАЗЛИЧНЫХ image — уточнить (≤219); паковать те, что реально используются
платформинг-последовательностями Фазы 1 (не все 219 — бой/катсцены
отдельно).
- **Палитра:** Kid 16 цветов → слоты Sprinter `0x70-0x7F` (env 0x50, wall
0x60 уже заняты; Kid не пересекается). Пиксель i: 0→0xFF, i→0x70+i.
Тот же пайплайн, что `pop_pack_bg.py`.
- **Атлас:** прямая адресация по номеру image (как фон): `kid[img>>5]`,
idx `img&31`; ~7 EMM-страниц (или SHIFT=4). Свой пакер `pop_pack_kid.py`
(переиспользовать код `pop_pack_bg.py`).
### 2.1 РЕШЕНИЕ ДО СТАРТА: per-frame offset vs padding
Кадры Kid — РАЗНОГО размера, а `sprite_t` рисует от угла фикс. w/h. Два
пути (см. PORT_PLAN §6.1, `memory/png_strip_padding_tradeoff`):
- **Padding** (bottom-center) — просто, но 219×53×35 ≈ 406 КБ (раздув ×2.3).
- **Per-frame offset** — хранить XCO/YCO кадра (у оригинала он и есть,
`APPLEII_RESOURCE_FORMAT §2.2`), рисовать `blit(x+xco, y+yco)`; паддинг не
нужен, память по факту (177 КБ). Требует лёгкого расширения хранения
(offset рядом с кадром) ИЛИ ручного смещения в коде рендера Kid.
**Рекомендация:** per-frame offset — оригинал так и делает (frame_table dx/dy
+ image XCO/YCO), даёт точное позиционирование И экономию. Хранить xco/yco
в нашей копии frame_table (добавить 2 байта/кадр — ~360 Б). Не тянуть
расширение `sprite.h` — рисовать Kid прямым `gfx_blit(x+xco, y+yco, img)`
(как фон), НЕ через retained `sprite_t`, раз позиция и кадр всё равно
задаются вручную каждый тик.
---
## 3. Данные для порта (объём)
- `frame_table_kid` → C-массив ~180×(5+2 offset) ≈ 1.3 КБ (const, ROM).
- `seqtbl` (нужные последовательности) → C-массив байт. Весь seqtbl ~1-2 КБ;
для Фазы 1 можно взять только платформинг-последовательности (вырезать
бой/гардов 55-92) — оценить после разметки. JMP-адреса пересчитать под
свою базу.
- Спрайты — атласы (EMM, не W2).
---
## 4. Фазы работы (по твоему списку, порядок по зависимостям)
**Фаза K0 — конвейер спрайтов + отрисовка одного кадра**
- `pop_pack_kid.py`: 219 (или подмножество) → `kid*.atl` + `kid.pal`
(слоты 0x70), таблица кадр→image + xco/yco.
- Отрисовать Kid ОДНИМ кадром (stand) в roomtest поверх фона на верном
тайле — проверить палитру/позицию/прозрачность на MAME.
- Артефакт-цель: Kid стоит на уступе комнаты 1 как в `1.1-2.png`.
**Фаза K1 — движок анимации (play_seq + frame_table)**
- Портировать `play_seq()` (интерпретатор) + `frame_table_kid` + минимальный
`seqtbl` (stand/run/turn).
- Прогнать несколько последовательностей вручную (stand→run→stop) —
проверить, что кадры и смещения совпадают с оригиналом (сверять с
SDLPoP/скриншотами, тайминг 50 Гц).
**Фаза K2 — управление на месте + ходьба (твои а, б)**
- `control_kid` подмножество: stand (2), run (1/84/13), turn (5/6),
standing_jump (3), crouch (50/49), safe_step (29-44 — аккуратный шаг).
- Held-state через `kbd_raw` (готово).
**Фаза K3 — коллизия с картой (твой п.3)**
- `check_on_floor`/`start_fall` — падение в ямы (тип тайла под ногами из
`fg[]`); `in_wall`/стоп у стены; `fell_out` (край экрана — пока без
перехода комнат).
- Падения/приземления (seq 7/17/19/20) + `fall_accel/fall_speed`.
**Фаза K4 — прыжки и повисание (твои а-прыжок, в)**
- run_jump (4), jump_up (28/14), grab (8/16/24), climb_up (10)/down (68),
hang (25/6), release (11/23). Это самый «PoP-овый» кусок — сверять
дистанции/тайминг с оригиналом (не на глаз).
**Фаза K5 — прочее (твой г)**
- drink (78), level_door (70), crouch_hop (79), spiked/loose/chomped
(ловушки, если тайлы есть в комнате), death (71).
Бой (меч, seq 55-92, стражники — seg005) — ВНЕ этого плана (отдельная фаза
полного приложения, PORT_PLAN §7 Фаза 3).
---
## 5. Риски/решения ДО кода (правило defer_unexplained_quirks)
1. **Per-frame offset** (§2.1) — решить до K0 (влияет на формат данных).
Рекомендация: xco/yco в frame_table, прямой blit.
2. **seqtbl rebasing** — JMP-адреса абсолютные (SEQTBL_BASE 0x196E); при
порте пересчитать в оффсеты своего массива. Проверить на 1-2 seq.
3. **Тайминг** — оригинал (DOS) фиксированный тик; наш 50 Гц. Если
логическая частота кадров иная — пересчёт dx/dy (PORT_PLAN §8.4).
Сверять дистанцию бега/прыжка с эталоном.
4. **Число реально нужных кадров/последовательностей** для Фазы 1 —
разметить (вырезать бой/катсцены/гардов), чтобы не тянуть все 219
спрайта и весь seqtbl.
5. **Копирайт графики Kid** — подтверждено решение пользователя (§вводная).
---
## 6. Что переиспользуем (готово)
- Фон комнаты (`pop_bg.c`) — Kid рисуется ПОВЕРХ (сейчас — прямым blit;
heal против фона — когда/если понадобится через RAM-копию, фон её уже
заполняет, `GFX_BANK_TRANSPARENT`).
- `kbd_raw` held-state (§2 PORT_PLAN) — готов и проверен.
- Пакер спрайтов/палитра (`pop_pack_bg.py`) — шаблон для `pop_pack_kid.py`.
- Разобранная карта комнаты (`fg[]/bg[]`, level.h) — для коллизий.
- `gfx_blit`/`gfx_w0_map` из W0-атласа — проверенный путь (bgtest/roomtest).
@@ -0,0 +1,301 @@
# Формат ресурсов Prince of Persia (MS-DOS, каталог `MSDOS/`)
Документ описывает бинарный формат `*.DAT`-файлов ресурсов DOS-версии PoP.
Исходников для этой версии нет, поэтому всё, что ниже — результат
структурного (эмпирического) анализа реальных файлов из `MSDOS/`, а не чтения
кода. Уровень уверенности указан для каждого раздела. Все находки проверены
скриптами (Python), которые разбирают файл и валидируют согласованность
(например: смещение+размер последней записи таблицы точно совпадает с
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
**Основной источник спецификации формата — `POP-DAT-FormatSpecifications.pdf`**
(и его текстовая конверсия `POP-DAT-FormatSpecifications.txt` в этой же папке,
для grep/цитирования): *«Prince of Persia — Specifications of File Formats»*,
Princed Development Team, 2008 — каноническая спецификация формата `DAT v1.0`,
на которой построен и SDLPoP, и Princed Resources. Разбирает контейнер, индекс,
чек-сумму, кодеки изображений (RLE / LZG), палитры, формат уровней (room
mapping, wall-drawing, room-linking, guards, start position, door events),
звук (digital waves / MIDI / PC speaker), бинарные файлы и Mac-варианты. При
любом расхождении между эмпирическими находками ниже и этим документом —
источником истины считать спецификацию (сверять §-номера: её §3.x).
Дополнительно как справка при реализации (порт на ZX Sprinter):
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
рабочий код чтения `.DAT`-файлов и полный кодек изображений/уровней. Точные
структуры (`dat_table_type` и т.п.), процитированные ниже, получены через
автоматический пересказ содержимого файла третьей стороной, а не через
прямое чтение исходника — поэтому такие детали помечены как "требует сверки
при реализации", в отличие от эмпирически подтверждённых байтовых оффсетов.
- **Princed Resources / PR** (github.com/NagyD/PR, princed.org, GPLv2) — это
профильный инструмент именно для распаковки/запаковки `.DAT`-ресурсов PoP
(версии DAT 1 и 2), сделанный тем же автором. В его документации
(`doc/Dataformats.md`) официально описаны экспортные форматы ресурсов —
это подтверждает и уточняет часть находок ниже (см. §3–4), и является более
надёжным источником, чем самостоятельная догадка по байтам.
**Важная находка:** репозиторий SDLPoP в папке `data/` содержит не только
код движка, но и **реальные ресурсы игры** — как сырые `.DAT`-контейнеры, так
и уже распакованные поштучно файлы (PNG-кадры спрайтов, `.pal`-палитры,
`.bin`-дампы уровней), см. §7. Это готовый источник ассетов и одновременно
независимая проверка формата, описанного в этом документе.
---
## 1. Общий контейнер `.DAT` (уверенность: высокая, подтверждено на 28 файлах)
Каждый `*.DAT`-файл (кроме служебных `config.dat`/`setup.dat`, см. §5) — это
простой архив-контейнер: блок данных + оглавление (каталог ресурсов) в конце
файла.
### 1.1 Заголовок файла (6 байт, смещение 0x00)
| Смещение | Размер | Поле | Значение |
|----------|--------|--------------|----------|
| 0x00 | 4 | `tableOffset`| LE u32. Абсолютное смещение в файле, с которого начинается таблица оглавления. Совпадает с "концом данных". |
| 0x04 | 2 | `tableSize` | LE u16. Размер таблицы оглавления в байтах. |
Инвариант, подтверждённый на всех 28 `.dat`-файлах в каталоге:
```
tableOffset + tableSize == размер файла (без исключений)
```
Данные ресурсов идут сразу после заголовка, начиная с байта 0x06, и
заканчиваются на `tableOffset`.
### 1.2 Таблица оглавления (по смещению `tableOffset`, длиной `tableSize`)
Таблица — плоский массив записей по 8 байт. Количество записей:
`tableSize / 8` (округление вниз; в файле почти всегда остаётся 2 "лишних"
байта в хвосте таблицы — назначение не установлено, вероятно, служебное поле
инструмента-упаковщика или паддинг; на итоговый разбор не влияет).
Запись (8 байт):
| Смещение в записи | Размер | Поле | Описание |
|---|---|---|---|
| 0 | 2 | `size` | LE u16 — размер данных ресурса в байтах |
| 2 | 2 | `id` | LE u16 — идентификатор ресурса |
| 4 | 2 | `offset` | LE u16 — **абсолютное** смещение данных ресурса в файле (не относительное!) |
| 6 | 2 | `reserved` | во всех проверенных записях (сотни штук) всегда `0x0000` |
Проверено на `levels.dat`: 16 записей, `id`=2000..2015, и `offset[i] + size[i]
== offset[i+1]` для всех соседних записей, а последняя запись заканчивается
ровно на `tableOffset` — то есть данные абсолютно плотно упакованы, без
пробелов, для этого файла. В других файлах (например `guard.dat`) между
записями изредка есть небольшие зазоры в несколько байт (вероятно, выравнивание
или "мёртвые" байты от инструмента-компоновщика) — не является нарушением
формата.
### 1.3 Диапазоны `id` по типам файлов (собрано эмпирически)
Похоже, что числовые ID образуют условные "пространства имён" по типу
контента — вероятно, глобальные константы в оригинальном коде:
| Файл(ы) | Диапазон `id` | Кол-во записей | Предполагаемое содержимое |
|---|---|---|---|
| `levels.dat` | 20002015 | 16 | id=2000 — служебный блок (16 байт, см. §3); id=2001..2015 — 15 уровней |
| `guard.dat`, `fat.dat`, `skel.dat`, `shadow.dat` | 750–784 (варьируется) | ~3035 | id=751(750) — служебный блок; остальные — кадры анимации спрайта |
| `vizier.dat` | аналогично guard | — | кадры анимации визиря |
| `kid.dat` | ~400+ | 220 | кадры анимации игрока (намного больше — герой умеет гораздо больше действий) |
| `guard1.dat`, `guard2.dat` | 750 (1 запись) | 1 | вероятно, дополнительные/альтернативные кадры/варианты |
| `title.dat` | 40–55 | 12 | картинки титульного экрана/логотипов |
| `cpalace.dat`,`epalace.dat`,`vpalace.dat`,`cdungeon.dat`,`edungeon.dat`,`vdungeon.dat` | 2001343 | 205238 | фоновые тайлы дворца/подземелья, отдельно для CGA(`c*`)/EGA(`e*`)/VGA(`v*`) |
| `pv.dat` | 800981 | 103 | доп. графика (возможно, "Prince/Vizier" катсцены) |
| `digisnd1/2/3.dat` | 10000+ | 20–44 | оцифрованный звук (Covox/Disney Sound Source) |
| `midisnd1/2.dat` | 10024+ / аналог | 16 | General MIDI музыка |
| `mt32snd1/2.dat` | 10000+ | 24/7 | музыка для Roland MT-32 |
| `ibm_snd1/2.dat` | 10000+ | 44 | музыка/эффекты через PC-спикер |
| `prince.dat` | — (1 крупный ресурс) | — | MIDI-тема (вероятно, финальная тема "Принц"/титры — см. текстовые события "The Princess awaits") |
Во всех файлах первая (наименьшая по `id`) запись — маленький "служебный"
ресурс (6–44 байта), стоящий перед основным контентом. Скорее всего это
локальная мини-таблица/палитра/список ссылок для данного набора ресурсов —
по аналогии с тем, что у уровней id=2000 отдельно от самих уровней (см. §3).
---
## 2. Формат уровня (`levels.dat`, id=2001..2015) — уверенность: высокая
Каждая запись уровня имеет размер **2305 байт** и по данным полностью
совпадает по объёму с уровнями из Apple II версии (`01 POP Source/Levels/LEVELn`
— ровно **2304 байта** каждый, см. `docs/APPLEII_RESOURCE_FORMAT.md`).
Вывод: формат карты уровня в DOS-версии, судя по всему, **унаследован
практически без изменений от оригинального Apple II формата** (Джордан
Мехнер писал игру на 6502 и данные уровней переносились как есть), с добавлением
одного лишнего байта в DOS-упаковке (2304+1=2305 — вероятно, контрольный байт/
маркер конца, добавленный DOS-упаковщиком ресурсов, а не часть игровых данных).
**Практическое следствие:** байтовая структура самого уровня (тайлы 3×10 на
экран, 24 экрана, таблицы стражников, дверей и т.д.) должна документироваться
один раз — по исходникам Apple II (см. соответствующий раздел), и напрямую
применяться к DOS `levels.dat`, отбросив 1 лишний байт в конце каждой записи.
Байтовые значения тайлов в дампе (в основном 0x00–0x39) визуально согласуются
с диапазоном небольших целых кодов тайлов, что для формата карты и ожидается.
Первая запись, id=2000, размер 16 байт — не уровень, а отдельный маленький
блок (возможно: количество уровней, начальный уровень, версия формата,
стартовые координаты игрока/охраны по умолчанию). Точное назначение не
установлено — требует сопоставления с диз­ассемблированным кодом загрузчика
уровней (в SDLPoP это, по всем признакам, отдельная процедура чтения
`level` ресурса).
**Сверка с независимой распаковкой SDLPoP (`data/LEVELS/`):** там лежат файлы
`res2000.bin``res2015.bin` (16 штук — количество совпадает). Байты
`res2001.bin` содержательно совпадают с тайловыми данными нашей записи
id=2001 (та же последовательность значений тайлов) — это подтверждает, что
нумерация id верна. Но есть нестыковка по размеру: у SDLPoP `res2000.bin`
**2305 байт** (как и все остальные), тогда как в нашем локальном
`levels.dat` запись id=2000 — всего **16 байт**. Скорее всего, это разные
релизы/сборки игры (см. §7 — размеры некоторых `.dat` у SDLPoP и у нас уже
отличались), и в версии SDLPoP маленький служебный блок либо отсутствует,
либо пронумерован иначе. Это не меняет сам формат контейнера, но означает,
что **точную семантику 16-байтного блока id=2000 в нашей копии игры пока
нельзя проверить через данные SDLPoP** — открытый вопрос.
---
### 2.1 Кросс-подтверждение по исходникам Apple II
Фоновый анализ исходников Apple II (см. `docs/APPLEII_RESOURCE_FORMAT.md`)
подтверждает и объясняет структуру уровня напрямую по коду. Уровень на Apple
II — дамп структуры `blueprnt` (`EQ.S`): `BLUETYPE`(720Б, 24 экрана×30 тайлов)
+ `BLUESPEC`(720Б) + `LINKLOC`(256Б) + `LINKMAP`(256Б) + `MAP`(96Б, граф
соседних экранов) + `INFO`(256Б, метаданные/старт Кида/стражников) = ровно
2304 байта. Учитывая, что DOS-запись уровня — это ровно 2304+1 байт с
байтовыми значениями тайлов, укладывающимися в диапазон 0–29 (id тайла) плюс
служебные биты (аналогично `idmask=%00011111`, `reqmask=%00100000` из
`EQ.S:484-486`), можно с высокой уверенностью считать, что **DOS-версия
использует ту же самую раскладку `blueprnt`**, лишь с добавлением одного
байта (вероятно, контрольной суммы) в конце DOS-упаковки. Это снимает
необходимость отдельно реверсить формат уровня для DOS — таблица тайлов,
enum id (0=space...29=archtop4), формат `LINKLOC`/`LINKMAP` и `INFO` из
Apple II документа применимы напрямую.
## 3. Графика (спрайты и фоновые тайлы) — уверенность: средняя/низкая
Файлы `kid.dat`, `guard.dat`, `fat.dat`, `shadow.dat`, `skel.dat`,
`vizier.dat`, `title.dat`, `c/e/v-palace.dat`, `c/e/v-dungeon.dat`, `pv.dat`
хранят по контейнерному формату (§1) множество мелких чанков (десятки—сотни
байт каждый).
Что подтверждено:
- Наборы `shadow.dat`/`kid.dat` и `fat.dat`/`vizier.dat` содержат **побайтово
идентичные фрагменты** данных в начале файла — это ожидаемо: "Тень" (Shadow)
визуально копирует анимацию Кида, а "Толстый страж" (Fat guard, пасхалка)
переиспользует модель Визиря. Подтверждает, что персонажи одного "типа
тела" используют общий набор геометрии/анимации.
- Отдельные чанки *не* имеют очевидного унифицированного заголовка
(высота/ширина/палитра) фиксированного размера — попытка интерпретировать
первые байты чанка как `{height:u16, width:u16, flags:u16}` не подтвердилась
на реальных данных (получаются нереалистичные размеры для маленьких чанков).
Вероятно, как и в Apple II версии (см. `FRAMEDEF.S`/`SEQTABLE.S`), геометрия
кадра (ширина, высота, точка привязки) хранится **отдельно от самих
пиксельных данных** — в таблицах внутри `PRINCE.EXE`, а не в `.DAT`-чанке.
Сам чанк, вероятно, содержит только упакованные пиксельные данные
(RLE/дельта-упаковка, по аналогии с `UNPACK.S` в Apple II исходниках).
- Точный алгоритм упаковки пикселей **не восстановлен** в рамках этого
анализа по сырым байтам — байт-в-байт разбор распаковщика без
дизассемблирования `PRINCE.EXE` надёжно не сделать. **Но для практических
целей это не требуется**: см. §7 — в SDLPoP уже есть тот же самый набор
изображений в готовом, распакованном виде (PNG), которым можно пользоваться
напрямую как источником ассетов, не реализуя свой декодер `.DAT`-пикселей.
Писать собственный декодер имеет смысл только если понадобится читать
оригинальные `.DAT` "на лету" (например, для точной сверки контента именно
нашей копии игры) — тогда ориентир — исходник SDLPoP (`src/seg009.c`).
---
## 4. Звук — уверенность: высокая (по структуре), низкая (по деталям кодека)
Обнаружено 4 параллельных набора звуковых ресурсов под разные звуковые
устройства DOS-эпохи — типично для игр начала 1990-х с "звуковым меню":
| Файл | Устройство | Формат чанка |
|---|---|---|
| `midisnd1.dat`, `midisnd2.dat` | General MIDI / MPU-401 | каждый чанк = 2-байтовый LE-префикс длины + встроенный Standard MIDI File (`MThd`...`MTrk`...) |
| `mt32snd1.dat`, `mt32snd2.dat` | Roland MT-32/CM-32L | тот же формат: префикс длины + `MThd`/`MTrk`, с MT-32-специфичными SysEx (видны строки `MT-32.mff`, текстовые мета-события вроде `"The Princess awaits"`) |
| `prince.dat` | (аналогично MIDI) | отдельный крупный музыкальный ресурс, тот же MIDI-контейнер — вероятно, финальная тема |
| `digisnd1/2/3.dat` | Covox / Disney Sound Source / Sound Blaster (оцифрованный звук) | чанк начинается с нескольких служебных байт, среди которых слово `0x2AF8` = 11000 — похоже на частоту дискретизации 11 кГц; далее — сырые 8-битные PCM-сэмплы (значения кластеризуются вокруг ~0x7A–0x90, типично для беззнакового 8-бит аудио, смещённого к середине шкалы) |
| `ibm_snd1.dat`, `ibm_snd2.dat` | PC Speaker | чанк — последовательность троек байт похожих на (длительность, делитель_частоты) — простой формат "бипера", отличный от MIDI |
Подтверждено разбором первых чанков в каждом файле (см. байтовые дампы,
проверялись скриптом). Точная семантика полей внутри `digisnd`/`ibm_snd`
(разрядность, порядок байт служебного заголовка) не выведена до конца — при
реализации порта достаточно распознавания по типу файла и (для MIDI-семейства)
можно напрямую воспроизводить встроенный Standard MIDI File, пропустив
2-байтовый префикс длины.
---
## 5. Готовые распакованные ассеты в SDLPoP (`data/`) — практический источник для порта
Репозиторий github.com/NagyD/SDLPoP содержит папку `data/`, где, помимо
самих `.DAT`-контейнеров, каждый ресурс **продублирован в виде отдельно
распакованного файла**, названного по его `id` из таблицы оглавления (§1.2).
Проверено через GitHub API (`api.github.com/repos/NagyD/SDLPoP/contents/...`):
| Подпапка/файл в `data/` | Содержимое | Соответствие нашему разбору |
|---|---|---|
| `GUARD.DAT`, `GUARD1.DAT`, `GUARD2.DAT` | сырые `.DAT` | размер **побайтово совпадает** с нашими локальными `guard.dat`/`guard1.dat`/`guard2.dat` (6950 / 117 / 117 байт) |
| `DIGISND1.DAT`, `MIDISND2.DAT` и др. | сырые `.DAT` | размер **не совпадает** с нашими локальными файлами (48545 vs 50101, 18408 vs 18958) — другой релиз/сборка игры |
| `GUARD/res751.png``res784.png` | готовые PNG, по одному на кадр анимации, имя = `res<id>.png` | id-диапазон (751-784) точно совпадает с нашим разбором `guard.dat` |
| `VPALACE/res200.pal`, `res201.png`, `res202.png`, … | палитра (JASC `.pal`) + PNG-кадры фонов дворца, **VGA-вариант (256 цветов)** | id-диапазон (200+) совпадает с `vpalace.dat` |
| `LEVELS/res2000.bin``res2015.bin` | сырые дампы уровней по 2304-2305 байт | id совпадает с `levels.dat`; содержимое `res2001.bin` **сверено побайтово** с нашим id=2001 — тайловые данные совпадают |
| `KID/`, `PRINCE/`, `SHADOW/`, `SKEL/`, `VIZIER/`, `FAT/`, `TITLE/`, `VDUNGEON/`, `PV/`, `IBM_SND1/`, `IBM_SND2/`, `font/`, `music/` | аналогичные наборы для остальных ресурсов | не проверялись по отдельности, но структура (папка на каждый `.dat`, файлы `res<id>.ext`) наблюдается одинаково |
**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что
лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но
формат контейнера и нумерация `id` — те же самые. Практически это значит:
1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно
реализовывать декодер сжатия пикселей** — можно взять готовые файлы
`data/<ИМЯ>/res<id>.png` напрямую как исходный материал для конвертации
под видеорежим ZX Sprinter (в т.ч. `VPALACE`/`VDUNGEON` — уже
256-цветный VGA-арт, что прямо отвечает на вопрос про полноцветность).
2. Если в проекте важно использовать именно ту версию контента, что в наших
`MSDOS/*.dat` (а не версию из SDLPoP) — распаковку своих файлов всё же
придётся делать (кодек пикселей по-прежнему не восстановлен для сырых
`.DAT`, см. §3), либо принять решение работать с версией SDLPoP как
мастер-источником ассетов вместо своей.
---
## 6. Служебные не-ресурсные файлы
- `config.dat`, `setup.dat` — 28 байт, не являются ресурсными контейнерами
(не проходят проверку §1.1 — "размер" получается больше самого файла).
Скорее всего простые бинарные структуры настроек (звук/видеорежим,
выбранный на этапе `SETUP.EXE`/`INSTALL.EXE`), не связаны с игровым
контентом.
- `desktopd.cfg`, `setup.cfg` — текстовые/бинарные конфиги DOS-инсталлятора,
вне скоупа игровых ресурсов.
- `PRINCE.EXE` / `PRINCE.REM` — почти идентичны (отличие в единичных байтах
в районе смещения ~0x4ED0), похоже на кряк/патч одного байта проверки —
не относится к формату ресурсов.
- `old-games.nfo` — ASCII-арт NFO релиз-группы (old-games.ru), не игровые
данные.
---
## 7. Итоговая таблица уверенности
| Раздел | Уверенность | Как подтверждено |
|---|---|---|
| Контейнер `.DAT` (заголовок + таблица) | Высокая | Проверено скриптом на всех 28 файлах, инвариант offset+size выполняется без исключений; независимо подтверждено именованием `res<id>.*` в SDLPoP `data/` |
| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res<id>` именами файлов SDLPoP и побайтовым содержимым `res2001.bin` |
| Формат уровня = формату Apple II | Высокая (по размеру и содержимому), служебный блок id=2000 — открытый вопрос | Совпадение размера (2304 vs 2305), тайловые байты сходятся с `res2001.bin` из SDLPoP |
| Формат изображений/спрайтов (сырой `.DAT`) | Низкая-средняя | Контейнер подтверждён, кодек пикселей — нет; но практически закрыто наличием готовых PNG в SDLPoP `data/` (§5) |
| Формат звука (тип контейнера) | Высокая для MIDI-семейств, средняя для digisnd/ibm_snd | Явные MIDI-сигнатуры `MThd`/`MTrk` видны в байтах |
**Рекомендация для дальнейшей работы:** для получения арт-ассетов (спрайты,
фоны, палитры) — использовать готовые распакованные файлы из
`github.com/NagyD/SDLPoP/tree/master/data` (§5), это быстрее и надёжнее
самостоятельной реализации декодера. Декодер сырого `.DAT`-формата
изображений и точную семантику служебных полей `digisnd`/`ibm_snd`
(§3, §4) стоит восстанавливать только если понадобится читать именно нашу
локальную копию `MSDOS/*.dat` "как есть" — тогда ориентир прежний: исходник
SDLPoP (`src/seg009.c`, `src/data.c`/`data.h`).
File diff suppressed because it is too large Load Diff
+509
View File
@@ -0,0 +1,509 @@
# Prince of Persia на ZX Sprinter — план порта
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
`applications/PoP/PR` (github.com/NagyD/PR, GPLv2) — используются только как
справочник по структурам/константам оригинального движка и как источник
готовых распакованных ассетов (`SDLPoP/data/`), не как код для копирования.
---
## 1. Что уже есть в sprinter-cc и библиотеках (используем как есть)
Собрано из `docs/TODO.md`, `docs/libc-reference.md`, `docs/sprite-api-design.md`,
`libbgi/include/{gfx.h,sprite.h,graphics.h}`, `examples/rpgwalk`.
- **Графика 320×256×256** (`GFX_MODE_320x256x256`, режим 0x81) — разрешение и
глубина цвета совпадают почти впрямую с VGA-ассетами оригинала
(`SDLPoP/data/VPALACE`, `VDUNGEON` — уже 256-цветные PNG). Не нужно ужимать
в EGA/CGA палитру.
- **BGI-слой** (`graphics.h`) — примитивы, палитра, текст, `getimage/putimage`
— Фазы 1-2d готовы и проверены в MAME.
- **Спрайтовый движок v2** (`sprite.h`, ветка `sprite-engine-v2`) — ровно то,
что нужно персонажам PoP:
- retained-модель (`sprite_update`/`sprite_flip`, double-buffer, dirty-биты,
heal+blit за один проход);
- кадровая анимация по ленте (`sprite_anim`, LOOP/PINGPONG/ONCE,
горизонтальная/вертикальная лента) и tween-перемещение
(`sprite_moveto`, DDA без knowledge-heavy арифметики);
- Y-сортировка слоями (`gfx_sprite_ysort`, `layer`) — то, что нужно для
«Кид перед/за стражником» без ручной пересортировки;
- атласы в EMM-страницах (`atlas_t`/`atlas_load`) — на восьмерых
персонажей в `rpgwalk` уже работает: прямой прецедент для Кида/стражника;
- ограничение кадра ≤ 64×64 — с запасом (см. §3: кадры Кида в оригинале
~12-30 × 39-42 px).
- **Frame pacing** (`gfx_set_fps_div`) + цепочка кадровых IRQ — стабильный
логический тик независимо от рендер-нагрузки экрана (проверено MAME).
- **EMM-бюджет**: ~3.3 МБ свободно на старте (`memory/sprinter_emm_budget`) —
с большим запасом на все спрайт-атласы и предрендеренные фоны комнат (см.
§4) даже без выгрузки неиспользуемых уровней.
- **Файловый ввод-вывод** (FILE* v2, `fopen/fread/...`) — для загрузки
уровней/атласов/палитр с дискеты, по образцу `rpgwalk` (`atlas_load`,
`gfx_pal_fload`).
- **Клавиатура (событийная)** — `kbhit/getch/getkey` (ASCII + `KEY_*` скан-код
для стрелок), см. §2 — это НЕ то, что нужно для управления Кидом один в
один (см. ниже).
- **Звук** — `cbl.h` (потоковый CBL/COVOX, callback-модель, verified MAME) —
подходит для оцифрованных эффектов (`digisnd*.dat` — PC-звук
~11 кГц 8-бит, см. `MSDOS_RESOURCE_FORMAT.md` §4).
Вывод: **движок отрисовки и анимации почти не требует нового кода**
самый близкий по духу пример (`rpgwalk`: атласы, анимация, tween, дабл-буфер,
FPS-делитель) переносится на PoP почти без изменений архитектуры.
---
## 2. Единственный принципиальный пробел: удержание клавиш
**Спайк проведён (2026-07-15), вопрос закрыт артефактами — не догадкой.**
`getch`/`getkey` — это события ESTEX WAITKEY/SCANKEY (по нажатию), без чёткой
информации о СОСТОЯНИИ (что зажато прямо сейчас, несколько клавиш
одновременно). Prince of Persia на управлении требует именно состояния:
держать направление (бег) + одновременно нажать вверх (прыжок вперёд), держать
Shift (модификатор) + направление и т.д.
### 2.1 Находки
1. **`docs/converted/ProgrammerManual.txt` документирует функцию, которую мы
раньше пропустили: `CTRLKEY` (ESTEX $33h)** — «Получить состояние
клавиатуры». Дословно: «данные берутся не из буфера клавиатуры (как в
остальных функциях), а непосредственно из результатов ПОСЛЕДНЕГО
сканирования» — то есть это НАСТОЯЩЕЕ live-state, не событие. Но
покрывает только модификаторы: Left/Right Shift, Ctrl, Alt,
Rus/Lat, Num/Scroll/Caps Lock, Insert (не обычные клавиши вроде стрелок).
Готовое решение для «держать Shift = бежать» — тривиальная обёртка,
без архитектурных рисков.
2. Для ОБЫЧНЫХ клавиш (стрелки, буквы) такого live-state нет нигде в ESTEX —
`WAITKEY`/`SCANKEY`/`TESTKEY` ($30/$31/$37h) — все три отдают ОДИНАКОВЫЙ
формат «очередное нажатие», без release. `TESTKEY` не удаляет событие из
буфера (полезно для «подсмотреть, не потребляя»), но это тоже разовое
нажатие, не состояние.
3. Автоповтор клавиатуры (typematic) не годится как замена held-state:
`MAME_MCP_GUIDE.md` фиксирует задержку до первого повтора ~1 секунда
(типично для PS/2) — на порядок медленнее кадра (20 мс), не подходит для
платформера.
4. **Решающий артефакт — `libc/irq/_irq_tramp.c` (сам трамплин прерывания,
не гипотеза):** вектор 0xFF общий для кадра/клавиатуры/CBL. Ветка
клавиатуры (бит 0 порта 0x19 = SIO-A RR0 «байт принят») делает буквально
`jp 0x0038` (прямиком в DSS) **до какого-либо чтения порта данных 0x18 И
до нашей кадровой цепочки (`_irq_chain`)** — наш `irq_chain_add`
вообще не видит клавиатурные прерывания, они физически не доходят до
цепочки (см. `tr_notkbd`/`tr_frame` разбор в файле). Значит текущая
инфраструктура (тот же механизм, что несёт FPS-делитель) НЕ дает
зацепки для клавиатуры без правки самого трамплина.
5. Регистр данных SIO (порт 0x18) — аппаратный приёмный буфer, чтение
деструктивно (дёргает байт из очереди); кто прочитал первым, тот и
владеет байтом. Значит «подглядеть, не мешая DSS» технически
невозможно — необходимо либо совсем не трогать этот путь (статус-кво),
либо взять его СЕБЕ полностью на время геймплея.
### 2.2 Рекомендация (конкретная, не три равнозначных варианта)
**A. Тривиально, почти без риска — обернуть `CTRLKEY` ($33h)** отдельной
функцией (например `kbd_mod_state()` в `<conio.h>`) — даёт настоящий
held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное
решение.
**B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL.** В
`_irq_tramp.c` уже есть пример «приватного» пути на том же векторе 0xFF,
который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой
хук `_irq_cbl_hook`, полный сейв, свой `reti`). Предлагаемый новый
компонент `<kbd_raw.h>` — симметричный: ветка по биту 0 порта 0x19 читает
порт 0x18 САМА (декодирует PS/2 make/break, `0xF0`-префикс — протокол
уже задокументирован в `docs/samples/sprinterKeybLib.asm`), ведёт битовую
карту «клавиша N зажата», и НЕ прыгает в DSS, пока путь активен —
жизненный цикл `kbd_raw_open()`/`kbd_raw_close()` один в один как у
`cbl_open`/`cbl_close`.
**Важное следствие (сообщить пользователю явно, не прятать):** пока
`kbd_raw_open()` активен, DSS вообще не получает клавиатурных байт —
`kbhit/getch/getkey/CTRLKEY` заведомо не будут работать, ESC для выхода
в DSS-смысле тоже (нужно проверять raw-битовую карту самим). Это
нормально для активной фазы геймплея (у самой игры и так свой цикл
ввода), но означает: экраны/паузы, которым нужен ESTEX-ввод (например,
диалог сохранения через `fopen`, если тот когда-либо потребует ввода
с консоли), должны на это время `kbd_raw_close()`.
**Не рекомендую вариант «таймаут-эвристика поверх SCANKEY»** — after
находки о typematic-задержке ~1с он не даёт нужной задержки для игры;
рекомендация A+B закрывает потребность без компромиссов.
**Статус: A+B РЕАЛИЗОВАНЫ (2026-07-15, по согласованию с пользователем).**
- A: `kbd_mod_state()``libc/conio/kbd_mod_state.c` + `<conio.h>`
(`KBD_MOD_*`).
- B: `<kbd_raw.h>` (`libc/kbd/`) + правка `libc/irq/_irq_tramp.c`
(новая ветка на бите 0 порта 0x19: raw активен → сама читает порт
0x18, декодирует make/break, НЕ чейнится к DSS; raw выключен —
поведение как раньше, без изменений). Трамплин вырос со 150 до
220 байт — `_IRQ_TRAMP_BUF_SIZE` поднят с 224 до 288 (было 4 байта
запаса, стало ≥60). `make -C libc` (fast+safe) — чисто.
- **Верификация в MAME** (`tests/kbdraw`, полный цикл open→держать→
отпустить→ESC-выход→close): `KBD_LEFT` (0x16B, расширенный код
E0 6B) — down на нажатие, up на отпускание, ТОЧНО совпало с
константой из `<kbd_raw.h>`; `KBD_ESC` (0x76, обычный код) —
корректно закрыл raw-канал и вернул DSS (`IM` вернулся в 1).
Побочно найдено и задокументировано в `docs/libc-reference.md`
(`<kbd_raw.h>`): MAME-мостовой `press_key` дёргает ОБЕ клавиатуры
(PC+ZX) одновременно и через ZX-путь давал паразitный незатухающий
бит — не относится к реальному сценарию (пользователь подтвердил:
матрица на Sprinter давно не используется), но означает, что
будущие MAME-тесты этой функции надо гонять через `:kbd:ms_naturl:*`
напрямую, не через удобный `press_key`. UP/DOWN/RIGHT/SPACE/SHIFT
константы — НЕ перепроверены поштучно (тот же общеизвестный
стандарт PS/2 Set 2, что и подтверждённые LEFT/ESC — проверить перед
использованием в PoC, если управление будет ощущаться неверно).
- На реальном железе — не проверено (только MAME).
---
## 3. Формат данных — что напрямую переносим из docs/*RESOURCE_FORMAT.md
- **Уровень** (`BLUETYPE`/`BLUESPEC`/`LINKLOC`/`LINKMAP`/`MAP`/`INFO`,
2304 байта, 24 экрана × 30 тайлов) — читаем один раз при загрузке уровня
в свою C-структуру (прямой memcpy дампа файла, поля читаем по офсетам
из `APPLEII_RESOURCE_FORMAT.md` §1). DOS `levels.dat` даёт то же самое
+1 байт в конце записи — отбросить.
- **Графика фона/спрайтов** — кодек сжатия DOS `.DAT` не восстановлен и
восстанавливать не будем: используем уже распакованные PNG из
`SDLPoP/data/{KID,GUARD,VPALACE,VDUNGEON,...}` (см.
`MSDOS_RESOURCE_FORMAT.md` §5, §7 — тот же контейнерный формат/нумерация,
просто другой релиз сборки данных). Измерено локально: кадры Кида —
~12×39 .. 30×42 px (P-режим, 4-бит палитра), фоновые тайлы подземелья —
32 px по ширине (10 колонок × 32 = 320 — сходится с шириной экрана), высота
тайла 20/60/62 px (неоднородные ряды пола/потолка/арок) — укладывается в
лимит спрайтового движка (кадр ≤ 64×64) без всяких изменений движка.
- **Звук** — `digisnd*.dat` (PC-звук 8-бит ~11 кГц) — конвертация в сырой
PCM и проигрывание через `cbl_open`/`cbl_push_*`; `ibm_snd*.dat` (PC-спикер
тройки «частота×2Б + длительность») — тривиальный бипер, не требует CBL.
MIDI-семейство (`midisnd`, `mt32snd`, `prince.dat`) — вне скоупа (нет
синтеза MIDI на платформе; не блокирует геймплей).
---
## 4. Стратегия фона — ПЕРЕСМОТРЕНО 2026-07-15: тайловый рендерер В РАНТАЙМЕ
**Было** (первая версия плана): офлайн-склейка каждой комнаты в готовую
растровую картинку 320×~193, `gfx_blit` целиком при входе — обоснование
было «ноль нового кода в libbgi». Пересчёт по факту наличия структурных
данных комнаты (§3.4 формата, `level.h`) показал: 16 уровней × 24 комнаты ×
~60-80 КБ/картинка — это **30+ МБ**, при том что одна и та же картинка
тайла (пол/стена/колонна) переиспользуется в десятках комнат — офлайн-
склейка печёт её заново в каждую копию.
**Стало**: тайлы — переиспользуемый набор картинок ОДИН на визуальный
стиль (не на комнату), структурные данные комнаты — компактные (60 байт:
30×foretable+30×backtable, все 16 уровней ≈ 37 КБ, см. `level.h`).
`room_draw()` (applications/PoP/poc/room.c) проходит 30 тайлов комнаты и
зовёт `gfx_blit` для каждого, читая картинку из таблицы по типу тайла
(`tile_images[TILE_TYPE]`). Итог: десятки-сотни КБ переиспользуемых
тайл-картинок на весь визуальный стиль + ~37 КБ структуры уровней —
вместо 30+ МБ.
**Почему это НЕ бьёт по бюджету кадра**: `room_draw()` зовётся ОДИН РАЗ
при входе в комнату (смена комнаты — не every-frame событие), не в
игровом цикле — это не `sprite_update`, тактовый бюджет кадра не
затронут.
Анимированные тайлы (факел, шипы, дверь-плита) по-прежнему рисуются как
отдельные `sprite_t` поверх фона — движок это уже умеет (Y-order/layers,
dirty-биты, heal против фона через ОЗУ-копию); `room_draw()` кладёт в
ОЗУ-копию именно статичную геометрию (пол/стены/колонны Фазы 1 — §5.2),
поверх неё heal спрайтов работает как обычно.
`toolchain/room_compose.py` (генерик-компоновщик тайлов в одну картинку,
§6.1) остаётся полезным ИНСТРУМЕНТОМ конвертации отдельных тайл-картинок
(PNG → getimage raw), просто теперь его выход — 32 маленьких файла
`tileNN.raw` (по одному на тип тайла), а не один большой файл на комнату;
сама раскладка/повторное использование по комнатам — в C-коде
(`room_draw`), не в офлайн-склейке.
---
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
**Объём**: одна комната (например Level 1, экран старта Кида), без
переходов между экранами, без стражников (стретч-цель, не обязательна).
**Что показываем**:
1. Кид на экране, с закреплённым офлайн-конвертированным набором кадров
(подмножество: idle, walk L/R, jump-начало/дуга/приземление, стоп-на-краю,
возможно повисание на краю) — атлас в W0-странице, по образцу `rpgwalk`.
2. Управление: держать влево/вправо — идёт; отпустил — тормозит/стоит;
нажатие вверх во время бега — прыжок вперёд (дуга по авторским таблицам
смещений, не по gravity-физике «с нуля» — см. §6). Здесь же проверяется
решение по §2 (реальный held-state).
3. Столкновения: пол/край экрана/провал — по факту чтения тайла из
`BLUETYPE` под ногами (без LINKLOC-триггеров пока).
4. Стабильный кадр 50 Гц через уже готовый `gfx_wait_vsync`/дабл-буфер
(без FPS-делителя — Кид анимируется каждый видеокадр, как в оригинале).
**Критерий успеха**: субъективно «прыжок ощущается как в PoP» (дистанция и
тайминг прыжка сверены с оригинальными таблицами, не подобраны на глаз —
см. §6), управление отзывчивое (не событийное с задержкой), сцена не мерцает
на стыке спрайт/фон.
**Не входит в PoC**: стражники/бой, звук, HUD/таймер, переходы между
комнатами, ловушки/триггеры, титры/меню, сохранения.
**Расположение**: `applications/PoP/poc/` (свой sprinter-cc проект + Python
конвертер ассетов, по структуре `examples/rpgwalk`).
### 5.1 Статус (2026-07-15) — первая итерация: управление + коллизия края
Сделано и проверено в MAME (`applications/PoP/poc/`, `make run`):
держать LEFT/RIGHT (`kbd_raw_down`, raw-канал из §2) — идёт непрерывно,
отпустил — стоит на месте (не событийно, реальный held-state);
столкновение с краями экрана (клип по `MINX`/`MAXX`); анимация
ходьбы/разворота лицом по направлению (`sprite_anim` пинг-понг);
дабл-буфер + `gfx_wait_vsync` — без видимого мерцания. Сборка —
`--memory huge` без `--bank` (§10, подтверждено рабочим).
**Важное отступление от плана (осознанно, не молча):** персонаж —
ВРЕМЕННАЯ заглушка (лицензированный спрайт-пак
`third_party/16x16-RPG-characters` через `tools/gen_kid_placeholder.py`,
тот же источник, что уже использует `examples/rpgwalk`), а НЕ
конвертированная графика оригинальной Prince of Persia. Причина:
исходный набор кадров Кида (`SDLPoP/data/KID`) — копирайт
Broderbund/Ubisoft; автоматический конвейер, который систематически
извлекает и переупаковывает его в новый формат, — это на практике
внутрипроектное решение, которое стоит принимать пользователю явно
для каждого шага, а не проводить асинхронно агентом без лишнего
подтверждения. Сама графика — не то, что проверяет PoC (§5 явно:
цель — ощущение управления/коллизий, не визуальная точность). Замена
на настоящую графику Кида — отдельный шаг, на усмотрение пользователя.
**Ещё не сделано** (следующие итерации §5): авторские таблицы
смещений кадров (§6 — движение при ходьбе линейное, px/кадр),
реальный уровень/фон по `BLUETYPE`/`LEVEL1` (сейчас — плейсхолдер:
плоский пол на весь экран, без ямы/выступа), `kbd_mod_state`/
Shift-бег не подключены к игровому циклу (обёртка готова с Фазы A).
**Прыжок/присед добавлены и ПРОВЕРЕНЫ (2026-07-15)**: состояние
`jumping`/`jump_t`/`crouching`, своя приблизительная дуга прыжка
(`jump_height[]`, 40 кадров) — не авторская таблица, см. §6.1.
HUD-текст статуса (нет отдельной позы).
Живое тестирование пользователем нашло реальный баг: держа UP чуть
дольше 0.8 с (длительность дуги), получали ДВА прыжка подряд — код
проверял `kbd_raw_down(KBD_UP)` как уровень (держится, пока клавиша
физически зажата), а не как фронт нажатия, поэтому в момент
приземления «UP всё ещё зажат» тут же триггерил новый прыжок.
Исправлено edge-detect'ом (`up_prev` — предыдущее состояние UP,
триггер только на переход 0→1). Проверено брейкпоинтом в отладчике
MAME на адресе входа в код прыжка: за одно длинное удержание UP
брейкпоинт срабатывает РОВНО ОДИН РАЗ — фикс подтверждён на уровне
кода, не только «на глаз».
Побочный урок (см. `docs/libc-reference.md` `<kbd_raw.h>`): моя
более ранняя попытка проверить UP/DOWN/RIGHT по скриншотам после
`press_key` ошибочно решила, что скрипт их не нажимает вообще —
на самом деле нажимает исправно, просто скриншот ловил случайный
момент дуги. Брейкпоинт/watchpoint на конкретный адрес кода —
надёжнее скриншота для таких проверок.
---
## 6. Модель движения: авторские таблицы кадров, не физика с нуля
Оригинальный движок PoP не считает прыжок как непрерывную физику
(gravity/velocity каждый тик) — движение персонажа задано таблицами кадров
анимации, где у части кадров зашито фиксированное смещение (dx, dy) для
ЭТОГО конкретного кадра последовательности (структура видна и в
исходниках Apple II — `SEQTABLE.S`/`MOVER.S`, и в SDLPoP `seg003.c`/`seq*`
таблицах). Практическое следствие для нашего движка:
- **Не использовать** `sprite_anim`/`sprite_moveto` для основного
персонажа как есть (они лианейно тянут по таймеру/тянут к линейной
цели) — вместо этого приложение само на каждый логический тик:
переключает кадр (`sprite_frame`, атлас как лента поз, не «прогрессия
первый..последний» автоматом) и одновременно применяет dx,dy ЭТОГО
кадра к позиции (`sprite_move`).
- Готовая автоматика движка (`sprite_anim`/`sprite_moveto`/tween,
Y-сортировка) остаётся полезной для декоративных/фоновых элементов
(факелы, патрулирующий стражник вне боя — почти один в один паттерн
`rpgwalk`).
- Источник таблиц смещений: переснять из `Prince-of-Persia-Apple-II/01 POP
Source/Source/{MOVER.S,SEQTABLE.S,FRAMEADV.S}` и/или
`SDLPoP/src/seq*.c` — задача Фазы 1 полной реализации (§7), не PoC
(для PoC можно взять урезанный набор смещений вручную по количеству
пикселей на кадр, посчитанному по видео/скриншотам оригинала, и уточнить
позже).
### 6.1 Инструмент конвертации кадров разного размера (`toolchain/png_strip.py`)
Кадры персонажа в оригинале — РАЗНОГО размера каждый (bbox зависит от
позы; `sprite_t` нашего движка (`libbgi/include/sprite.h`) хранит ОДИН
фиксированный w/h на весь спрайт и рисует от угла, без per-frame
смещения — в отличие от оригинала, где на каждый кадр было своё XCO/YCO
(`APPLEII_RESOURCE_FORMAT.md` §2.2). `toolchain/png_strip.py` (генерик,
не завязан на PoP — принимает произвольный список PNG) закрывает это
ПАДДИНГОМ: канвас = макс. w/h среди кадров ленты, якорь по умолчанию
bottom-center («ноги на месте»), остальное — прозрачность.
**Компромисс, не полноценное решение**: один сильно выбивающийся по
размеру кадр в ленте раздувает канвас (и память) ВСЕХ кадров этой же
ленты. Смягчается группировкой по похожим размерам в отдельные атласы
(не одна лента на все позы актора — так уже сделано для ходьбы отдельно
от прыжка).
**Полноценное решение (кандидат в будущее расширение библиотеки, НЕ
делать без предложения и подтверждения пользователя)**: per-frame
смещение в `sprite_t` (аналог XCO/YCO оригинала) — тогда паддинг
не нужен вообще, экономия памяти по полной. Делать только если память
станет РЕАЛЬНОЙ проблемой (не гипотетической) — тогда предложить как
отдельную правку `sprite.h`/движка. Подробности компромисса —
memory/png_strip_padding_tradeoff.
---
## 7. Полноценное приложение — фазы (после PoC)
Порядок — по риску и зависимостям, не по геймплейной важности.
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
подтверждения пользователем).
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
не обязательно разворачивать в C-struct с указателями).
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
хватит текущего лимита — см. риск в §8).
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
спереди/сзади».
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
§3), толстый стражник/визирь (общая база анимации с визирем).
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
геймплейно не критично.
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
кадра по методике `sprite_engine_perf`/`sprite-api-design.md` §9д на самых
насыщенных экранах (несколько стражников + ловушки одновременно —
проверить лимит ~21 спрайт/кадр и Y-sort лимит 32); при необходимости —
банкинг (`--memory big/huge`) для кода/уровня, если размер вылезет за
tiny/small.
---
## 8. Риски, требующие спайка/артефакта до架构 решений
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
несколько анимированных ловушек может приблизиться к лимиту
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
уровням (сколько объектов одновременно активно в худшем экране).
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
атласов на актора с переключением по фазе действия (стоять/идти отдельно
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
атласы — оценить фактический байтовый вес конвертированных кадров Кида
прежде чем проектировать.
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
Sprinter; если оригинал считался на другой частоте — потребуется
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
визуально быстрее/медленнее эталона.
---
## 10. Режим памяти сборки
Пользователь предложил `huge` (горячий код в W1, данные в W2, редко
вызываемая логика — банками в W3) как целевой режим. Согласен, с уточнением
по срокам принятия решения.
**`huge` — правильная цель для ПОЛНОГО приложения**, но не то, с чего надо
стартовать:
- Layout `huge` (см. `memory_modes_implemented`): CODE_LOC=0x4100 (W1),
DATA_LOC=0x8000 (W2), банки — W3 (порт 0xE2), `crt0_banked` +
автодетект W2 (как `small`). Состояние приложения (структуры Кида,
уровня, массив `sprite_t`) остаётся в обычном W2-heap ДАЖЕ если код,
который его трогает, забанкован — `malloc` из банка возвращает
W2-указатель (`bank_local_data_pattern`), так что данные не привязаны к
конкретному банку.
- Оверхед `__banked`-вызова (trampoline: +3 байта на стеке между ret и
аргументами, виртуальный 24-битный адрес, см. `sdcc_banking`) — фиксированная
небольшая цена ЗА ВЫЗОВ, не за такт. Это не страшно для функций, которые
вызываются РЕДКО за кадр (AI одного стражника, диалог, переход между
комнатами) — страшно было бы забанковать что-то, что дёргается ВНУТРИ
горячего цикла отрисовки (там уже и так основной бюджет уходит на
`sprite_update`/блиты — см. `sprite_engine_perf`, ~19.5К тактов/спрайт).
Правило простое: **не банковать код на пути "раз в кадр на объект",
банковать код на пути "раз в кадр на комнату/раз в переход/раз в
редкое событие"**: логика ИИ стражника целиком, диалоги/катсцены, меню/
титры/выбор уровня, парсинг уровня при входе в комнату, сериализация
сохранений — хорошие кандидаты в банки; тик Кида, чтение столкновений,
вызов `sprite_update`/`gfx_wait_vsync`, обработка ввода — должны остаться
небанкованными (W1/W2).
- Гранулярность банкования — целый файл (`--bank N=FILE.c`), это уже
системный паттерн проекта (тот же принцип, что и «1 файл = 1 юнит DCE» в
libc) — значит выгодно с САМОГО начала Фазы 1 (не задним числом) резать
исходники приложения по границе «горячее/холодное» файл-в-файл: например
`kid_tick.c`/`collision.c`/`room.c`/`input.c` — неизменно вне банков;
`guard_ai_*.c`/`dialogue.c`/`menu.c`/`levelload.c`/`combat.c` — кандидаты
под `--bank`. Тогда переход на `huge` позже — это правка Makefile/
sprinter-cc-вызова (`--memory huge --bank N=file.c ...`), а не рефакторинг
логики.
**Уточнение (проверено в `bin/sprinter-cc`, строки ~342-350): можно сразу
собирать PoC на `--memory huge` без единого `--bank`.** Скрипт сам
подставляет стаб `const unsigned char n_banks = 0;`, когда `--bank` не
передан ни один раз — `crt0_banked` линкуется и корректно пропускает цикл
загрузки банков при старте. Layout при этом byte-в-byte совпадает с тем,
что делает `crt0_small` для режима `small` (CODE 0x4100/W1, DATA 0x8000/W2,
автодетект W2) — разница только в том, что попутно линкуется сам
`bank.s` (таблица `_bank_pages` + trampoline-инфраструктура), это
незначительный довесок к размеру, не к рантайм-цене. Значит **PoC можно
сразу собирать вызовом `sprinter-cc --memory huge` без `--bank`-флагов** —
и когда в полном приложении появятся первые «холодные» файлы, переход на
банкование — это просто добавление `--bank N=file.c`, без смены
`--memory`/адресов/crt0. Сборочная конфигурация не потребует миграции
между PoC и полным приложением.
Единственное, что стоит сделать уже в Фазе 1 полного приложения (не в
PoC) — планировать структуру исходников с расчётом на будущий файл-в-файл
сплит под банки (см. выше), раз гранулярность банкования — целый файл.
---
## 9. Что нужно от пользователя, прежде чем двигаться дальше
- Подтверждение направления по §2 (какой из трёх вариантов held-state
клавиатуры пробовать первым, или сначала спайк-эксперимент в MAME).
- Подтверждение объёма PoC (§5) — устраивает ли «одна комната без
стражников», или сразу закладывать хотя бы одного патрулирующего
стражника (это не архитектурно сложнее — Y-order и tween уже есть,
просто больше конвертации ассетов).
+89
View File
@@ -0,0 +1,89 @@
# Форматы ресурсов Prince of Persia — сводка
**Каноническая спецификация форматов**`POP-DAT-FormatSpecifications.pdf`
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
*«Prince of Persia — Specifications of File Formats»*, Princed Development Team,
2008. Это первоисточник формата `DAT v1.0` (контейнер, индекс, чек-сумма,
кодеки RLE/LZG, палитры, уровни, звук), на котором построены и SDLPoP, и
Princed Resources. Документы ниже — наши практические заметки/сверки; при
расхождении источником истины считать спецификацию.
Цель этих документов — подготовить почву для будущего порта Prince of Persia
на ZX Sprinter, разобрав, как устроены ресурсы игры в двух доступных нам
версиях:
- [`APPLEII_RESOURCE_FORMAT.md`](./APPLEII_RESOURCE_FORMAT.md) — формат
уровней и графики по официально опубликованным исходникам 1989 года
(6502-ассемблер). Уверенность высокая везде — восстановлено прямым чтением
кода движка, а не догадками.
- [`MSDOS_RESOURCE_FORMAT.md`](./MSDOS_RESOURCE_FORMAT.md) — формат `.DAT`
ресурсов DOS-версии (исходников нет). Восстановлено эмпирически (разбор
байтов + перепроверка скриптами) и сверено с документацией открытых
сторонних инструментов (SDLPoP, Princed Resources).
## Главный вывод
**Формат уровня практически идентичен в обеих версиях**: Apple II `LEVELn`
занимает ровно 2304 байта (структура `blueprnt` — тайлы, связи
плит/дверей, граф экранов, метаданные старта Кида/стражников), а запись
уровня в DOS `levels.dat` занимает 2305 байт с байтовыми значениями тайлов
того же диапазона. То есть Джордан Мехнер перенёс формат карты уровня в
DOS-порт практически без изменений (+1 байт, вероятно контрольная сумма от
DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`BLUESPEC`/`LINKLOC`/
`LINKMAP`/`MAP`/`INFO`, задокументированную по Apple II исходникам, можно
применять напрямую и к DOS `levels.dat`.
Формат же **графики отличается принципиально**: на Apple II это простой
несжатый rowbyte-формат hi-res экрана с плоской таблицей указателей; в DOS —
контейнер с оглавлением ресурсов (id/size/offset), с отдельными вариантами
под CGA/EGA/VGA — точный кодек пикселей внутри сырого `.DAT`-чанка не
восстановлен ни для той, ни для другой версии до конца. **Но для DOS-графики
это не блокирует работу**: в репозитории github.com/NagyD/SDLPoP (папка
`data/`) уже лежат готовые распакованные PNG для каждого спрайта/фона
(включая VGA-256-цветный вариант `VPALACE`/`VDUNGEON` — то, что нужно под
320×256×256 Sprinter), см. §5 `MSDOS_RESOURCE_FORMAT.md`. Это другой
релиз/сборка данных, чем наш локальный `MSDOS/` (некоторые звуковые `.dat`
отличаются по размеру), но нумерация ресурсов и формат контейнера — те же,
что подтверждено побайтовой сверкой уровня `res2001.bin`.
## Общий контейнерный формат DOS `.DAT` (кратко)
```
[0x00] u32 LE tableOffset — смещение начала таблицы оглавления
[0x04] u16 LE tableSize — размер таблицы оглавления
[0x06..tableOffset) — данные ресурсов (конкатенация чанков)
[tableOffset..tableOffset+tableSize)
— массив записей по 8 байт:
u16 size, u16 id, u16 offset(абсолютный), u16 reserved(=0)
```
Инвариант `tableOffset + tableSize == размер файла` подтверждён на всех 28
`.dat`-файлах в `MSDOS/`, и независимо — именованием файлов `res<id>.*` в
`data/` репозитория SDLPoP.
## Готовые ассеты для порта (важно для практической работы)
`github.com/NagyD/SDLPoP/tree/master/data` содержит не только код движка, но
и сами ресурсы игры — как сырые `.DAT`, так и распакованные поштучно файлы
(`res<id>.png` для спрайтов/фонов, `res<id>.pal` для палитр, `res<id>.bin`
для уровней). Для арт-ассетов (в т.ч. нужного полноцветного VGA-варианта
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
декодер сжатия пикселей DOS `.DAT`.
## Что дальше (не сделано в этом заходе)
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
"как есть", а не ассеты из SDLPoP `data/`) — сверка с исходником SDLPoP,
`src/seg009.c`.
2. Семантика служебных полей `digisnd*.dat`/`ibm_snd*.dat` перед сырыми
сэмплами/нотами (частично прояснено документацией Princed Resources —
PC speaker: 1 байт заголовка + повторяющиеся тройки байт "2 байта частоты
+ 1 байт длительности"; WAV: 8 бит, моно, unsigned, 11025 Гц).
3. Назначение бит `secmask` в `BLUETYPE` (Apple II) и служебного блока
`id=2000` в начале DOS `levels.dat` (16 байт в нашей копии, но 2305 байт
в версии SDLPoP — расхождение между релизами, не разобрано).
4. Оценка, какие видеорежимы/цветовые палитры ZX Sprinter реалистично
покрывают исходную графику (CGA/EGA/VGA варианты в DOS-ресурсах против
hi-res Apple II) — отдельная архитектурная задача порта, не формат
ресурсов как таковой.
+112
View File
@@ -0,0 +1,112 @@
# План: порт `clip_char()` — обрезка спрайта персонажа
Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8»
(Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке).
## Симптом
room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт
Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии
пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке.
## Корень
Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из
`add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ
`set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД
`add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и
прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только
внутри него. Мы рисуем без клипа вообще.
## Что делает оригинал (дословно)
`reset_obj_clip()``left=0, top=0, right=320, bottom=192`.
Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый
`get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена
ИЛИ пол»):
```
T_L = get_tile(room, char_col_left, char_top_row)
T_R = get_tile(room, char_col_right, char_top_row)
if (T_L — стена или пол) &&
( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */
|| (T_R — стена или пол) )
{
clip_row = Char.curr_row + 1;
clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y))
obj_clip_top = char_top_y = clip_y;
}
```
Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой
пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»).
Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО.
Метрики из `set_char_collision` (seg006:0723):
- `char_x_left = obj_x/2 + 58``-= char_width_half`, если смотрит вправо)
- `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2`
- `char_top_y = obj_y - h + 1`; если `>= 192``0`
- `char_top_row = y_to_row_mod4(char_top_y)`
- `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`,
`char_col_right = MIN(get_tile_div_mod(char_x_right), 9)`
Второй блок `clip_char``obj_clip_right` (doortop / стена / зеркало для
виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228
(выход в дверь уровня). **Фаза 2**, не в этом заходе.
## Наши точки касания
- `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит
через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает
прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`.
- `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`,
`get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже
есть в `check_spike_below()` (строка ~1198) — брать за образец.
- `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0`
пропускает `sy` верхних строк колонки), но произвольной верхней границы нет.
## Шаги
1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`».
Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник
`+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`.
Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро).
2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w,
uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y
(0 = клипа нет). Экспорт в `pop_map.h`.
3. **pop_kid.c**: в `kid_draw()` после расчёта `top` —
`ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`,
рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`;
в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее
и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает
`reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять).
4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`,
`make size-check`; вывести свободное место в W2/W3 (порог 512 Б).
5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после
пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск.
Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`).
## Риски / что проверить отдельно
- `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт 1 для
полосы у потолка; `get_tile(row 1)` уже умеет ряд 2 верхнего соседа.
- Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип
прыгнет.
- `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в
обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой
должно отсекать).
- Клип меняет прямоугольник heal → возможен «хвост» на второй странице
дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер).
## Дальше (Фаза 2, отдельно)
`obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при
взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по
колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но
без тестовой сцены проверять нечем.
См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`,
`pop_check_sdlpop_first`, `gfx_blit_noclip_fast`.
@@ -0,0 +1,69 @@
# Double buffer (два экрана + флип) — план
Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей)
и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком
слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не
промежуточные состояния heal-рендера). **Тумблер обязателен**
однобуферный режим удобнее для отладки багов рисования.
## Что уже готово (libbgi — трогать НЕ нужно)
- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает
`_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы),
`gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS).
- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов.
- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync();
set_visible_page(hidden);`
- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры
(page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ.
## Текущая модель рендера roomtest (однобуфер)
`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты
рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT).
Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()`
(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick`
→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее
бланка, луч ловит промежуток.
## Работа на стороне PoP
1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1
(теневая копия одна — общая, из неё heal'ит любая страница).
2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync.
Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync —
plane-параметр).
3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в
массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)`
восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем
через страницу). То же для fore/пол-оверлея, если они рисуют вне
футпринта Кида.
4. **Ping-pong в цикле**:
```
uint8_t back = dbuf ? (front ^ 1) : 0;
gfx_set_draw_page(back);
kid_heal(back); kid_tick; phys; kid_draw; fore;
gfx_wait_vsync();
if (dbuf) { gfx_set_visible_page(back); front = back; }
```
5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] —
бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или
compile-флагом.
6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед
свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным
счётом кадров, чтобы скорость игры не изменилась.
## Порядок
- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page
heal (проверить, что флип работает, фон корректен на обеих).
- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида.
- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет).
- D4: пейсинг (fps_div) — вернуть исходную скорость.
## Связанные
Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]];
heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors
[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в
обе страницы по той же дисциплине.
+264
View File
@@ -0,0 +1,264 @@
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст).
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
перед кодингом читать соответствующий код seg*.c, не гадать.
---
## 0. КОНТЕКСТ: текущее состояние `applications/PoP/roomtest` (что уже готово)
roomtest — живой прототип порта PoP: комната 1 уровня 1 живой композицией
тайлов + Kid (анимация/управление/коллизия/падение/зацеп/переходы). Собрать:
`cd applications/PoP/roomtest && make`. Тест в MAME: см. memory
`mame_mcp_bridge`/`mame_hdd_test_disk` (канонический цикл: `make` → пересобрать
`mame/v306/IMG/test_hdd.chd` через `toolchain/make_hdd.sh` со всеми ассетами →
рестарт `run_bridge.sh``resume` → ~13с бут → `type_string("d:{ENTER}roomtest.exe{ENTER}")`).
Отладка: клавиши `1`=freeze / `2`=resume в roomtest; MCP-мост `mame-z80`
(read_logical_memory, set_breakpoint, disassemble); адреса символов —
`.sprinter-cc-roomtest/roomtest.map` (сдвигаются при пересборке!).
### Модули (все в `applications/PoP/roomtest/`)
- `roomtest.c` — главный цикл (дабл-буфер 2 стр.), `enter_room(room)`,
обработчики переходов. File-static рабочие массивы (W2):
`room_fg[30]`, `room_bg[30]`, `lcol_fg/lcol_bg[3]`, `rcol_fg/rcol_bg[3]`,
`below_fg[10]`, `cur_room`.
- `pop_level.c/.h`**уровень из файла** (Фаза L1):
- `pop_level_load("res2001.bin")` — читает сырой blueprnt в EMM-страницу
(данные с offset `0x100`, ISR-стаб как атлас).
- `pop_room_load(room, fg,bg, lcol_fg,lcol_bg, rcol_fg,rcol_bg, below_fg)`
извлекает комнату (fg маскирован `&0x1F`, bg raw) + срезы соседей:
leftcol=col9 левого соседа, rightcol=col0 правого, belowrow=row0 нижнего.
- `pop_room_link(room, side)` — связь (side 0=L,1=R,2=U,3=D; 0=нет).
- `pop_level_start_room/pos/dir()`.
- `pop_bg.c/.h` — отрисовка тайлов (порт seg008 draw_tile), fore-окклюзия над
Kid (`pop_fore_over_kid`, порт set_char_collision+redraw_at_char/char2),
**loose-полы** (shake/bake/mob). `draw_tile` — статическая, знает
`draw_gate_back` (грань гейта из левой комнаты).
- `pop_kid.c/.h` — анимация Kid (интерпретатор seqtbl `play_seq`, порт seg006),
`Kid` struct (frame,x,y,dir,curr_col,curr_row,action,fall_x,fall_y,repeat,
curr_seq); `knock`-флаг; `kid_cur_dx/flags`, `kid_fp_*` (футпринт).
- `pop_ctrl.c/.h` — ввод (порт seg005 control) через `<kbd_raw.h>`.
- `pop_map.c/.h` — коллизия/физика (порт seg005/006). Ключевое:
- `pop_map_set(fg)` — карта текущей комнаты.
- `pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg)` — связи + кромки швов для
коллизии (`get_tile(col=-1)`=lcol, `get_tile(col=10)`=rcol; порт
find_room_of_tile).
- `pop_phys_tick()` — кадр физики (fall/land/wall/knock/leave).
- Переходы: `pop_fell_out` (вниз, y>=211), `pop_leave_dir` (1=left,2=right,
x∓140).
- **loose-состояние**: `pop_loose_modif[30]` (публично, читает pop_bg),
`loose_bake[30]`, `loose_rest[30]` (static); `pop_loose_tick()`,
`pop_loose_reset()` (сброс при смене комнаты).
### Что сделано по фазам
- **L1** — данные уровня из файла (room1_data.h удалён). Коммит `21f978d`.
- **L2** — переход в комнату снизу (провал/спуск), фиксы окклюзии. Коммит `7f3e32d`.
- **L3** — переходы вбок (право+лево) через швы. **Не закоммичено** на момент
написания (вместе с этим планом). **L3-вверх (climb-up в комнату сверху) —
НЕ сделано.**
- **Loose-полы** (тряска knock / падение mob+окклюзия) — коммит `8b30dc2`.
### Известные ОГРАНИЧЕНИЯ (важно для этого плана)
1. **Нет персистентности тайлов**: `enter_room``pop_room_load` каждый раз
перезагружает ИСХОДНЫЕ тайлы из level-страницы. Изменения (упавший loose,
открытый гейт) при повторном входе ТЕРЯЮТСЯ. Для кнопок/гейтов это
блокер (см. P0).
2. **Нет HP/смерти** Кида (нужно для пик).
3. Loose-механика — частный случай trob (нужно обобщить).
---
## 1. ДАННЫЕ УРОВНЯ (формат, offsets, объекты)
Сырой `res2001.bin` (2305 Б) = blueprnt DAT 1.0 (Table 6 в
`POP-DAT-FormatSpecifications.txt`). Читается в EMM-страницу с offset `0x100`.
Тайл-код = байт `& 0x1F`; верхние биты (модификатор BLUETYPE) сейчас отброшены.
| Блок | Offset | Размер |
|------|--------|--------|
| foretable (fg) | 0 | 720 (24 комн × 30) |
| backtable (bg=modifier) | 720 | 720 |
| **LINKLOC** (doorlink1) | **1440** | 256 |
| **LINKMAP** (doorlink2) | **1696** | 256 |
| links (roomlinks) | 1952 | 96 (24×{L,R,U,D}) |
| start_position | 2112 | 3 (room,pos,dir) |
Тайл-коды: `0x00`empty `0x01`floor `0x02`**SPIKE** `0x03`pillar `0x04`**GATE**
`0x06`**DROP-кнопка(closer)** `0x0B`loose `0x0F`**RAISE-кнопка(opener)**
`0x10`lvldoor-L `0x11`lvldoor-R `0x13`torch `0x14`wall.
### Объекты уровня 1 (по комнатам)
```
room 5: DROP(0,2)m11 RAISE(0,4)m9 GATE(0,5)m2 RAISE(0,6)m8 GATE(0,9)m1
room 6: RAISE(0,2)m7 SPIKE(2,3) SPIKE(2,4) <-- тестовая
room 7: RAISE(0,2)m6 GATE(0,9)m2
room 8: RAISE(0,6)m5 GATE(0,9)m2 RAISE(1,7)m4
room 9: RAISE(0,0)m3 (+ lvldoor(1,3)/(1,4) — выход на level2, отложено)
room12: RAISE(0,3)m2 GATE(0,9)m2 SPIKE(2,4)
room10/13/14/16/19/24: только SPIKE
room20: DROP(1,4)m1 RAISE(1,7)m0
```
(m = modifier тайла = ИНДЕКС в LINKLOC/LINKMAP.)
### Room6 (тестовая) — раскладка
```
fg row0: 13 01 0F 00 03 01 01 13 01 03 (0,2)=RAISE-кнопка, (0,0)/(0,7)=torch
fg row1: 14 14 14 00 14 14 14 14 14 14 (1,3)=empty
fg row2: 14 14 14 02 02 14 14 14 14 14 (2,3)(2,4)=SPIKE
links: L=8 R=2 U=5 D=0
```
- **Шахта пик**: col3 (row0=empty, row1=empty, row2=spike) + col4 (spike).
- **Гейт, видимый у ЛЕВОЙ кромки room6, — это гейт room8 (0,9)**, отрисованный
в col0 room6 через левый шов (L=8, leftcol=col9 room8). В room6 гейта НЕТ.
### Связь кнопка→цель (декод LINKLOC/LINKMAP), проверено:
- `get_doorlink_tile(i) = d1[i] & 0x1F`
- `get_doorlink_next(i) = !(d1[i] & 0x80)` (0 = конец цепочки)
- `get_doorlink_room(i) = ((d1[i]&0x60)>>5) + ((d2[i]&0xE0)>>3)`
- `get_doorlink_timer(i) = d2[i] & 0x1F`
- где `d1`=LINKLOC@1440, `d2`=LINKMAP@1696. Цепочка: idx++ пока next.
**Кнопка room6 (0,2) mod=7 → цель: room8 tile(0,9) = ГЕЙТ.** Подтверждено
в SDLPoP-скринах: нажатие кнопки поднимает решётку у левой кромки room6.
Связь КРОСС-КОМНАТНАЯ (кнопка в room6, гейт в room8) и задаётся таблицей,
а НЕ позицией. Кнопка может открывать НЕСКОЛЬКО гейтов в разных комнатах.
---
## 2. МЕХАНИКА SDLPoP (точные ссылки)
### 2.1 Trob-система (анимируемые тайлы)
- `add_trob(room,tilepos,type)` seg007:0A5A — в список анимируемых.
- Каждый кадр `redraw_needed_tiles`/`process_trobs` продвигает; диспетч по
типу тайла → `animate_button/animate_door/animate_spike/animate_loose`
(seg007:0033+ таблица `animate_*`).
- Состояние тайла хранится в `curr_room_modif[tilepos]` (per-room modifier).
- У нас есть частный случай для loose (`pop_loose_tick`+`pop_loose_modif[30]`).
### 2.2 Пики (spike)
- **Триггер выдвижения** — `check_spike_below()` seg006:1199 (зовётся в
физике Кида каждый кадр): для каждой колонки футпринта Кида
(`get_tile_div_mod_m7(char_x_left)`..`char_x_right`) идёт ВНИЗ от
`Char.curr_row` через НЕ-floor тайлы; если встретил `tiles_2_spike`
`start_anim_spike(room,tilepos)`. → Кид у края (0,2) правым краём задевает
col3 → скан вниз col3 (empty/empty/spike) → пики вылезают.
- `start_anim_spike` seg007:596: если `modif<=0`: `modif==0` → add_trob(type1)
+ звук; `modif<0` (кроме 0xFF disabled) → `modif=0x8F`.
- `animate_spike` seg007:317: автомат по modif — выдвиг `++modif` (1..4; на 5 →
`0x8F`; на 9 → 0, trob кончился); убирание `& 0x80``--modif` (на 0 → `=6`).
`0xFF` = disabled (не двигать).
- `is_spike_harmful` seg007:1178: modif `0/-1`→0 (безопасно); `<0`→1;
`1..4`→2; `>=5`→0.
- **Смерть**: `check_spiked` seg006:0968 — если тайл под Кидом = spike И harmful
И кадр бега (7..14) / старта прыжка (34..39) с harmful>=2, ИЛИ кадр приземления
(43/26) с harmful!=0 → `spiked()`. Падение на пики — отдельный путь (см.
`is_dead` seg006:1907, frame_177_spiked). Осторожный ШАГ по невыдвинутым — ок.
### 2.3 Кнопки
- `trigger_button(playsound, button_type, modifier)` seg007:0C53: modifier =
индекс LINKLOC. `link_timer = get_doorlink_timer(mod)`; если `!=0x1F`
(не заклинено): `set_doorlink_timer(mod,5)`; если был `<2``add_trob`
(кнопка нажимается) + звук; затем `do_trigger_list(mod, button_type)`.
- `do_trigger_list` seg007:09E5: идёт по цепочке LINKLOC от idx, для каждой
цели `trigger_1(target_type,room,tilepos,button_type)` → если >=0
`add_trob(room,tilepos,result)`.
- `animate_button` seg007:0D3A: `timer=get_doorlink_timer(mod)-1`;
`set_doorlink_timer(mod,timer)`; `timer<2` → кнопка отжимается.
- Когда Кид ВСТАЁТ на кнопку: `check_press`-путь seg006 (opener → trigger_button,
closer → тоже; если Кид мёртв — `died_on_button`). RAISE=`tiles_15_opener`
(0x0F), DROP=`tiles_6_closer` (0x06).
### 2.4 Гейты
- `trigger_1` seg007:0999 → для `tiles_4_gate``trigger_gate`.
- `trigger_gate(room,tilepos,button_type)` seg007:092C: modif = высота открытия.
opener: `0xFF`→игнор; `>=188`(открыт)→держать `238`; иначе `modif=(modif+3)&0xFC`,
return 1 (открывать). closer/иначе: `modif!=0` → return 3 (закрыть быстро).
- `animate_door` seg007:0522: анимация открытия/закрытия; `door_delta[]={-1,4,4}`,
`gate_close_speeds[]={0,0,0,20,40,60,80,100,120}`. Гейт медленно закрывается
после истечения таймера кнопки.
- Отрисовка: кадры гейта; у нас `draw_gate_back` в pop_bg (грань из левой комн.).
- **Проходимость**: Кид блокируется недостаточно открытым гейтом (коллизия
как стена, порог по высоте открытия); проходит при `modif` открытом.
---
## 3. Поправки к описанию пользователя (что важно)
1. Гейт НЕ в room6 (0,0) — он в **room8 (0,9)**, виден через левый шов; связь
кросс-комнатная (по таблице LINKLOC, не по позиции).
2. Кнопка может открывать несколько гейтов в разных комнатах.
3. Пики выдвигаются по `check_spike_below` (Кид над колонкой с пиками),
имеют состояния (не всегда смертельны), убираются со временем.
4. **Кросс-комнатное состояние тайлов ДОЛЖНО ПЕРСИСТИТЬ** — блокер (см. P0).
5. HP/смерть — новая подсистема.
6. Кнопка сама анимируется (нажата/отжата).
---
## 4. ПЛАН РЕАЛИЗАЦИИ (фазы)
### P0 — Персистентное per-room modifier-состояние + trob-каркас (ПРЕРЕКВИЗИТ)
**Проблема:** нажатие кнопки в room6 меняет modif гейта room8 (не текущей
комнаты); при входе в room8 нужно отрисовать гейт в текущем состоянии. Плюс
это чинит «re-entry восстанавливает тайлы» (loose/гейты).
Дизайн (предложение — уточнить в реализации):
- Массив `room_modif[24][30]` (или lazy per-visited-room) — modifier каждого
тайла каждой комнаты. Инициализируется из bg уровня при первой загрузке
комнаты; далее ЖИВЁТ (не перезагружается). ~720 Б — влезает в W2 (или в
EMM-страницу уровня рядом с данными: остаётся >10КБ).
- `enter_room` берёт modif из `room_modif[room]`, а fg — из level-страницы
(fg почти не меняется; исключения — loose→empty, надо тоже персистить: либо
отдельный `room_fg_override`, либо флаг «loose упал»).
- Обобщить loose-trob: единый список trob (room,tilepos,type) + диспетчер
`animate_*` по коду тайла. Loose (`pop_loose_*`) — перевести на него.
- Кросс-комнатный trigger: `add_trob` в НЕ текущую комнату меняет
`room_modif[room][tilepos]`; анимация продвигается даже для невидимой комнаты
(в оригинале — да; можно упростить: для невидимой комнаты гейт сразу в
финальном состоянии, анимировать только при видимости — решить при реализации).
### Фаза S — Пики (самодостаточно; отладит HP/смерть)
Порядок:
1. `room_modif` для пик (из P0 или временно локально).
2. `check_spike_below` (порт seg006:1199) — в `pop_phys_tick`.
3. `start_anim_spike` + `animate_spike` (порт seg007) — состояние в modif.
4. `is_spike_harmful` + `check_spiked` (порт seg006:0968).
5. **HP/смерть**: ввести `hitp_curr` (старт напр. 3); `take_hp`; при пиках —
мгновенная смерть; seq смерти (`seq_22_crushed`/`frame_177_spiked`..185);
анимация смерти; респавн (kid_init на старте или чек-поинт).
6. Отрисовка: кадры выдвижения пик. В pop_bg есть `SPIKES_FRAM_RIGHT` — нужны
pop-out кадры (spikes_fram по modif) + fore над Кидом.
### Фаза B — Кнопки + гейты (нужен P0)
Порядок:
1. Доступ к LINKLOC/LINKMAP из pop_level (добавить геттеры doorlink1/2[i] с
маппингом W0 или скопировать таблицы в W2 при load — 512 Б).
2. `pop_map` детект «Кид встал на кнопку» (check_press-путь) → `trigger_button`.
3. `trigger_button` → таймер + `do_trigger_list` (обход цепочки) →
`trigger_gate` для целей → изменить `room_modif[целевой]`.
4. `animate_button` (кнопка отжимается) + `animate_door` (гейт откр/закр +
авто-закрытие).
5. Отрисовка гейта (кадры по modif) через ЛЕВЫЙ шов (гейт room8 в room6) +
при входе в room8. Расширить `draw_gate_back`/добавить `draw_gate`.
6. Коллизия: закрытый гейт = стена (порог по высоте открытия); открытый —
проход. Учесть кросс-комнатный гейт на шве (проход влево room6→room8).
**Порядок фаз:** P0 → S → B. S в основном независим (кроме HP/trob-каркаса),
но проще и отладит смерть/анимацию; B требует P0 (кросс-комнатное состояние).
---
## 5. Открытые вопросы / грабли
- Персистентность `fg` для loose (тайл→empty): решить в P0 (override-массив или
флаг), иначе упавший loose «вернётся».
- Анимация trob в НЕВИДИМОЙ комнате: упростить (финальное состояние сразу) или
портировать честно.
- Двоебуфер: любой транзиент (пики/гейт/кнопка) финализировать перерисовкой
«покоя» на ОБЕИХ страницах (урок из loose — см. memory `pop_loose_floors`).
- Дверь уровня (lvldoor room9) + переход на level 2 — ОТДЕЛЬНО, отложено.
- L3-**вверх** (climb-up в комнату сверху) — ещё не сделан; можно закрыть до
объектов или параллельно.
+143
View File
@@ -0,0 +1,143 @@
# Loose floors (проваливающиеся полы) — план порта
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose). ПЛАН, ещё не
реализовано. Тайл в комнате 1: `[2,6] = 0x0B = tiles_11_loose`.
## 1. Хранение состояния
- **Тип тайла**: `curr_room_tiles[tilepos] & 0x1F == 11` (tiles_11_loose).
Бит `0x20` = «solid» loose (авто-падающий вариант, ур.13 — от шага НЕ
падает). После падения тайл → `0` (tiles_0_empty).
- **Модификатор** `curr_room_modif[tilepos]` = состояние анимации:
- `0` — покой (обычный loose-пол);
- `0x80..0x83`**трясётся** (бит7); за ~4 кадра затухает обратно в 0;
- `1..11`**обратный отсчёт до падения** (на нём что-то стоит);
достигает `loose_floor_delay = 11` → падает.
## 2. Анимация тряски (shake) — когда включается
- **Триггер = do_knock** (seg007:0FE0): на ЖЁСТКОМ приземлении в кадрах
посадки играет `SEQ_KNOCK_DOWN` → взводит `knock``check_knock()`
`do_knock(room, curr_row (knock>0))`.
- `do_knock(room, row)`: по всем колонкам ряда — если тайл loose →
`loose_make_shake()`.
- `loose_make_shake()` (seg007:0FB4): если `modif==0` (и не ур.13) →
`modif = 0x80`, `add_trob(type 1)`.
- **Отсюда кейс пользователя**: Kid падает/приземляется на `[2,4]`
do_knock трясёт ВСЕ loose-тайлы ряда 2 → `[2,6]` трясётся. (Через
knock-смещение ряда может задеть и соседний ряд.)
- `animate_loose` (кадрово): `++modif`; при бите7 трясёт до `>=0x84`
сброс в 0, `trob.type=-1`. `loose_shake()` играет звук
(sound 20/21/22) по таблице `loose_sound[]`.
## 3. Анимация падения (fall) — когда включается
- **Триггер = make_loose_fall(1)** (seg007:0EF6), вызывается когда:
- Kid СТОИТ на loose-тайле — `check_press()` (seg006): кадр с
FRAME_NEEDS_FLOOR над loose → make_loose_fall(1);
- зацеп/подтягивание на loose (`check_grab`, `check_jump_up`);
- пробой сверху: кадр 79 (jumphang) над loose → make_loose_fall(1);
- авто-падающие (ур.13) — `make_loose_fall(-(prandom&0x0F))`.
- `make_loose_fall(modifier)`: если НЕ solid (`tiles & 0x20 == 0`) и
`(sbyte)modif <= 0``modif = modifier`, `add_trob(type 0)`.
- `animate_loose`: `++modif`; когда `modif >= 11` (loose_floor_delay) →
`remove_loose()` (тайл → empty) + `add_mob()` (спавн падающего куска).
## 4. Падающий кусок (mob)
- `add_mob()` кладёт `curmob` в `mobs[]` (до 14). `do_mobs()` каждый
кадр: `move_mob()` (гравитация, y растёт) + `check_loose_fall_on_kid()`
(урон Киду/страже, если попал).
- Приземление куска → тайл под ним `curr_room_tiles[...] = tiles_14_debris`
(seg007 move_mob:1053). Т.е. **loose(11) упал → сверху empty(0), снизу
debris(14)**.
## 5. Отрисовка по статусу
- Куски тайла: `loose_fram_left[]={41,69,41,70,70,41,41,41,70,70,70,0}`,
`loose_fram_right[]={42,71,...}`, `loose_fram_bottom[]={43,73,...}`
(env-спрайты, seg008:518/596/608).
- Индекс кадра = `get_loose_frame(modifier)` (seg008): `0` = ровный
(41/42/43); `1..10` = дрожащие варианты (69–74); при бите7/большой
задержке — низкие индексы.
- **До падения**: рисуем loose с `get_loose_frame(modif)` (0 = ровно,
иначе колеблется). **После**: сверху empty, снизу debris(14) — обычная
статическая отрисовка (у нас уже есть tile 0x0E/14 debris в tile_table).
## 6. Что нужно в нашем движке (сейчас НЕТ)
Наш `pop_bg` рисует комнату СТАТИЧЕСКИ один раз. Loose-полы требуют
**динамического тайлового слоя**:
1. **Массив модификаторов** `room_modif[30]` (у нас есть `bg[30]` — можно
переиспользовать/рядом) — состояние каждого тайла.
2. **Очередь trob** (список анимируемых тайлов) + `animate_loose` пер-кадр
→ перерисовка ТОЛЬКО изменившихся тайлов (как heal-прямоугольник Kid).
3. **make_loose_fall / do_knock / loose_make_shake** — триггеры (из
физики Kid: приземление→knock, стойка на loose→fall).
4. **mob-система** (падающий кусок): минимум 1–2 mob'а, гравитация,
приземление → debris. Урон Киду (`check_loose_fall_on_kid`) — можно
Фазой 2.
5. **Перерисовка тайла**: `draw_tile(row,col)` у нас уже умеет loose
(`code==11`, `loose_fram_*` в env) — нужно вызывать его выборочно с
текущим модификатором (сейчас draw_tile берёт статический bg).
**Порядок реализации (предложение):**
- L1: room_modif[] + выборочная перерисовка тайла по модификатору
(draw_loose с get_loose_frame) — статика→динамика одного тайла.
- L2: trob-очередь + animate_loose (тряска по do_knock на приземлении).
- L3: make_loose_fall (стойка на loose) + отсчёт + remove → empty.
- L4: mob (падающий кусок → debris снизу).
- L5: урон Киду от падающего куска.
## Конкретика из SDLPoP (сверено 2026-07-18, готово к реализации)
Таблицы (seg008.c), индекс = `get_loose_frame(modif)`:
- `loose_fram_left[] = {41,69,41,70,70,41,41,41,70,70,70,0}`
- `loose_fram_right[] = {42,71,42,72,72,42,42,42,72,72,72,0}`
- `loose_fram_bottom[]= {43,73,43,74,74,43,43,43,74,74,74,0}`
- `get_loose_frame(m)`: если `(m&0x80)` (или delay>11) → `m&=0x7F; if(m>10) return 1;``return m;`
- `y_loose_land[] = {2,65,128,191,254}` (mob), `loose_floor_delay = 11`.
Триггеры (call-sites):
- **make_loose_fall(modifier=1)** — из `check_press()` (seg006): когда Kid
СТОИТ на тайле (FRAME_NEEDS_FLOOR, action < hang_climb / turn / bumped) и
`get_tile_at_char()==11`; ИЛИ `frame==79` (прыжок вверх) и
`get_tile_above_char()==11` (пробой сверху). `tile_is_floor(11)==1`
Kid стоит на loose (start_fall НЕ зовётся). Тело:
`if(!(tile&0x20) && (sbyte)modif<=0){ modif=modifier; add_trob(type0); }`
- **do_knock(row)** — на ЖЁСТКОМ приземлении (SEQ_KNOCK_DOWN→check_knock,
seg003): по всем колонкам ряда `if(tile==11) loose_make_shake()`
(`if(modif==0){ modif=0x80; add_trob(type1); }`).
- **animate_loose** (каждый кадр, seg007:816): `++modif`; если `&0x80`
(тряска): `if(modif>=0x84){modif=0; trob=-1;}`; иначе (отсчёт):
`if(modif>=11){ remove_loose(tile→0); trob=-1; add_mob(); } else shake`.
## Интеграция в наш движок (roomtest) — подход
Наш движок ПЕКЁТ комнату один раз (двойной буфер: своя ОЗУ-копия на
страницу). Loose требует динамики + общего состояния pop_map↔pop_bg:
1. **Общая МУТАБЕЛЬНАЯ копия комнаты**: roomtest.c держит `uint8_t
room_fg[30]` (копия room1_fg) и передаёт ОДИН указатель и в
`pop_room_draw`, и в `pop_map_set` → мутации loose видны обоим.
(Сейчас g_fg — `const`; сделать неконстантным.)
2. **Состояние**: `uint8_t pop_loose_modif[30]` (0 / 0x80.. / 1..11).
3. **Модель** (pop_map): `check_press()` в `pop_phys_tick` (make_loose_fall
при стоянии на 11), `animate` каждый кадр.
4. **Перерисовка** (pop_bg, на back-странице каждый кадр):
- тряска/отсчёт (код всё ещё 11): `gfx_heal(tile rect)` (вернуть
печёный фон) + нарисовать loose-кадр (left/right/bottom по
get_loose_frame) банком SPRITE;
- падение (11→0): mutate `g_fg[pos]=0` + «запечь пустоту» на ОБЕИХ
страницах (bake-счётчик 2: чёрный bar + draw_tile empty банком NORMAL
на текущей странице 2 кадра подряд) → дальше heal показывает пусто.
5. **L4 mob**: падающий кусок → debris(14) снизу (y_loose_land); пока
отложено — упавший loose = пусто. **L5** урон — позже.
Риск: перерисовка динамического тайла в двойном буфере (per-page heal +
bake) — единственное тонкое место; остальное — прямой порт логики выше.
## Связанные
Триггеры завязаны на физику Kid ([[pop_hang_state]] check_press/check_grab,
приземление land/SEQ_KNOCK_DOWN). Отрисовка — [[pop_fore_layer]] /
[[pop_background_strategy]] (draw_tile уже знает loose_fram_*).
+68
View File
@@ -0,0 +1,68 @@
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
## Факты из SDLPoP (подтверждено чтением исходника)
- `Char.room` (реальная комната персонажа) ≠ `drawn_room` (отрисованная) —
штатное состояние.
- **Коллизия через ±140:** `xpos_in_drawn_room()` (seg004:0405) сдвигает
xpos на `±TILE_SIZEX*SCREEN_TILECOUNTX = ±140`, когда `curr_room` колонки
(`curr_row_coll_room[col]`) ≠ `drawn_room` (room_L/room_BL → 140,
room_R/room_BR → +140). Т.е. коллизия строится по РЕАЛЬНЫМ тайлам соседей.
- **Смена экрана:** `check_the_end()` (seg000:0FBD): `if (next_room!=0 &&
next_room!=drawn_room) { drawn_room=next_room; load_room_links; redraw }`.
`next_room` ставится в `exit_room()` (= `Char.room` ПОСЛЕ успешного
`leave_room`). Значит drawn_room следует за Char.room, но Char.room меняется
ТОЛЬКО при реальном пересечении шва (leave_room, seg002:0504) на «легальном»
кадре/действии (не turn/climb/standup).
- **Отрисовка левого соседа:** только `load_leftroom()` (col9 левого соседа в
левую кромку); правый сосед НЕ рисуется (изометрия). Окклюзия ворот на шве —
только левая (seg008:696).
- **Ceiling-полоса:** `draw_room` рисует доп. ряд из `room_A` (row2, draw_main_y
=-1). (Уже реализовано, баг #3.)
## Текущее состояние нашего движка (до #4)
`cur_room` (=drawn_room) ВСЕГДА == комната Kid. Шов подделан: Kid остаётся в
drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol` (коллизия ±1
кол) / `lcol_bg` (openness ворот). Уход из комнаты — `pop_leave_dir`/`enter_room`
МГНОВЕННО при пересечении порога `char_x`. Отсюда #4: экран переключается
раньше, чем в оригинале (Kid должен «отступить» за кромку, оставив старую
комнату).
## План (инкременты, каждый проверяется в MAME)
### S1. Данные + рендер-смещение Kid
- Ввести `kid_room` (реальная комната Kid) отдельно от `cur_room`(=drawn_room).
- `kid_x_offset()` = разница комнат: kid_room == left(drawn) → лог. x Kid 140
(рисуется за левой кромкой); right → +140; равны → 0. (порт
xpos_in_drawn_room).
- `kid_draw`/heal/fore используют смещение (Kid рисуется частично за кромкой).
- Проверка: Kid у шва рисуется со сдвигом, экран не дёргается.
### S2. Коллизия по kid_room
- Коллизионный контекст (`g_fg`/edges/`g_room`/modif в pop_map) следует за
`kid_room`, а не за drawn_room. Когда kid_room≠drawn_room — грузим
соседа как коллизионную комнату (curr_col 0..9 в кадре kid_room).
- Отрисовка (room_fg и т.п.) остаётся по drawn_room.
- Порт `curr_row_coll_room[]`/`xpos_in_drawn_room` можно упростить: держим
ОДИН коллизионный room (kid_room) + существующие снапшоты кромок для ±1 кол.
### S3. Отложенная смена drawn_room
- Уход (`check_leave`/`check_leave_below`): ставит `kid_room=сосед`,
репроецирует Kid (x∓140, col∓10) — но drawn_room НЕ меняет сразу.
- `check_the_end`-эквивалент в главном цикле: `if (kid_room != drawn_room &&
<условие коммита>) enter_room(kid_room)`. Условие коммита — по SDLPoP:
как только Char.room сменилась легальным leave (не bumped/turn). Для
«bumped назад за кромку» drawn_room остаётся (симптом #4).
- Проверка сценариев #4/#5 в MAME.
### S4. Полировка
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
## Связанные баги (bug_list.md)
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
колонны). Memory: `pop_seam_room_model`.
@@ -0,0 +1,335 @@
# roomtest — план оптимизации по размеру + переход на huge/banking
Статус: **план для отдельной сессии** (2026-07-21). Документ самодостаточный
(рассчитан на старт с пустого контекста). Цель — освободить место: сейчас
`applications/PoP/roomtest` в режиме `small` почти упёрся в потолок 32 КБ.
Правило проекта (`applications/PoP/CLAUDE.md`): механику/раскладку памяти
сверять с исходником и с memory (`sprinter_memory_modes`, `sdcc_banking`,
`bank_local_data_pattern`, `pop_banking_architecture`). Перед оптимизацией —
`make size-check`-подобный замер до/после (здесь — руками по `.map`).
---
## 0. Как мерить
- Сборка: `cd applications/PoP/roomtest && make roomtest.exe` (режим `small`,
`--gfx 256`). Карта символов — `.sprinter-cc-roomtest/roomtest.map`
(адреса сдвигаются при каждой пересборке!).
- Размеры областей — из `.map` (`_CODE`, `_DATA`, `_BSS`).
- Вклад модулей в `_CODE` — атрибуция диапазонов между символами по модулю
(скрипт-однострочник на python в истории; группировать символы `.map` по
3-й колонке-модулю и суммировать `addr[i+1]-addr[i]`).
- MAME-проверка после изменений раскладки ОБЯЗАТЕЛЬНА (режимы памяти —
типовой источник «молча ломается», см. `sprinter_memory_modes`).
## 1. ТЕКУЩЕЕ СОСТОЯНИЕ (замер 2026-07-21)
Режим `small` = единое пространство **W1+W2 = 0x4000..0xBFFF (32 КБ)**; CODE с
0x4100, DATA/BSS/heap цепляются ЗА CODE автоматически (`--data-loc 0` =
linker chains), стек — вверху W2.
| Область | Размер | Диапазон |
|---------|--------|----------|
| `_CODE` | ~27 250 Б (0x6A6F) | 0x41000xAB6F |
| `_HOME` | 227 Б | 0xAB6F |
| `_DATA` | 3 449 Б (0x0D79) | 0xAC780xB9F1 |
| `_BSS` | 290 Б | |
**Образ ≈ 31.2 КБ; до верха W2 (0xBFFF) остаётся ≈ 1.3 КБ на кучу+стек.**
Куча в roomtest почти не используется (атласы/уровень — в EMM-страницах),
но запас критично мал.
### Вклад модулей в _CODE (по .map, приблизительно)
```
7003 pop_bg (вся отрисовка тайлов/слоёв/wall_pattern)
6326 pop_kid (из них ~3745 Б — СТАТ. ТАБЛИЦЫ kid_data.h, см. ниже)
3762 pop_map (коллизия/физика/пики)
1455 pop_trob (кнопки/ворота/пики-каркас)
1224 pop_level (загрузка уровня, doorlink)
914 roomtest (главный цикл)
~7000 libc/libbgi (gfx_blit*, atlas_load, kbd_raw, open/read, irq, div/mul…)
```
### Крупные СТАТИЧЕСКИЕ данные (сейчас в _CODE как `const`)
- **`kid_data.h` — самый большой кусок, ~3.7 КБ**, живёт в _CODE (атрибутируется
pop_kid):
- `kid_seqtbl[2310]` — байткод последовательностей (play_seq).
- `kid_frames[241]` × 5 Б = 1205 Б — таблица кадров (image,dx,dy,flags,sword).
- `kid_seq_off[115]` × 2 Б = 230 Б — смещения seq.
- `pop_bg`: `tile_table[31]`×12 = 372 Б + ~20 мелких const-таблиц (COL_XH,
WALL_FRAM_*, SPIKES_FRAM_{RIGHT,LEFT,FORE}, LOOSE_FRAM_*, DOOR_FRAM_SLICE,
BLUELINE_*, LPOS/RPOS, FLOOR_LEFT_OVERLAY) — суммарно ~0.50.7 КБ.
- `pop_map`: `x_bump[20]`, `y_land[5]`, `wall_dl/dr`, `dir_front/behind` — ~100 Б.
- В `_DATA` (W2, не CODE): `room_modif[24][30]`=720 Б + копии LINKLOC/LINKMAP=512 Б
(pop_trob/pop_level) + рабочие массивы roomtest.
---
## 2. ПУТЬ A — оптимизация КОДА (без смены модели)
1. **Компиляторные флаги** (`bin/sprinter-cc`): попробовать `--opt-code-size`
у SDCC и подобрать `--max-allocs` (сейчас дефолт 100000; меньше = мельче код,
но медленнее компиляция; см. `mdview2_size_budget` — там `--max-allocs`
давал −1.4 КБ). Замерить каждый модуль отдельно.
2. **Дедуп подстановки нажатой кнопки**: логика `opener→floor / closer→stuck`
по таймеру связи ПРОДУБЛИРОВАНА в `draw_tile` и `fore_tile` (pop_bg.c).
Вынести в `static inline`/helper `subst_pressed_button(code,mod)`.
3. **wall_pattern / prandom** (pop_bg): 32-битный LCG (`unsigned long`) —
пользователь не любит 32-бит (см. `avoid_32bit_arith_z80`); но это PRNG
оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только
если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность.
4. **Ревизия дублей**: `y_to_row` определён в pop_bg И pop_map; мелкие
геометрические хелперы дублируются — свести в один internal-модуль.
5. `/simplify`-проход по последним правкам Фазы B (pop_trob/pop_bg).
Ожидаемый выигрыш пути A: единицы–первые сотни байт на пункт; в сумме,
оптимистично, ~1–2 КБ. Недостаточно как единственная мера.
---
## 3. ПУТЬ B — вынос СТАТ. ДАННЫХ в EMM-страницы (с атласами / с level)
**Идея (по замечанию пользователя):** EMM-страницы атласов и уровня
использованы лишь частично (страница 16 КБ, данных меньше), в «хвосте» —
свободное место. Часть `const`-таблиц можно хранить ТАМ, а не в _CODE/_DATA,
если таблица читается ИМЕННО ТОГДА, когда нужная страница уже в W0.
**Механика W0:** атласы блитятся из W0 (`_gfx_w0_state`: `_gfx_w0_cur`
спрайт-страница в W0; ISR-стаб `_gfx_w0_isr` возвращает её после прерывания).
Уровень (pop_level) маппит свою страницу в W0 на время извлечения
(`gfx_w0_map`/`gfx_w0_unmap`). → пока страница в W0, CPU может читать и
данные из неё по адресам 0x0000..0x3FFF.
**Категоризация таблиц по W0-контексту (задача сессии — уточнить по каждой):**
- **(a) Читается, когда в W0 АТЛАС** → хранить в свободном хвосте атлас-страницы.
Кандидаты — таблицы, которые нужны В МОМЕНТ блита конкретного атласа.
ГРАБЛИ: `draw_tile` читает `tile_table`/`COL_XH` ДО блита (чтобы решить, какой
спрайт/куда) — в этот момент в W0 может быть ДРУГАЯ страница (DSS/предыдущий
атлас). Т.е. большинство draw-таблиц читаются ВНЕ W0-атлас-контекста →
«в лоб» не переносятся. Нужен аудит КАЖДОГО чтения: гарантирована ли нужная
страница в W0 в этот тик.
- **(b) Читается, когда в W0 LEVEL** → хранить с уровнем (в его странице; там
~13.7 КБ свободно из 16). Кандидаты: константы декода doorlink, разбор
комнат — всё, что pop_level делает под `gfx_w0_map(lvl_page)`.
- **(c) Нужна и там, и там** → дублировать в обеих страницах ЛИБО оставить
резидентной (если дубли дороже экономии).
- **(d) Читается в чистой ЛОГИКЕ (W0 не важен)** → перенос требует ЯВНОГО
`gfx_w0_map` на каждое чтение (дорого, особенно в горячих циклах) → как
правило оставить резидентной.
**Отдельно `kid_data.h` (3.7 КБ — самый жирный кандидат):**
- `kid_frames`/`kid_seqtbl` читаются в `play_seq` (ЧИСТАЯ логика, каждый тик) И
в `kid_draw` (блит из kid-атласа, kid-страница в W0). Т.е. частично (a),
частично (d). Перенос всей таблицы в kid-атлас-страницу заставит `play_seq`
делать `gfx_w0_map` на каждый шаг байткода → замерить стоимость (может убить
бюджет спрайтов, см. `sprite_engine_perf`). Вариант: держать в EMM отдельной
страницей данных Kid и маппить один раз на кадр вокруг kid_tick+kid_draw.
- Это самый большой одиночный выигрыш (−3.7 КБ из _CODE), но и самый рискованный
по скорости — приоритетный к ПРОТОТИПИРОВАНИЮ и замеру.
**Паттерн переноса writable/const данных в банк/страницу:** см. memory
`bank_local_data_pattern` (--codeseg/--constseg/--dataseg BANKn + trampoline-fix
+ mkexe -p 0) и `sdcc_static_storage_gotcha`.
### 3.1 Свободное место в страницах (замер 2026-07-21, страница = 16384 Б)
```
BG-атласы: размер свободно
pop_env0.atl 10578 5806
pop_env1.atl 12449 3935 <- САМАЯ ТЕСНАЯ из bg
pop_env2.atl 10798 5586
pop_env3.atl 5032 11352 <- много места
pop_env4.atl 8498 7886
pop_wall.atl 11543 4841
pop_fore.atl 7763 8621
Kid-атласы (28 стр): free min=6161 max=15452 avg=9722
Level (res2001.bin): данные 2305, свободно ~13823 (16384 0x100 стаб 2305)
```
**Выводы по вместимости:**
- **Макс. данных в ОДНОМ атлас-банке = свободный хвост ЭТОЙ страницы** (см.
таблицу). Связывающее ограничение — самая тесная нужная страница (env1 =
3935 Б; не перегружать её).
- Если страница будет маппиться в **W0** — минус ~0x100 Б на ISR-стаб (как
level). Атлас-страницы стаб УЖЕ содержат (atlas_load патчит) → данные класть
в хвост ПОСЛЕ атласа.
- **`kid_data.h` (3.7 КБ) влезает в kid-страницу** (min free 6161) или в
отдельную выделенную страницу данных Kid — предпочтительно отдельную (маппить
раз на кадр, не конфликтуя с kid-атласами блита).
- **Level-таблицы** — вагон места в level-странице (~13.8 КБ).
- **BG draw-таблицы** (~0.7 КБ) влезут в env3/fore/env4 (много free), НО см.
граблю W0-контекста в §3(a) — читаются ли они, когда нужная страница в W0.
- **Выделенная страница ТОЛЬКО под данные** (не делить с атласом) = до ~16 КБ
(−0x100 стаб при W0-маппинге). EMM-бюджет это позволяет (см.
`sprinter_emm_budget`: 215/3440 КБ free на старте).
- **Принудительно уменьшать макс. атлас (репак мельче) — КРАЙНИЙ случай:** это
резко поднимет число атлас-банков (сейчас 5 env-страниц адресуются как id>>5;
дробление ломает эту адресацию и множит страницы). Сначала использовать
СУЩЕСТВУЮЩИЙ свободный хвост и отдельные data-страницы.
---
## 4. ПУТЬ C — переход на huge (banked code)
### 4.1 Что такое huge сейчас (`bin/sprinter-cc`, `runtime/crt0_banked`)
- `--memory huge`: `MODE_CODE_LOC=0x4100`, **`MODE_DATA_LOC=0x8000` (ФИКС.)**,
banked code в W3. crt0_banked, как crt0_small, авто-детектит W2.
Помечено `[TODO]` — не обкатано.
- Отличие от small: small цепляет DATA сразу за CODE (`--data-loc 0`); huge
ФИКСИРУЕТ DATA на 0x8000.
### 4.2 ТРЕБОВАНИЕ (по пользователю): huge должен переносить DATA динамически
Сейчас huge жёстко кладёт DATA на 0x8000. Если РЕЗИДЕНТНЫЙ CODE вылезет за
0x8000 (W1 = только 0x4000..0x7FFF ≈ 16 КБ; резидент > 16 КБ лезет в W2) →
коллизия с DATA. **Надо научить huge класть DATA динамически ЗА резидентным
CODE (как small: `--data-loc 0` + crt0 считает старт), а не на фикс 0x8000.**
Тогда huge = «small-раскладка резидента (W1+W2, DATA за CODE) + ДОП. код в
банках W3». Это первый пункт работ по huge.
### 4.3 КОНФЛИКТ: графика тоже хочет W3 (ключевой риск)
`pop_banking_architecture` прямо говорит: **графику нельзя в W3** (блиты/атласы
используют окна; см. §4.5). Поэтому в банки W3 можно выносить ТОЛЬКО
НЕ-графические блоки, и такой банк НЕ должен во время своего исполнения держать
графику в W3. Если W3-банкованная функция ЗОВЁТ графику (которой нужен W3),
трамплин обязан сохранить/восстановить банк вокруг вызова (проверить, что
banking-ABI это делает — `sdcc_banking`). Альтернатива без этого риска —
**big + BANK_W1** (банк кода в W1, не W3), рекомендованная в
`pop_banking_architecture` именно из-за W3-графики. Сессия должна выбрать:
huge(W3) с аккуратным save/restore ИЛИ big(BANK_W1).
### 4.4 Какие блоки МОЖНО вынести (не работают с графикой напрямую)
Замер graphics-ref по модулям (grep `gfx_|blit|env_b|wall_b|fore_b|setfillstyle|
bar(|GFX_BANK|initgraph`):
```
pop_bg.c : 83 — РЕЗИДЕНТ (вся отрисовка)
roomtest.c : 23 — РЕЗИДЕНТ (главный цикл + флип страниц)
pop_level.c : 17 — использует gfx_w0_map (W0, не W3-блиты) — ПОГРАНИЧНЫЙ
pop_kid.c : 12 — kid_draw = графика; НО play_seq — чистая логика (можно split)
pop_ctrl.c : 0 — КАНДИДАТ В БАНК (ввод/диспетчер control)
pop_map.c : 0 — КАНДИДАТ В БАНК (коллизия/физика, ~3.8 КБ) — лучший по объёму
pop_trob.c : 0 — КАНДИДАТ В БАНК (кнопки/ворота/пики-логика)
```
- **Лучшие кандидаты в W3-банк(и): pop_map + pop_trob + pop_ctrl** (нет прямой
графики; вместе ~5.3 КБ CODE). Освобождают резидент → он влезает в W1.
- **Осторожно с межбанковыми вызовами:** pop_map/pop_trob ЗОВУТ pop_bg
(перерисовка loose/пик/кнопок/шва) и pop_kid (play_seq/kid_set_seq). Это
кросс-банк вызовы через трамплин (`sdcc_banking`: стек +3 байта, виртуальный
24-битный адрес). Правило `pop_banking_architecture`: «один файл = один банк
= прямые вызовы», main резидентен. Проверить, что трамплин сохраняет W3
вокруг вызова в графический pop_bg (см. §4.3).
- **pop_kid split** (по желанию): вынести play_seq/seqtbl-интерпретатор
(логика + таблицы kid_data.h) в банк, оставить kid_draw/kid_heal резидентными.
Даёт и −код, и −данные из резидента, но требует аккуратного разделения TU
(1 функция = 1 модуль, см. `libc_one_function_per_module`).
- **pop_level: пограничный** — не блитит, но маппит уровень в W0; банковать
можно, если W0-логика совместима с трамплином (проверить ISR-стаб взаимодействие).
### 4.5 Почему графику нельзя в W3 (контекст)
Блиттер держит спрайт-страницу атласа в **W0** (`_gfx_w0_state`,
`_gfx_w0_isr`). Ускоритель/адресация видео — отдельная тема (см.
`sprinter_accelerator`, `sprinter_graphics`). W3 в banked-раскладке — окно
кода-банка; смешивать с окном, которое графика перемапливает, нельзя без
save/restore. Детально — `pop_banking_architecture`, `graphics_constraints`.
---
## 5. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии)
1. **Замер-базлайн** (CODE/DATA/BSS + per-module) — зафиксировать до.
2. **Путь A** дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый 1..2 КБ.
3. **huge §4.2**: научить huge класть DATA динамически (как small) — инфраструктурный
пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем
резиденте (пока без выноса — просто huge-раскладка = small + пустой W3).
4. **huge §4.4**: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить
кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1.
5. **Путь B** (по остатку нужды): прототип выноса `kid_data.h` в EMM-страницу
Kid с маппингом раз на кадр; замерить скорость (`sprite_engine_perf`).
Затем аудит draw-таблиц по W0-контексту (§3 a/b/c/d).
## 6. Ссылки
- `bin/sprinter-cc` (§162+ — резолв memory-mode → CODE_LOC/DATA_LOC).
- `runtime/crt0_small.*`, `runtime/crt0_banked.*`, `runtime/bank.s`.
- memory: `sprinter_memory_modes`, `memory_modes_implemented`,
`setwin2_for_w2_alloc`, `sdcc_banking`, `bank_local_data_pattern`,
`pop_banking_architecture`, `avoid_32bit_arith_z80`,
`libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`.
- `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют
оптимизацию, но держать в уме при рефакторе pop_map/pop_bg).
---
## 7. Лишние блиты в горячем пути (добавлено 2026-07-27)
Найдено при разборе окклюзии по эталону SDLPoP: **наш «передний слой» рисовал
спрайты, которых в оригинале там нет** — это и артефакты, и лишняя работа
каждый кадр. Исправлено: `fore_tile` (вызывается для КАЖДОГО тайла футпринта
Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — переднюю кромку пола; в
оригинале `draw_tile_fore` (seg008:690) добавляет только `add_foretable`-часть,
а `bottom` идёт через `draw_tile_bottom` в backtable (ПОД персонажем).
Итог: −2..4 блита за кадр, `_CODE` −388 Б, ушла «тень» у основания колонны.
**Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли
этот спрайт в оригинале в ЭТОЙ таблице?):**
1. `pop_room_draw`/`draw_tile` — вызовы на входе в комнату не критичны по
скорости, но по ним стоит сверить состав слоёв (backtable vs foretable).
2. `overlay_mid_tile` — сейчас точный порт midtable-части `draw_tile2`;
проверить, не рисуем ли `base_id` там, где оригинал его не рисует
(loose: base=0, потому что кадр плиты идёт через `draw_loose` в backtable).
3. `pop_loose_mob_tick` — перерисовка соседнего тайла (`draw_tile(mob_row,
mob_col+1)`) КАЖДЫЙ кадр падения: в оригинале это `set_redraw_full` на
один кадр; можно ограничить только тайлом, который реально пересекается
с куском.
4. `pop_ceil_shake_draw` — heal 64×8 + два `draw_tile(-1,·)` на кадр тряски;
проверить, нужен ли второй тайл (правую грань loose в полосе потолка
оригинал не рисует вовсе — `draw_tile_aboveroom` без `draw_tile_anim_right`).
5. `fore_only_tile` для полосы потолка: вызывается для всех колонок габарита,
а оригинал (`redraw_needed_above`) — только для колонок с флагом
`redraw_frames_above`; сузить до колонок, реально задетых спрайтом.
6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить,
не зовём ли её там, где оригинал ограничивается `wall_fram_main`.
---
## 8. Скорость отрисовки: замеры и запас (2026-07-27)
Профилирование в MAME (маркеры в порт 0xFE + `wpiset … totalcycles`, приём из
memory `mame_mcp_bridge`). Кадр Sprinter = **430 080 тактов**.
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
пересчёт src, нарезка полос >256), а не за пиксели:
| путь (спрайт 32×3) | тактов |
|---|---|
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
| линейное спрайтовое ядро без клипа (`putsprite` при `gfx_sprite_clip(0)`) | 4 617 |
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
кадра**; сам `bar` — только 13 308.
**СДЕЛАНО (шаг 1):** в libbgi добавлен `gfx_blit_noclip()`
(`common/gfx_blit_noclip.c`, прототип в `include/gfx.h`) — блит без клипа в
ТЕКУЩЕМ банке через линейное ядро; `pop_bg.blit_b` уходит на него, когда
спрайт целиком на экране и не нужен `g_clip_top`. Выигрыш ~2.9× на каждом
фоновом блите (подтверждено в MAME).
**ВАЖНО:** W3-скобку (`_bgi_begin/_bgi_end`) ставит САМА libbgi — вызывать её
из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
белый экран).
**ЗАПАС (шаг 2), когда перестанет хватать бюджета кадра:**
1. **Батчинг W3-скобки** — одна `_bgi_begin/_bgi_end` на весь `draw_tile`
вместо скобки на блит; нужен публичный batch-API в libbgi (как у
спрайтового движка). Осторожно: между begin/end стоит `DI` — длинная
серия задержит кадровое прерывание.
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
(`env 52`, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор
бара на высоту тайла) и выводить одним `gfx_blit_part` с обрезкой по фазе
`gate_bot_y & 7`: 7 блитов → 1.
3. **Не перерисовывать статичные части шва** — грань ворот (env 47, 26×62),
пол (41) и кромка (43) при анимации решётки не меняются; если стирать
только полосу баров, уйдут ещё 3 блита из 9.
4. См. также §7 (лишние блиты, которых нет в оригинале).
+53
View File
@@ -0,0 +1,53 @@
# PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md §5).
# --memory huge, БЕЗ --bank.
#
# ПОЧЕМУ huge, а НЕ small (исправлено 2026-07-16): poc использует
# raw-клавиатуру (kbd_raw_open → IM2-таблица). Буферы IM2 (_irq_vec_buf,
# BSS) ОБЯЗАНЫ жить в W2 (0x8000-0xBFFF) — во время прерывания W1/W3
# могут быть перемаплены DSS (см. libc/irq/_irq_table.c, memory/
# fps_divider: «verified tiny/big/huge, small=EINVAL»). --memory small
# пулит W1+W2 в плоские ~32КБ и чейнит DATA за CODE — при небольшом CODE
# BSS уезжает в W1 (<0x8000), и _irq_table_ref отдаёт EINVAL →
# kbd_raw_open молча возвращал -1, poc печатал ошибку УЖЕ в графическом
# режиме (невидимо) и выходил в prompt. huge кладёт CODE в W1, а
# DATA/BSS/STACK/HEAP жёстко в W2 → IM2 работает. Цена: раздел 16КБ
# CODE / 16КБ DATA вместо общего 32КБ-пула small — сейчас влезает с
# запасом; при росте настоящего PoP CODE>16КБ понадобится банк под код.
#
# --bank НЕ нужен: gfx_blit_part()/atlas_load() сами временно трогают W3
# (видеобанк / чтение атласа) — банк room.c в W3 давал вероятностный
# «снег»; банк в W1 несовместим с CODE=W1 (трамплин переключения W1 сам
# бы уехал). sprintf() заменён на ручное hex-форматирование пути в
# tile_atlas_load() (единственный потребитель printf, ~2.9КБ).
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
EXAMPLE := poc
MEMORY ?= huge
EXTRA_FLAGS ?= --gfx 256
EXTRA_SRCS := room.c
TILE_ATLASES := res/tiles/tile01.atl res/tiles/tile14.atl res/tiles/tile03.atl \
res/tiles/tile13.atl res/tiles/tile0e.atl res/tiles/tile0b.atl
EXTRA_DATA := tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES)
include $(PROJ_ROOT)/app.mk
LEVEL1_BIN := $(PROJ_ROOT)/applications/PoP/SDLPoP/data/LEVELS/res2001.bin
tools/kid.raw tools/kid.pal: tools/gen_kid_placeholder.py
cd tools && python3 gen_kid_placeholder.py
tools/kid.atl: tools/kid.raw
python3 $(PROJ_ROOT)/toolchain/mkatlas.py $@ tools/kid.raw:16x16:1x12
res/room1.dat: tools/extract_room.py $(LEVEL1_BIN)
python3 tools/extract_room.py $(LEVEL1_BIN) 1 res/room1.dat
res/tiles/1F-0.png: tools/gen_tile_placeholders.py
cd tools && python3 gen_tile_placeholders.py
tools/room.pal $(TILE_ATLASES): tools/kid.pal res/tiles/1F-0.png tools/build_room_palette.py
cd tools && python3 build_room_palette.py
# make_disk.py упаковывает EXTRA_DATA на диск ПОД БАЗОВЫМ ИМЕНЕМ
# (плоская ФС) — tileNN.atl/room1.dat оказываются в корне рядом с
# kid.atl/room.pal, room.c/poc.c грузят их без пути.
$(EXAMPLE).exe: room.c tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES)
+153
View File
@@ -0,0 +1,153 @@
/*
* level.h — структуры уровня PoP под наш движок.
*
* Формат — POP-DAT-FormatSpecifications.pdf §3.4 (DAT 1.0): комната =
* 30 тайлов (10 колонок x 3 ряда), foretable даёт тип тайла,
* backtable — модификатор/состояние (семантика зависит от типа).
* Адресация тайла: tile = (room-1)*30 + tileOffset, tileOffset 0-9 =
* верхний ряд, 10-19 = средний, 20-29 = нижний, слева направо.
*
* Фаза 1 (applications/PoP/docs/PORT_PLAN.md §5, сузили объём):
* только геометрия (пол/стены/провалы) для коллизий — двери, факелы,
* ловушки, гарды НЕ используются (структуры под них здесь тоже
* упрощены/оставлены как заготовка на потом, см. §7 плана Фаза 2).
*/
#ifndef LEVEL_H
#define LEVEL_H
#include <stdint.h>
/* 1 (не 24) — PoC грузит и рисует только комнату 1; расширить, когда
* появится настоящий level_load() на несколько комнат. */
#define LEVEL_ROOMS 1 /* комнаты нумеруются 1..24 в файле,
* здесь индекс 0..23 = комната N+1 */
#define ROOM_COLS 10
#define ROOM_ROWS 3
#define ROOM_TILES (ROOM_COLS * ROOM_ROWS) /* 30 */
/* --- Типы тайлов (Table 7 спецификации) --- */
#define TILE_TYPE(byte) ((uint8_t)((byte) & 0x1F))
#define TILE_MODIFIER(byte) ((uint8_t)(((byte) >> 5) & 1))
enum {
TILE_EMPTY = 0x00,
TILE_FLOOR = 0x01,
TILE_SPIKES = 0x02,
TILE_PILLAR = 0x03,
TILE_GATE = 0x04,
TILE_STUCK_BUTTON = 0x05,
TILE_DROP_BUTTON = 0x06,
TILE_TAPESTRY = 0x07,
TILE_PILLAR_BOTTOM = 0x08,
TILE_PILLAR_TOP = 0x09,
TILE_POTION = 0x0A,
TILE_LOOSE = 0x0B,
TILE_TAPESTRY_TOP = 0x0C,
TILE_MIRROR = 0x0D,
TILE_DEBRIS = 0x0E,
TILE_RAISE_BUTTON = 0x0F,
TILE_EXIT_LEFT = 0x10,
TILE_EXIT_RIGHT = 0x11,
TILE_CHOPPER = 0x12,
TILE_TORCH = 0x13,
TILE_WALL = 0x14,
TILE_SKELETON = 0x15,
TILE_SWORD = 0x16,
TILE_BALCONY_LEFT = 0x17,
TILE_BALCONY_RIGHT = 0x18,
TILE_LATTICE_PILLAR = 0x19,
TILE_LATTICE_SUPPORT= 0x1A,
TILE_LATTICE_SMALL = 0x1B,
TILE_LATTICE_LEFT = 0x1C,
TILE_LATTICE_RIGHT = 0x1D,
TILE_TORCH_DEBRIS = 0x1E,
TILE_NULL = 0x1F
};
/* Твёрдые тайлы — Фаза 1 (только геометрия); классификация наша, для
* коллизий движка, не часть исходного формата. Двери/шипы/дробилки и
* т.п. сознательно исключены из объёма Фазы 1 (см. §5 плана) — при
* встрече в реальных данных трактовать как проходимые до Фазы 2.
*
* tile_is_solid() — "есть опора сверху" (вертикальный смысл: можно
* стоять НА этом тайле) — Floor ТОЖЕ solid в этом смысле! Для
* горизонтальной коллизии (можно ли ВОЙТИ в эту клетку сбоку) нужен
* ОТДЕЛЬНЫЙ предикат — см. tile_blocks_side ниже. Баг 2026-07-16:
* col_blocked() в poc.c ошибочно звал tile_is_solid() для бокового
* упора — Floor блокировал сам себя, Кид не мог сдвинуться с места
* стоя на полу. */
static inline uint8_t tile_is_solid(uint8_t byte)
{
switch (TILE_TYPE(byte)) {
case TILE_FLOOR:
case TILE_PILLAR:
case TILE_PILLAR_BOTTOM:
case TILE_PILLAR_TOP:
case TILE_WALL:
case TILE_BALCONY_LEFT:
case TILE_BALCONY_RIGHT:
return 1;
default:
return 0;
}
}
/* tile_blocks_side() — настоящая преграда СБОКУ (нельзя войти в
* клетку по горизонтали): Wall/Pillar-семейство. Floor/Balcony НЕ
* блокируют — по ним идёшь (тайл под ногами, не впереди). Lattice-
* колонны (0x19-0x1D) — узкие, Кид физически проходит мимо (см.
* gen_tile_placeholders.py draw_lattice_like) — тоже НЕ блокируют. */
static inline uint8_t tile_blocks_side(uint8_t byte)
{
switch (TILE_TYPE(byte)) {
case TILE_PILLAR:
case TILE_PILLAR_BOTTOM:
case TILE_PILLAR_TOP:
case TILE_WALL:
return 1;
default:
return 0;
}
}
/* --- Тайл и комната --- */
typedef struct {
uint8_t type; /* foretable byte (rrmccccc — см. TILE_TYPE/MODIFIER) */
uint8_t state; /* backtable byte — модификатор/состояние */
} tile_t;
typedef struct {
tile_t tiles[ROOM_TILES]; /* индекс = tileOffset 0..29 */
uint8_t link_left, link_right; /* links-блок: 0 = нет соседа */
uint8_t link_up, link_down;
uint8_t guard_location; /* 0..29; 30 (и выше) = нет гарда */
int8_t guard_direction; /* 0 = вправо, -1 = влево */
uint8_t guard_skill; /* 0..9 */
uint8_t guard_colour; /* индекс палитры, Table 11 */
/* door I/II (событийные цепочки) — Фаза 2, не здесь */
} room_t;
typedef struct {
room_t rooms[LEVEL_ROOMS]; /* индекс 0 = комната 1 (файл 1-based) */
uint8_t start_room; /* 1..24 */
uint8_t start_location; /* 0..29 */
int8_t start_direction; /* 0 = вправо, -1 = влево */
} level_t;
/* Тайл по (room 1-based, col 0-9, row 0-2). */
static inline tile_t *level_tile(level_t *lv, uint8_t room, uint8_t col, uint8_t row)
{
return &lv->rooms[room - 1].tiles[row * ROOM_COLS + col];
}
/* Загружает ОДНУ комнату + стартовую позицию уровня из файла в формате
* tools/extract_room.py (63 Б: foretable[30]+backtable[30] той комнаты
* + start_room+start_pos+start_dir, вырезанные из res20NN.bin — layout
* подтверждён декодом байт 2026-07-15/16: файл начинается СРАЗУ с
* foretable[720], потом backtable[720], без заголовка; start_position
* — смещение 2112, сверено со структурой level_type в SDLPoP/src/
* types.h). Заполняет lv->start_room/start_location/start_direction.
* 0 — OK, -1 — файл не найден/короче 63 Б. */
int level_load_room(level_t *lv, uint8_t room, const char *path);
#endif
+264
View File
@@ -0,0 +1,264 @@
/*
* poc.c — PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md
* §5): проверяем управление (raw-клавиатура, held-state) + коллизию
* по краям экрана + анимацию ходьбы + прыжок/присед поверх готового
* спрайтового движка (sprite.h).
*
* ВАЖНО: персонаж — ВРЕМЕННАЯ ЗАГЛУШКА (лицензированный спрайт-пак
* third_party/16x16-RPG-characters через tools/gen_kid_placeholder.py,
* тот же источник, что уже использует examples/rpgwalk), НЕ графика
* оригинальной Prince of Persia — см. §5 и §8.5 плана. У заглушки нет
* отдельных поз прыжка/приседа — механика (тайминг дуги, состояние,
* коллизия с полом) проверяется на том же спрайте без смены позы;
* визуально это упрощение, не финальный вид.
*
* Дуга прыжка (jump_height[]) — СВОЯ, приблизительная (не таблица
* смещений оригинала — см. §6 плана: авторские таблицы кадров решено
* не переносить, только код/структуры).
*
* Нет ещё (следующие итерации): реальный уровень/фон по BLUETYPE,
* рывок вбок при прыжке с разбега, зацепление за уступ.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <kbd_raw.h>
#include <conio.h>
#include <stdio.h>
#include "level.h"
#include "room.h"
/* Комната 1 уровня 1 — РЕАЛЬНАЯ геометрия (foretable/backtable),
* вырезана tools/extract_room.py из applications/PoP/SDLPoP/data/
* LEVELS/res2001.bin (level_load_room(), см. level.h/room.c) — не
* плейсхолдер. Верхний ряд (row0) — floor-уступ на cols 3-7 (там
* реально стоит персонаж в оригинале), стены по cols 8-9; ряды 1-2 —
* ниже уступа (торч/колонны/пол — Фаза 1 просто их отрисовывает по
* тем же типам, без многоуровневой физики падения). */
static level_t test_level;
#define CHAR_ROW 0 /* ряд, где стоит персонаж (floor-уступ room1) */
#define ROW_Y(r) ((r) * 64) /* room_row_h все по 64 */
#define GROUND_Y ROW_Y(CHAR_ROW + 1) /* низ ряда CHAR_ROW = верх пола */
#define KIDY (GROUND_Y - 16) /* y спрайта (16 px высотой) */
#define SPEED 2 /* px/кадр — заглушка, не авторский темп */
/* Настоящая преграда (Wall/Pillar) слева/справа от кандидата x в
* CHAR_ROW — блокирует движение (грубая проверка по краям хитбокса
* 16px, без под-тайловой подгонки — для PoC достаточно, см.
* PORT_PLAN.md §6). tile_blocks_side(), НЕ tile_is_solid(): Floor —
* тайл, на котором Кид СТОИТ (тот же CHAR_ROW), tile_is_solid() его
* тоже считает "твёрдым" (можно стоять сверху) — если проверять им же
* боковую преграду, Кид не мог сдвинуться с собственного пола (баг,
* найден 2026-07-16). */
static uint8_t col_blocked(int x)
{
uint8_t c0, c1;
if (x < 0 || x + 15 >= ROOM_COLS * ROOM_TILE_W)
return 1;
c0 = (uint8_t)(x / ROOM_TILE_W);
c1 = (uint8_t)((x + 15) / ROOM_TILE_W);
if (tile_blocks_side(level_tile(&test_level, 1, c0, CHAR_ROW)->type))
return 1;
if (tile_blocks_side(level_tile(&test_level, 1, c1, CHAR_ROW)->type))
return 1;
return 0;
}
/* Ленты атласа: dir*3+frame, dir 0=вниз/1=влево/2=вправо/3=вверх,
* 3 кадра маятника на направление (см. tools/gen_kid_placeholder.py). */
#define DIR_DOWN 0
#define DIR_LEFT 1
#define DIR_RIGHT 2
/* Дуга прыжка: своя, приблизительная (не авторская таблица, см. шапку
* файла) — высота над полом (px) по кадрам 0..19, УЖЕ ПОЛНЫЙ горб
* (подъём 2→22 к элементу 10, спуск обратно к 2 к элементу 19) — БЕЗ
* зеркалирования в коде, массив читается один раз целиком. БАГ,
* найденный пользователем 2026-07-15: раньше код ЕЩЁ РАЗ зеркалил
* этот уже полный горб на 40 кадров — получалось два полных прыжка
* подряд от одного триггера (не проблема клавиатуры/декодера, чистая
* рассинхронизация данных и комментария). 20 кадров @ 50 Гц ~= 0.4 с. */
static const uint8_t jump_height[20] = {
2, 4, 7, 10, 13, 16, 18, 20, 21, 22,
22, 21, 20, 18, 16, 13, 10, 7, 4, 2
};
#define JUMP_FRAMES 20
static atlas_t at;
static sprite_t kid;
static uint8_t facing = DIR_DOWN; /* текущее направление анимации */
static uint8_t jumping = 0; /* 0 = на земле */
static uint8_t jump_t = 0; /* кадр дуги, 0..JUMP_FRAMES-1 */
static uint8_t crouching = 0;
/* Прыжок — level-triggered НАМЕРЕННО (не edge-detect): если UP всё ещё
* зажат к моменту приземления — следующий прыжок стартует СРАЗУ (цепочка
* прыжков, пока держишь); отпустил раньше — второй прыжок не начнётся
* сам, только по следующему нажатию. Раньше здесь были up_prev/
* jump_cooldown — попытка "починить" ровно ЭТО поведение, приняв его
* за баг; убрано 2026-07-15 после уточнения желаемого поведения. */
static void draw_room(void)
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
room_draw(&test_level, 1);
setcolor(LIGHTGRAY);
outtextxy(60, 4, "PoP PoC: hold LEFT/RIGHT to walk, ESC to quit");
outtextxy(4, 14, "(placeholder tiles -- not original PoP art)");
}
/* HUD-плашка состояния (нет отдельной позы прыжка/приседа — статус
* текстом, рисуется банком 0x50, heal спрайтового движка её не
* трогает — как fps-плашка в examples/rpgwalk). */
static void show_state(uint8_t jump, uint8_t crouch)
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, 24, 60, 32);
setcolor(YELLOW);
if (jump)
outtextxy(0, 24, "JUMP");
else if (crouch)
outtextxy(0, 24, "CROUCH");
}
static void set_facing(uint8_t dir)
{
if (facing == dir)
return;
facing = dir;
sprite_anim(&kid, (uint8_t)(dir * 3), (uint8_t)(dir * 3 + 2),
6, ANIM_PINGPONG);
}
int main(void)
{
uint8_t page, hidden;
int x;
if (level_load_room(&test_level, 1, "room1.dat") != 0 &&
level_load_room(&test_level, 1, "a:\\room1.dat") != 0) {
puts("room1.dat not found");
return 1;
}
/* Стартовая позиция. В данных room1 start_location = tileOffset 0
* (row0/col0) — а там EMPTY (провал, без пола); в Фазе 1 нет физики
* падения, поэтому для PoC ставим Кида на floor-уступ (col 3, где он
* реально стоит в оригинале). Когда появится многоуровневая физика —
* брать col из start_location. start_direction: -1 = влево, 0 =
* вправо (level.h). */
x = 3 * ROOM_TILE_W;
facing = (test_level.start_direction < 0) ? DIR_LEFT : DIR_RIGHT;
if (atlas_load(&at, "kid.atl") != 0 &&
atlas_load(&at, "a:\\kid.atl") != 0) {
puts("kid.atl not found");
return 1;
}
if (tile_atlas_load("") == 0 && tile_atlas_load("a:\\") == 0) {
puts("tile atlases not found");
atlas_free(&at);
return 1;
}
initgraph();
/* room.pal = EGA16 + Kid + Floor + Wall — ОДНА палитра на всё,
* собрана tools/build_room_palette.py (см. --seed-pal в
* toolchain/png_strip.py) — Kid и тайлы на экране одновременно,
* их "свои" цвета обязаны жить в одной таблице. */
if (gfx_pal_fload(0, "room.pal") < 0)
gfx_pal_fload(0, "a:\\room.pal");
gfx_pal_sync();
gfx_sprite_clip(0); /* коллизия по краям гарантирует границы */
/* kid.atl — ОДНА лента (12 кадров вертикально: dir*3+кадр, см. шапку
* файла и examples/rpgwalk); индекс atlas_sprite_init — это НОМЕР
* ЛЕНТЫ (персонажа), а не кадра. Лента одна → всегда 0. Стартовый
* кадр направления выставляем sprite_frame (вертикальная лента, fh=16:
* кадр N на sy=N*16). Баг Соннета (найден 2026-07-16): здесь стоял
* facing*3 как индекс ЛЕНТЫ — при старте лицом влево (idx 3) читался
* мусор за каталогом атласа → мусорные w/h/src → блит спрайта заливал
* пол-экрана «снегом». */
atlas_sprite_init(&kid, &at, 0);
sprite_frame(&kid, 0, (int)(facing * 3) * 16);
kid.x = x;
kid.y = KIDY;
sprite_show(&kid);
for (page = 0; page < 2; page++) { /* фон + спрайт на обе страницы */
gfx_set_draw_page(page);
draw_room();
sprite_update(&kid, 1);
}
gfx_set_visible_page(0);
/* kbd_raw требует BSS в W2 (IM2-таблица) — недоступно в --memory small
* (там BSS может уехать в W1 → EINVAL); poc собирается --memory huge
* (CODE в W1, DATA/BSS в W2). closegraph ДО puts — иначе сообщение
* ушло бы в графический режим (невидимо), а программа молча вышла бы
* в prompt (баг Соннета, найден 2026-07-16). */
if (kbd_raw_open() != 0) {
closegraph();
puts("kbd_raw_open failed (need memory mode with BSS in W2)");
atlas_free(&at);
return 1;
}
for (;;) {
uint8_t moving = 0;
int y = KIDY;
if (jumping) {
/* дуга идёт сама; направлением можно скользить вбок,
* поза не меняется (нет отдельного кадра прыжка) */
if (kbd_raw_down(KBD_LEFT)) {
if (!col_blocked(x - SPEED)) x -= SPEED;
} else if (kbd_raw_down(KBD_RIGHT)) {
if (!col_blocked(x + SPEED)) x += SPEED;
}
y = KIDY - jump_height[jump_t];
jump_t++;
if (jump_t >= JUMP_FRAMES) {
jumping = 0;
y = KIDY;
}
} else if (crouching) {
if (!kbd_raw_down(KBD_DOWN))
crouching = 0;
} else if (kbd_raw_down(KBD_UP)) {
jumping = 1;
jump_t = 0;
} else if (kbd_raw_down(KBD_DOWN)) {
crouching = 1;
} else if (kbd_raw_down(KBD_LEFT)) {
if (!col_blocked(x - SPEED)) x -= SPEED;
set_facing(DIR_LEFT);
moving = 1;
} else if (kbd_raw_down(KBD_RIGHT)) {
if (!col_blocked(x + SPEED)) x += SPEED;
set_facing(DIR_RIGHT);
moving = 1;
}
if (!moving && !jumping && (sprite_anim_status(&kid) & SPR_ANIM_ON))
sprite_anim_stop(&kid, (int8_t)(facing * 3));
sprite_move(&kid, x, y);
if (kbd_raw_down(KBD_ESC))
break;
hidden = gfx_get_visible_page() ^ 1;
gfx_set_draw_page(hidden);
show_state(jumping, crouching);
sprite_update(&kid, 1);
gfx_wait_vsync();
gfx_set_visible_page(hidden);
}
kbd_raw_close();
closegraph();
tile_atlas_free();
atlas_free(&at);
return 0;
}
Binary file not shown.
@@ -0,0 +1,34 @@
/* pop_bg_atlas.h — раскладка атласов статического фона PoP.
* Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.
*
* Прямая адресация (ноль remap-таблиц в W2):
* ENV фон id N -> atlas env_bg[N>>5], idx N&31
* WALL id N -> atlas wall, idx N
* FORE id N -> atlas fore, idx N
*/
#ifndef POP_BG_ATLAS_H
#define POP_BG_ATLAS_H
#define POP_ENV_SHIFT 5
#define POP_ENV_MASK 31
#define POP_ENV_PAGES 5
/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */
#define POP_PAL_ENV 0x50
#define POP_PAL_WALL 0x60
/* Имена файлов атласов (грузятся atlas_load). */
static const char *const pop_env_atl[POP_ENV_PAGES] = {
"pop_env0.atl",
"pop_env1.atl",
"pop_env2.atl",
"pop_env3.atl",
"pop_env4.atl",
};
#define POP_WALL_ATL "pop_wall.atl"
#define POP_FORE_ATL "pop_fore.atl"
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */
#define POP_PAL_POT 0x40
#define POP_BG_PAL "pop_bg.pal"
#endif
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41
View File
@@ -0,0 +1,41 @@
/* kid_atlas.h — раскладка атласов Kid. Сгенерировано pop_pack_kid.py. */
#ifndef KID_ATLAS_H
#define KID_ATLAS_H
#define KID_SHIFT 3
#define KID_MASK 7
#define KID_PAGES 28
#define KID_PAL 0x70
static const char *const kid_atl[KID_PAGES] = {
"kid0.atl",
"kid1.atl",
"kid2.atl",
"kid3.atl",
"kid4.atl",
"kid5.atl",
"kid6.atl",
"kid7.atl",
"kid8.atl",
"kid9.atl",
"kid10.atl",
"kid11.atl",
"kid12.atl",
"kid13.atl",
"kid14.atl",
"kid15.atl",
"kid16.atl",
"kid17.atl",
"kid18.atl",
"kid19.atl",
"kid20.atl",
"kid21.atl",
"kid22.atl",
"kid23.atl",
"kid24.atl",
"kid25.atl",
"kid26.atl",
"kid27.atl",
};
#define KID_PAL_FILE "kid.pal"
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
#define KID_SWORD_ID0 20 /* индекс в атласе = sword_tbl.id - ID0 */
#endif
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Some files were not shown because too many files have changed in this diff Show More