17 Commits

Author SHA1 Message Date
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
105 changed files with 11941 additions and 320 deletions
+13 -1
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.
@@ -85,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).
+12 -2
View File
@@ -8,8 +8,18 @@
(например: смещение+размер последней записи таблицы точно совпадает с
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
Для справки при последующей реализации (порт на ZX Sprinter) стоит держать в
уме два внешних проекта:
**Основной источник спецификации формата — `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`. Содержит
File diff suppressed because it is too large Load Diff
+8
View File
@@ -1,5 +1,13 @@
# Форматы ресурсов 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, разобрав, как устроены ресурсы игры в двух доступных нам
версиях:
+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`.
+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 в комнату сверху) — ещё не сделан; можно закрыть до
объектов или параллельно.
+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 (лишние блиты, которых нет в оригинале).
Binary file not shown.
@@ -27,6 +27,8 @@ static const char *const pop_env_atl[POP_ENV_PAGES] = {
};
#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.
+67
View File
@@ -0,0 +1,67 @@
# roomtest — правила подпроекта
Живой прототип порта PoP: комната 1 уровня 1 (композиция тайлов в рантайме)
+ Kid (анимация seqtbl, управление, коллизия, падение, зацеп, fore-окклюзия,
проваливающиеся полы). Действуют правила корневого `CLAUDE.md` и
`applications/PoP/CLAUDE.md`.
**Главное правило (из `../CLAUDE.md`): `../SDLPoP/src/` — источник истины.**
Перед реализацией ЛЮБОЙ функции и по ЛЮБОМУ вопросу «как в оригинале» —
СНАЧАЛА прочитай соответствующий код SDLPoP и портируй по нему; не пиши
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
по умолчанию баг у нас.
## Сборка и запуск
```
make # собрать roomtest.exe (упаковав ассеты через toolchain/)
make run # exe + EXTRA_DATA на дискету + запуск MAME (см. корневой док)
```
`MEMORY=small`, `--gfx 256`. Ассеты (`pop_env0..4.atl`, `pop_wall.atl`,
`pop_fore.atl`, `kid0..27.atl`, `kid.pal`) генерируются python-скриптами
`../toolchain/` — Makefile дёргает их сам при изменении. Данные комнаты —
`room1_data.h` (fg=foretable code, bg=backtable modifier); данные анимации
Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
## Проверка в MAME
Только через `toolchain/mame_interactive.py` или MCP-мост `mame-z80`
(см. memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
Эталон комнаты — `../toolchain/1.1-2.png`. Пересобрал HDD-образ → MAME
ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
**ESC** — выход.
## Модули
| Файл | Роль (порт SDLPoP) |
|------|--------------------|
| `roomtest.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), tick→draw порядок слоёв. |
| `pop_bg.c/.h` | Отрисовка фона композицией тайлов (`seg008` draw_tile/wall_pattern), fore-окклюзия поверх Kid (`seg003` redraw_at_char), loose-полы (shake/bake/mob, `seg007/008`). |
| `pop_kid.c/.h` | Анимация/движение Kid: интерпретатор seqtbl `play_seq` + frame_table (`seg006`). |
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
## Порядок слоёв в кадре (важно для окклюзии)
`roomtest.c` каждый тик рисует в СКРЫТУЮ страницу: `kid_heal` (стереть
прошлый кадр) → `kid_tick``pop_phys_tick``pop_loose_tick` (loose СЛОЙ
ФОНА — ДО Kid, чтобы Kid был поверх плиты) → `kid_draw``pop_fore_over_kid`
(передние грани тайлов поверх Kid) → `pop_room_clip_borders` (чистка бортов,
гейт по флагу — только в кадрах падения) → флип на vsync.
**Дабл-буфер:** у каждой страницы своя видео-ОЗУ и теневая ОЗУ-копия; heal
берёт чистый фон из копии ТОЙ страницы, в которую рисуем. Любой динамический
элемент (Kid, loose-плита, падающий кусок) обязан чистить свой прошлый кадр
на КАЖДОЙ из двух страниц — иначе остаток виден через кадр как **мерцание**.
Это типовой источник багов «остаётся кусочек» (см. memory
`pop_fall_debug_baseline`).
## Ключевые memory
`pop_kid_plan`, `pop_hang_state`, `pop_fore_layer`, `pop_fall_debug_baseline`,
`accel_vertical_copy`, `kbd_raw_fifo_drain`, `pop_check_sdlpop_first`.
+8 -5
View File
@@ -6,21 +6,24 @@ EXAMPLE := roomtest
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
MEMORY ?= small
EXTRA_FLAGS ?= --gfx 256
EXTRA_SRCS := pop_bg.c pop_kid.c pop_ctrl.c pop_map.c
EXTRA_FLAGS ?= --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c
EXTRA_SRCS := pop_kid.c pop_ctrl.c pop_level.c
BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
LVL_DATA := $(LVL_DIR)/res2001.bin
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_wall.atl $(BG_DIR)/pop_fore.atl \
$(BG_DIR)/pop_pot.atl
# ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23)
# не рисуются (kid_draw пропускает незагруженную страницу). Палитра —
# ЕДИНАЯ игровая (env+wall+kid).
KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl)
KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal
EXTRA_DATA := $(BG_DATA) $(KID_DATA)
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(LVL_DATA)
include $(PROJ_ROOT)/app.mk
TC := $(PROJ_ROOT)/applications/PoP/toolchain
@@ -31,4 +34,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
kid_data.h: $(TC)/pop_extract_kid_data.py
cd $(TC) && python3 pop_extract_kid_data.py
$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h kid_data.h room1_data.h $(BG_DATA) $(KID_DATA)
$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(LVL_DATA)
+47
View File
@@ -0,0 +1,47 @@
# roomtest
Живой прототип порта Prince of Persia: комната 1 уровня 1 отрисовывается
реальной композицией слоёв-тайлов, по ней бегает и прыгает Kid под
управлением с клавиатуры. Служит стендом для переноса механик движка
(анимация, коллизия, падение, зацеп, окклюзия, проваливающиеся полы) до их
выделения в переиспользуемый код.
Правила разработки и порядок слоёв рендера — [`CLAUDE.md`](CLAUDE.md).
## Быстрый старт
```
make # собрать roomtest.exe (ассеты упакуются автоматически)
make run # собрать, упаковать на дискету и запустить в MAME
```
Клавиши в запущенном тесте: ←/→ бег, ↓ присед, ↑+→ прыжок, Shift шаг,
ESC выход.
**Отладка рисования:**
- **SPACE** — тумблер дабл-буфера (в однобуфере баги рисования видны без
мерцания-через-кадр).
- **1** — заморозить кадр (`frozen`: tick/draw/flip пропускаются, картинка
стоит), **2** — снять заморозку и продолжить. Приём для разбора
транзиентных артефактов (тряска/падение loose, поза Kid): поймать
проблемный кадр на `1`, спокойно рассмотреть/снять пиксели — в т.ч.
удалённо из MAME MCP-моста, пока анимация стоит.
Эталон вида комнаты — `../toolchain/1.1-2.png`.
## Модули
- `roomtest.c` — главный цикл (дабл-буфер, порядок tick/draw).
- `pop_bg.c` — фон комнаты композицией тайлов + fore-окклюзия + loose-полы.
- `pop_kid.c` — анимация/движение Kid (интерпретатор seqtbl).
- `pop_ctrl.c` — управление с клавиатуры (диспетчер control).
- `pop_map.c` — коллизия с картой, физика падения, loose-полы.
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
(оба генерируются скриптами `../toolchain/`).
## Статус
Готово и проверено в MAME: статический фон комнаты 1; Kid — анимация,
управление, коллизия/падение, зацеп/подтягивание/спуск, fore-окклюзия
пола/стены над Kid. В работе: проваливающиеся полы (loose floors) — см.
`../docs/loose_floors_plan.md`.
+434
View File
@@ -0,0 +1,434 @@
# roomtest — список известных багов (Фаза B и смежное)
Найдено при тестировании Фазы B (кнопки/ворота) в MAME. **НЕ решаем сейчас**
— задача следующего этапа. Правило проекта: механику сверять с
`applications/PoP/SDLPoP/src/` ДО кодинга.
Статус на момент записи: 2026-07-21. Тестовый старт — комната 6 (`START_ROOM`
в `roomtest.c`). Кнопка room6(0,2, opener) → ворота room8(0,9), видны в левом
шве room6.
---
## BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — **Critical**
**Симптом:** при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов)
Kid оказывается на ряду **row 1** вместо row 0 — «проваливается» на этаж ниже
(стоит в дверном проёме на неправильной высоте).
**Где:** `pop_map.c` `check_leave()` (порт seg002 leave_room) — боковой уход
`pop_leave_dir`; репроекция координат `Kid.x ∓= 140` есть, но Y/`curr_row`
при боковом переходе НЕ репроецируются (в отличие от `check_leave_below`,
где `Kid.y -= 189; curr_row = y_to_row`). При входе в соседнюю комнату у
шва Kid, видимо, попадает в позицию, где `determine_col`/физика следующего
кадра сбрасывают его на row 1 (нет пола на row 0 у входа, либо неверный y).
**Сверить с SDLPoP:** `goto_other_room` (seg002) — как именно
пересчитываются `Char.curr_row`/`Char.y` при side-переходе (left/right).
Для вертикального перехода мы это сделали, для бокового — нет.
## BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — **Critical**
**Симптом:** после перехода в соседнюю комнату попытка сразу вернуться
обратно приводит к тому, что Kid снова закидывается в ту же комнату —
переход «пинг-понгует», выйти нельзя.
**Где:** `pop_map.c` `check_leave()`. После перехода Kid репроецирован к
ПРОТИВОПОЛОЖНОЙ кромке новой комнаты (`x ∓ 140`). На СЛЕДУЮЩЕМ же кадре
передний край снова за порогом ухода в обратную сторону → `pop_leave_dir`
взводится опять → мгновенный обратный переход. Нужен guard «только что
перешёл» (подавить повторный leave на N кадров / пока Kid не отойдёт от
кромки), как в оригинале (`next_room`/leave обрабатываются раз за кадр +
Kid входит достаточно внутрь).
**Сверить с SDLPoP:** порядок `leave_room`/`goto_other_room` + на какой
X ставится персонаж при входе (должен быть НЕ на самой кромке, иначе
мгновенный обратный триггер).
## BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия (Kid поверх кнопки) — **Critical**
**Симптом:** когда Kid подтягивается (climb-up) на тайл, верх которого —
КНОПКА (opener/closer), а не обычный пол, отрисовка неверная: Kid рисуется
ПОВЕРХ кнопки, хотя передняя грань пола/кнопки должна перекрывать нижнюю
часть Kid во время подъёма.
**Где:** `pop_bg.c` `climb_overlay_tile()` — floor-overlay при подъёме
рисуется только для floor-подобных тайлов `code ∈ {1,3,5,19}`. Кнопка
(opener 0x0F / closer 0x06) в этот набор НЕ входит → пол-оверлей кнопки не
рисуется поверх Kid. Нужно: для нажатой кнопки (draw как floor/stuck)
трактовать её как floor-тайл в climb-overlay (учесть подстановку из
`get_tile_to_draw`), и/или добавить opener/closer в набор.
**Сверить с SDLPoP:** `draw_floor_overlay` (seg008) + `get_tile_to_draw`
(нажатая кнопка → tiles_1_floor / tiles_5_stuck) — оверлей должен идти по
ПОДСТАВЛЕННОМУ типу тайла.
---
## Оптимизации (отложено)
### OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22
**Возможность:** `pop_room_redraw_seam_left()` (pop_bg.c) на каждое изменение
openness рисует `bar(BLACK)` по всему col0 + **полный `draw_tile(0,0)`**
(стены, `topright`, `wall_pattern` с `prandom()` — десятки блитов). Реально
анимируются только бары решётки — `draw_gate_back` (~9 `env_b`).
**Стоимость:** seam-блок (синий io_border) занимает ~30-50% кадрового периода,
**но только пока openness меняется** — т.е. во время открытия и медленного
авто-закрытия (~5 сек после схода с кнопки). В покое (ворота открыты-держим
кнопку / закрыты / FF) — 0%. Замерено в MAME: брейк на
`_pop_room_redraw_seam_left` (0x5A54) срабатывает ⟺ сегмент дорогой.
**Приём** (как у пик, но сложнее — heal НЕ годится: печёные бары устаревшие,
поэтому и стоит `bar(BLACK)`+redraw): `bar(BLACK)` только по полосе баров
(x=0, gate_top..gate_bot) + `draw_gate_back(lmod,...)` + дорисовать статику
тайла (0,0), задетую полосой. Ожидаемо срезает бóльшую часть 30-50%.
**Пиксель-чувствительно** — полоса баров пересекается со статикой col0,
обязательна выверка в MAME по кромкам.
**Решение (2026-07-22):** оставляем как есть — стоимость транзиентная, в бюджет
помещаемся. Делать, если упрёмся в кадровый бюджет на сценах с воротами.
---
## Исправлено
- **Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука
срезана до одного пикселя** — ИСПРАВЛЕНО 2026-07-28. Две причины:
(а) не был портирован `clip_char()` (seg006:1749) — верхняя обрезка спрайта по
`y_clip[curr_row+1]`, когда тайл над головой стена/пол; сделано
(`pop_clip_char_top` в pop_map.c + `gfx_blit_cols_part` в libbgi, heal чистит
уже обрезанный прямоугольник);
(б) `climb_overlay_tile` выбирал ветку `draw_floor_overlay` (seg008:1E3A) по
СЫРОМУ коду тайла — а нажатая кнопка в `get_tile_to_draw` (seg008:240)
подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в
`draw_other_overlay` и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки
`floor_left_overlay[frame-137]`. Фикс — `tile_code_drawn()` (одна подстановка
на все слои). Проверено покадрово в MAME (кадры 148..138).
- **Шов ворот жёг 50% кадра в покое (закрытая решётка)** — ИСПРАВЛЕНО
2026-07-22. Причина — **баг кодогенератора SDCC z80** (memory
`sdcc_z80_cmp_store_a_bug`): `if (m[9] != seam_sig) seam_sig = m[9];`
компилировался в `sub (seam_sig)` (A ← разность) + `ld (seam_sig),a`
сохранял РАЗНОСТЬ `m[9]-seam_sig`, не `m[9]`. `seam_sig` осциллировала
(напр. 25↔231), `m[9]!=seam_sig` истинно каждый кадр → `draw_tile(0,0)`
каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения
(`seam_sig = g;` из чистого `g` ДО `sub`), подтверждён в .asm. Прочёс всех
модулей PoP: других случайных compare-then-store нет.
- **Пики: 2×полный `draw_tile` на кадр анимации** → хирургический редрой,
2026-07-22. `pop_spike_redraw`: `heal_off` уже возвращает всю печёную
статику (база 127, пол, грани); поверх анимируются только два острия
(`SPIKES_FRAM_LEFT` в своей ячейке + `SPIKES_FRAM_RIGHT` в соседней).
Заменили 2×`draw_tile` на 2×`env_b` — пиксель-в-пиксель тот же результат,
в разы дешевле (шахта из нескольких пик больше не съедает полный кадр).
- **Отрисовка нажатой кнопки (0,2)/(0,3)** — ИСПРАВЛЕНО. Причина: `fore_tile`
(pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id
149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки».
Фикс: `fore_tile` применяет ту же подстановку нажатой кнопки, что и
`draw_tile` (opener→floor / closer→stuck при таймере связи >1). Плюс
`pop_button_redraw` — wipe своей ячейки + правой грани (дальний угол в
0,3), низ строго yb+64 (не залезать в стену ряда 1).
## Заметки
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
сторону = разворот (как в оригинале safe_step), движение со второго.
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`).
- `START_ROOM=6` — временный тестовый старт; вернуть на
`pop_level_start_room()` после отладки объектов.
---
## BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **Medium**
**Симптом (2026-07-22):** при прыжке вверх (SEQ up, кадры 67..79) руки/голова
Kid заходят в полосу кладки у потолка (row -1) и рисуются ПОВЕРХ неё — потолок
должен окклюдить руки (как передняя грань).
**Где:** `pop_bg.c` — полоса потолка (`draw_tile(-1)`) сейчас только фон
(запечена в pop_room_draw), не окклюдер поверх Kid. Нужен ceiling-overlay в
`pop_fore_over_kid` (перерисовать срез полосы над Kid поверх спрайта), по
аналогии с fore-тайлами. Инфраструктура `g_clip_top`/`gfx_blit_part` уже есть.
## BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row -1) не работает — **Medium**
**Симптом (2026-07-22):** loose-плита в ряду 2 верхнего соседа (room5 (2,5) →
потолок room6 над (0,5)) при прыжках Kid на (0,5) НЕ трясётся и не разбивается.
**Где:** верхний сосед не «живой» — `above_fg` берётся из статики уровня,
loose-состояние соседней комнаты не тянем (per-room loose/modif не персистятся,
P0). check_press/knock работают только по текущей комнате. Нужно: (а) пробой
loose сверху через границу комнаты, (б) персистентное loose-состояние соседа +
его отрисовка в полосе. Пока полоса рисует loose статически (кадр покоя).
## BUG-OCCL-1. Тень дальней колонны перекрывает Kid в (0,4)-(0,5) — **Medium**
**Симптом (2026-07-22):** Kid, стоя/прыгая у (0,4)-(0,5), частично перекрыт
тёмной штриховкой — это ТЕНЬ/боковая грань ДАЛЬНЕЙ колонны (сзади по глубине),
которая НЕ должна окклюдить персонажа. Over-occlusion fore-слоя.
**Где:** `pop_bg.c` `pop_fore_over_kid`/`fore_tile` — рисует fore футпринт-тайлов
без учёта глубины (какая грань РЕАЛЬНО перед Kid). Порт seg003 redraw_at_char
окклюдит только передние грани; дальняя колонна (за Kid) не должна. Сверить с
SDLPoP redraw_at_char (какие тайлы/грани идут в fore vs back по y-сортировке).
## BUG-CEIL-3. Анимация ворот стирает потолок над ними — **Medium**
**Симптом (2026-07-22):** при опускании/поднятии решётки шва (анимация openness)
полоса кладки у потолка НАД воротами пропадает (см. потолок над левым швом).
**Где:** `pop_bg.c` `pop_room_redraw_seam_left``pop_button_redraw`/
`pop_spike_redraw` — общий класс): чёрный `bar` по col0 row0 стирает потолочную
полосу (row-1), а redraw рисует только сам гейт, не восстанавливая полосу.
Нужно: при перерисовке шва/тайла у потолка перерисовать и срез потолочной полосы
(draw_tile(-1,col) с g_clip_top) над затронутыми колонками.
---
# BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22**
Статус: 2026-07-22, вечер. НАСТОЯЩИЙ корень найден потиковой трассой ЖИВОГО
SDLPoP 1.23 (lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char +
char_x_left/right; fixes выключены = vanilla). Все прежние гипотезы ниже
оставлены для истории — они НЕ были причиной.
## КОРЕНЬ (подтверждён трассой + исходником)
`set_char_collision` (seg006:0723): `char_x_right = obj_x/2 + 58`, где
`load_frame_to_obj` (seg008:1728) считает `obj_x = 2*char_dx_forward(dx) - 116`
и **добавляет +1** для кадров «чётного пикселя»:
`if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x;`
(бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит).
Деление `obj_x/2` — C-усечение К НУЛЮ, поэтому при `e = x+dx <= 57`
(obj_x < 0, зона левого шва) поправка +1 даёт `char_x_right = e+1`, а при
e >= 58 формула сокращается к чистому `e`.
Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 —
бит 0x80 не стоит), имеет **char_x_right = 58** и порога leave-left (<=57)
НЕ достигает. Наш движок считал передний край как `Kid.x + dx` без поправки
→ 57 → ложный leave → пинг-понг.
Эталонный цикл SDLPoP (сн normalized по трассе): стойка x=61 → тап вправо →
safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61 →
seq47 dx(-4) → **x=57** (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58, у
всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ срабатывает;
тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61 (2/3 видно); тап →
bump → 57 … по кругу. Char.room и drawn_room НЕ меняются вообще.
## Фикс (pop_map.c)
`char_x_forward_edge()`: `e = char_dx_forward(dx); if (((flags ^ (dir<0 ?
0x80 : 0)) & 0x80) == 0 && e <= 57) e++;` — используется в `char_front_coll`
(коллизия/bump/edge_distance) и в `check_leave` (порог ухода). Плюс порт
doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого
выхода нет). `pop_leave_timer` (exit_room_timer) оставлен — он реален в
seg002/seg003.
## Гипотезы, ОТПАВШИЕ после трассы (история диагноза)
## Симптом (подтверждён пользователем + инструментально)
Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке
room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя
состояниями:
- **A**: показывается room6, Kid у левой кромки за решёткой (спрайт виден на 2/3);
- **B**: показывается room8, Kid у его правой кромки.
Эталон SDLPoP (описан пользователем): drawn_room **всегда остаётся room6**,
Kid осциллирует у кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ
переключается. То есть у нас лишний свитч A→B.
## Тестовый контекст
START_ROOM=6. Ворота room8(0,9) закрыты, видны в col0 room6. Kid-структура
kframe: см. pop_kid.h (frame,x,y,direction,curr_col,curr_row,action,...).
Адрес _Kid и статиков — в .sprinter-cc-roomtest/roomtest.map (сдвигается от
сборки к сборке). MAME: memory `mame_mcp_bridge`/`mame_hdd_test_disk`.
Отладочные: J/L — короткий шаг влево/вправо (эмуляция Shift+стрелка).
## Инструментальный диагноз (watchpoint на pop_leave_dir)
В момент лишнего свитча A→B: `pop_leave_dir=1 (LEFT)`, Kid **frame=15 (СТОЯ,
не transient!), Kid.x=57** (до репроекции +140). То есть:
- `char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57`.
- Порог leave-left (взгляд вправо): `char_x_right <= 57` → срабатывает РОВНО на 57.
- Грань ворот (где их держит коллизия) = **61** (`wall_dist_from_left[1]=10 +
coll_tile_left_xpos=51`). Между 57 и 61 — **зазор 4px**: Kid НЕ удержан
воротами (d=6157=4≥0 → check_bumped не бампит), но уже на пороге ухода.
- Kid оседает на 57 из-за recoil отскока: seq_47 = `act(bumped), dx(-4),
frame_50, 51, 52`; SEQ_DX(4) двигает Char.x на −4 суммарно; frame_50.dx=4
компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку
(frame15, dx=0) `char_x_right = Char.x = aligned4 = 57`.
## Что ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)
- Формула char_x: `char_x_right = obj_x/2+58 = Char.x+frame.dx` (seg006
set_char_collision) — совпадает с нашим char_dx_forward.
- Позиция грани ворот: `get_left_wall_xpos = wall_dist_from_left[1](10) +
xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184140)+7 = 61` — совпадает с нашим
(`x_bump[1+5]=44`, +7, +10 = 61).
- Порог leave: SDLPoP leave_room looking-right `char_x_right<=57` — совпадает.
- Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15
(image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
- seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.
## Почему точечные фиксы НЕ работают
- Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА,
не SDLPoP (в leave_room такого нет); откачено.
- `exit_room_timer=2` (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как
pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасает:
положение Char.x=57 **устойчивое** (Kid стоит), а не transient — после
истечения таймера leave срабатывает.
- По коду SDLPoP raw-рассуждением раз за разом выходит, что leave-left ДОЛЖЕН
сработать и там (Char.x=57), но эталон показывает, что нет. Значит различие
не в этих кусках, а в ДИНАМИКЕ: где именно оседает Char.x у шва И как
вычисляется drawn_room.
## КОРЕНЬ (гипотеза)
Наш движок держит `drawn_room == комната_Kid` ВСЕГДА, а шов подделан:
- коллизия — самодельные снапшоты кромок соседа (g_lcol/g_rcol, 1 колонка) +
`x_bump[tcol=-1]`, а НЕ порт `curr_row_coll_room[]`/`get_all_coll_row`/
`xpos_in_drawn_room` из seg004;
- уход — мгновенный `enter_room(сосед)` при пороге char_x, а НЕ отложенный
drawn_room через `next_room`/`check_the_end`.
Из-за упрощённой коллизии Char.x у шва оседает иначе, чем в SDLPoP (у нас ровно
57 = порог), и/или drawn_room переключается там, где в оригинале нет.
## План S2/S3 (порт coll_room + отложенный drawn_room) — НЕ потребовался для
## этого бага (трасса показала: в эталоне Char.room/drawn_room вообще не
## меняются). Остаётся как задел под стражей/двух персонажей в кадре.
Правило проекта: SDLPoP — источник истины, переводить 1:1, НЕ рассуждать «как
должно быть». Нужно портировать связку целиком:
1. **Раздельные комнаты.** `Char.room` (реальная комната персонажа) ≠
`drawn_room` (отрисованная). У нас уже есть `kid_room` (S1) + рендер-смещение
`pop_kid_set_render_dx(∓140)` (порт xpos_in_drawn_room для СПРАЙТА).
2. **Коллизия по Char.room через coll_room (seg004, S2).** Портировать
`check_collisions` → `get_row_collision_data` (строки curr/below/above) →
`get_left_wall_xpos`/`get_right_wall_xpos` (по `xpos_in_drawn_room(x_bump[
col+FIRST_ONSCREEN_COLUMN])`, ±140 когда колонка принадлежит соседу) →
массивы `curr_row_coll_room[]`/`_flags[]`, `bump_col_left_of_wall`/
`bump_col_right_of_wall` → `check_bumped_look_left/right` → `bumped()`.
Тайлы берутся из РЕАЛЬНОЙ комнаты колонки (get_tile(room,col,row) для любой
комнаты), НЕ из снапшота. Именно это определяет, где Char.x оседает у шва.
3. **Отрисовка по drawn_room, персонаж со сдвигом.** Рисуем drawn_room; Kid
(и будущие стражи) — со смещением ±140, если их комната ≠ drawn_room.
4. **Отложенная смена drawn_room (seg002/seg000, S3).** Уход =
`leave_room` (seg002:0504, ПОРТ ЦЕЛИКОМ, включая up/down/side, guard'ы
climb/standup/turn, doortop-check для right) → `goto_other_room`
(seg002:0486: Char.room=сосед, x±140/y±189). `exit_room` (seg002:03C7):
`exit_room_timer` + `next_room = Char.room`. `check_the_end` (seg000:0FBD):
`if (next_room != 0 && next_room != drawn_room) drawn_room = next_room;
redraw`. drawn_room меняется ТОЛЬКО так, а не по нашему прямому enter_room.
Ключевой момент, который надо переводить БУКВАЛЬНО (а не выводить): порядок
вызовов в play_frame (seg000:863 `play_frame`: … play_kid_frame → exit_room →
check_the_end) + set_char_collision перед leave_room в exit_room.
Карта сегментов: seg002 (leave/goto/exit_room), seg004 (коллизия/coll_room/
bumped), seg006 (set_char_collision/play_seq/frame_table_kid), seg000
(play_frame/check_the_end/draw). Наши файлы: pop_map.c (коллизия/физика/leave),
roomtest.c (enter_room/drawn_room/главный цикл), pop_kid.c (кадры/рендер-сдвиг).
Реализовано на 2026-07-22: S1 (kid_room + render_dx) + pop_leave_timer.
S2/S3 отложены — баг закрыт odd-pixel поправкой (см. КОРЕНЬ выше).
Память: [[pop_seam_room_model]], [[sdlpop_odd_pixel_char_x]].
---
# TODO: оптимизация отрисовки (записано 2026-07-29)
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
кадр), а у нас каждая такая пометка превращается в реальный heal (копию из
ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем
безусловно» корректен, но дорог.
## T-1. Пики: перерисовывать по причине, а не безусловно
**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
каждой живой пики в комнате (порт `redraw_21h`, который `animate_spike`
вызывает вне всяких `if`). Это корректно, но лишнее для пик, до которых
Киду дела нет.
**Надо:** перерисовывать тайл пики, только если
1. **сменился её видимый кадр** (шаг выдвижения/уборки), ЛИБО
2. **её кто-то стёр** — а стереть у нас может только heal, то есть тайл
попал в прямоугольник `kid_heal` этого кадра.
Это и есть модель оригинала, просто выраженная флагами: `redraw_at_char`
(seg003:0576) каждый кадр помечает `set_redraw_fore` тайлы персонажа, причём
**объединение текущего и предыдущего** прямоугольника
(`MIN(char_top_row, prev_char_top_row)` и т.д.), а `animate_spike` помечает
свой тайл. Итог = {тайл сменил кадр} ∪ {тайлы Кида}.
**Как:** слой Кида и так считает `cL..cR`/`rT..rB` в `pop_fore_over_kid` —
пусть публикует их (плюс предыдущие, как в оригинале), а цикл trob'ов
сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений
прямоугольников (см. память `manual_hints_over_auto_detect`).
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит
**Сейчас:** `kid_heal` → `kid_draw` → `pop_fore_over_kid` идут каждый кадр,
даже когда Kid стоит и в его тайлах ничего не меняется. Это ровно поведение
оригинала (`draw_game_frame`, seg000:917 — `draw_moving()` + `draw_tables()`
безусловно), но у него это дёшево, а у нас нет.
**Надо:** пропускать heal+draw Кида, когда кадр/поза/координаты не менялись
и в его тайлах нет активной анимации.
**Осторожно (дабл-буфер):** пропускать можно **не раньше второго подряд**
неизменного кадра — иначе одна из двух страниц останется со старым
содержимым. Условие «обе страницы уже получили это состояние».
**Связь с T-1:** после T-1 idle-skip не требует ничего дополнительного для
пик: раз Кида не перерисовываем — heal'а нет — стирать пики нечем — редрой
не нужен, он отпадает сам.
---
# Обход всех 24 комнат уровня 1 — поиск багов отрисовки
Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо
`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой),
Kid ставится на первый пол, `pop_trob_reset()` возвращает пики/ворота в
исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа
единицы (`||` `||||` = 24). Убирается закомментированием одной строки.
| комната | статус | что не так |
|---------|--------|------------|
| 1 | | |
| 2 | | |
| 3 | | |
| 4 | | |
| 5 | | |
| 6 | | |
| 7 | | |
| 8 | | |
| 9 | | |
| 10 | | |
| 11 | | |
| 12 | | |
| 13 | | |
| 14 | | |
| 15 | | |
| 16 | | |
| 17 | | |
| 18 | | |
| 19 | | |
| 20 | | |
| 21 | | |
| 22 | | |
| 23 | | |
| 24 | | |
File diff suppressed because it is too large Load Diff
+92
View File
@@ -13,6 +13,18 @@
#include <stdint.h>
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
* POP_YOFF = (256-200)/2 = 28px (чёрные борта сверху/снизу). Внутри 200px
* верхние POP_PLAYFIELD_H = 192 — поле тайлов/спрайтов (комната = 3 ряда),
* нижние 8 — статус-полоса (жизни/сообщения): тайлы/спрайты туда НЕ лезут.
* Сдвиг применяется на границе примитивов (blit/heal) — логика рисования
* остаётся в «room-space» y=0..191. */
#define POP_YOFF 28
#define POP_PLAYFIELD_H 192
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
@@ -29,6 +41,16 @@ void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg,
const uint8_t *lcol_fg, const uint8_t *lcol_bg);
/* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright,
* когда тайл нижнего ряда стал empty (после падения loose): «стена вниз»
* рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */
void pop_room_set_below(const uint8_t *below_row0_fg);
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
* комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod);
/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него —
* звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
* спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики
@@ -36,4 +58,74 @@ void pop_room_draw(uint8_t room_num,
void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir, uint8_t frame, uint8_t action);
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
* прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице
* дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */
void pop_fore_heal(void);
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
* после падения (NORMAL, в ОЗУ-копию). */
void pop_loose_shake_draw(int row, int col);
void pop_loose_bake_empty(int row, int col);
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
* Состояние — pop_ceil_modif[10] в pop_map. */
void pop_ceil_shake_draw(int col);
void pop_ceil_bake_empty(int col);
/* Кусок приземлился (loose_land): 0 = нет, иначе tilepos+1 тайла, на который
* он лёг. Ставит pop_loose_mob_tick, разбирает pop_loose_tick (debris+knock). */
extern uint8_t pop_loose_landed;
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
void pop_spike_redraw(int row, int col);
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
* (пламя рисуется в ячейке правого соседа). */
void pop_torch_draw(int row, int col, uint8_t modif);
void pop_potion_draw(int row, int col, uint8_t modif);
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
void pop_room_redraw_seam_left(void);
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
* кнопка нажата/отжата, floor → debris от упавшей плиты. */
void pop_floor_bake(int row, int col);
void pop_button_redraw(int row, int col); /* = pop_floor_bake (кнопка) */
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col);
void pop_loose_mob_tick(void);
void pop_loose_mob_reset(void); /* остановить кусок (смена комнаты) */
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после kid_draw, перед fore-слоем. */
void pop_loose_mob_draw_over(void);
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
uint8_t pop_loose_mob_pos(int *col, int *y);
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
void pop_clip_sprite(int x, int w, int sy_top, int h);
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
* КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */
void pop_room_clip_borders(void);
#endif
+22 -1
View File
@@ -20,6 +20,12 @@
#include "pop_kid.h"
#include "pop_map.h"
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
* scancodes. TODO: убрать после отладки. */
#define KBD_DBG_STEPL 0x3B /* J */
#define KBD_DBG_STEPR 0x4B /* L */
/* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */
#define CONTROL_RELEASED 0
#define CONTROL_IGNORE 1
@@ -291,6 +297,16 @@ static void control_jumpup(void)
static void control(void)
{
uint8_t f = Kid.frame;
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
* пару пикселей меньше, чем должен»). */
if (Kid.action == 5 || Kid.action == 4) { /* bumped / in_freefall */
release_arrows();
return;
}
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
control_standing();
else if (f == FRAME_48_TURN)
@@ -314,9 +330,14 @@ static void read_input(void)
uint8_t r = kbd_raw_down(KBD_RIGHT);
uint8_t u = kbd_raw_down(KBD_UP);
uint8_t d = kbd_raw_down(KBD_DOWN);
/* Отладочный осторожный шаг: J/L = Shift+влево/вправо (см. KBD_DBG_STEP*). */
uint8_t sl = kbd_raw_down(KBD_DBG_STEPL);
uint8_t sr = kbd_raw_down(KBD_DBG_STEPR);
if (sl) l = 1;
if (sr) r = 1;
control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED);
control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED);
control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
control_shift = (sl || sr || kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
? CONTROL_HELD : CONTROL_RELEASED;
}
+92 -6
View File
@@ -6,10 +6,16 @@
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "kid_data.h"
kid_t Kid;
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
int8_t knock;
/* Координатные таблицы (data.h). */
static const uint8_t x_bump[20] = {
(uint8_t)-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
@@ -108,7 +114,11 @@ void play_seq(void)
Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++;
break;
case 0xF5: case 0xF4: /* KNOCK up/down */
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
knock = 1;
break;
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
knock = -1;
break;
case 0xF2: /* SOUND — потребить 1 байт */
Kid.curr_seq++;
@@ -176,17 +186,73 @@ static uint8_t kid_lvalid[2];
* obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
static int kid_fp_x, kid_fp_y;
static uint16_t kid_fp_w, kid_fp_h;
/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room):
* ∓140 когда комната Kid ≠ отрисованной; 0 обычно. */
static int kid_render_dx;
void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; }
int kid_fp_obj_x(void) { return kid_fp_x; }
int kid_fp_obj_y(void) { return kid_fp_y; }
uint16_t kid_fp_width(void) { return kid_fp_w; }
uint16_t kid_fp_height(void) { return kid_fp_h; }
/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт
* chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27).
* Позиция от кадра Kid: obj_y = 15, obj_x += 5 ВПЕРЁД (obj_dx_forward).
* Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают
* иначе — тут только общий случай + «на пиках» (порт по мере надобности).
* Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */
#define KID_SPLASH_IMAGE 218
void kid_draw_splash(void)
{
int obj_x = kid_fp_x, obj_y = kid_fp_y - 15;
uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7;
const uint8_t *img;
uint16_t w, h;
if (page >= kid_npages) return;
if (Kid.frame == 177) /* напоролся на пики */
obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5;
else
obj_x += (Kid.direction < 0) ? -5 : 5;
obj_x = obj_x * 8 / 7; /* лог. X -> экран (calc_screen_x_coord) */
img = (const uint8_t *)atlas_image(&kidp[page], idx);
gfx_w0_map(kidp[page].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
uint8_t flip = (Kid.direction >= 0);
int bx = obj_x;
int top = obj_y - (int)h + 1;
if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols(bx, top + POP_YOFF, img, flip);
gfx_set_bank(GFX_BANK_NORMAL);
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе
* брызги останутся на экране после кадра. */
{
uint8_t dp = gfx_get_draw_page();
if (kid_lvalid[dp]) {
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx;
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
int x1 = kid_lx[dp] + (int)kid_lw[dp];
int y1 = kid_ly[dp] + (int)kid_lh[dp];
if (bx + (int)w > x1) x1 = bx + (int)w;
if (top + (int)h > y1) y1 = top + (int)h;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
}
}
}
gfx_w0_unmap();
}
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */
void kid_heal(void)
{
uint8_t p = gfx_get_draw_page();
if (kid_lvalid[p])
gfx_heal(kid_lx[p], kid_ly[p], kid_lw[p], kid_lh[p]);
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */
kid_lvalid[p] = 0;
}
@@ -200,7 +266,10 @@ void kid_draw(void)
if (fr.image == 255)
return; /* пустой кадр */
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx);
/* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
* отрисованной — спрайт за кромкой. Коллизия (Kid.x в pop_map) НЕ
* смещается: она в кадре комнаты Kid; смещение только рендерное. */
fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx;
obj_x = (fwd << 1) - 116;
obj_y = (int)Kid.y + fr.dy;
if ((int8_t)(fr.flags ^ Kid.direction) >= 0)
@@ -224,15 +293,32 @@ void kid_draw(void)
if (w && h) {
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
int bx = obj_x + KID_XOFF;
int skip = 0; /* строк спрайта срезано сверху */
int ct;
if (flip)
bx -= (int)w; /* seg008: xpos -= image->w */
top = obj_y - (int)h + 1 + KID_YOFF;
uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */
/* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше
* линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает
* сквозь щели кладки при спуске/висе. */
ct = pop_clip_char_top(kid_fp_x, obj_y, w, h);
if (ct > top) {
skip = ct - top;
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
}
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
gfx_blit_cols(bx, top, img, flip);
if (skip < (int)h) /* +YOFF: центрирование */
gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0);
gfx_set_bank(GFX_BANK_NORMAL);
kid_lx[dp] = bx; kid_ly[dp] = top;
kid_lw[dp] = w; kid_lh[dp] = h; kid_lvalid[dp] = 1;
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
* узко, только по прямоугольнику Кида и только при выходе. */
pop_clip_sprite(bx, w, top + skip + POP_YOFF, (int)h - skip);
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
kid_lx[dp] = bx; kid_ly[dp] = top + skip;
kid_lw[dp] = w; kid_lh[dp] = (uint16_t)((int)h - skip);
kid_lvalid[dp] = (skip < (int)h);
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
}
gfx_w0_unmap();
+16
View File
@@ -26,6 +26,10 @@ typedef struct {
extern kid_t Kid;
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
extern int8_t knock;
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
* 0 — OK, -1 — ошибка. */
int pop_kid_load(uint8_t npages);
@@ -52,10 +56,22 @@ uint8_t kid_cur_flags(void);
* точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */
void kid_set_seq(uint8_t seq_id);
/* Рендер-смещение Kid по ЛОГИЧЕСКОЙ X (порт xpos_in_drawn_room, seg004):
* когда комната Kid ≠ отрисованной (straddle у шва), спрайт рисуется со
* сдвигом ∓140 (левый сосед −140 / правый +140), при этом Kid.x/коллизия
* остаются в кадре комнаты Kid. 0 — комнаты совпадают (обычный случай).
* Влияет на kid_draw и метрики футпринта (fore) — heal берёт запомненный
* прямоугольник, поэтому согласован автоматически. */
void pop_kid_set_render_dx(int dx);
/* Нарисовать текущий кадр Kid (банк GFX_BANK_SPRITE — видео-ОЗУ, без
* теневой копии; запоминает прямоугольник для kid_heal). */
void kid_draw(void);
/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid —
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
void kid_draw_splash(void);
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
void kid_heal(void);
+239
View File
@@ -0,0 +1,239 @@
/*
* pop_level.c — загрузка уровня PoP из сырого res200N.bin (Фаза L1).
* См. pop_level.h. Порт формата blueprnt DAT 1.0 (Table 6 POP-DAT).
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
#include "pop_level.h"
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
* _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но
* стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
extern void _gfx_w0_isr(void);
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */
/* Смещения блоков blueprnt относительно начала данных (Table 6). */
#define BP_FG 0u
#define BP_BG 720u
#define BP_LINKLOC 1440u /* doorlink1 (LINKLOC), 256 Б */
#define BP_LINKMAP 1696u /* doorlink2 (LINKMAP), 256 Б */
#define BP_LINKS 1952u
#define BP_START 2112u
#define ROOM_TILES 30u
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
static uint8_t lvl_blk, lvl_page, lvl_ok;
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
static uint8_t dl1[256], dl2[256];
int pop_level_load(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
lvl_blk = blk;
lvl_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(lvl_page);
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
uint16_t i;
stub = (uint16_t)&_gfx_w0_isr;
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
for (i = 0; i < 256; i++) {
dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
}
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
lvl_ok = 1;
return 0;
}
void pop_level_free(void)
{
if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; }
}
/* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
* замаплена вызывающим). */
static const uint8_t *room_fg_ptr(uint8_t r)
{
return (const uint8_t *)(LVL_DATA_OFF + BP_FG) + (uint16_t)(r - 1) * ROOM_TILES;
}
static const uint8_t *room_bg_ptr(uint8_t r)
{
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
}
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg)
{
const uint8_t *rfg, *rbg, *lnk;
uint8_t left, right, down, i;
if (!lvl_ok || room < 1 || room > 24) return -1;
gfx_w0_map(lvl_page);
rfg = room_fg_ptr(room);
rbg = room_bg_ptr(room);
lnk = (const uint8_t *)(LVL_DATA_OFF + BP_LINKS) + (uint16_t)(room - 1) * 4;
for (i = 0; i < ROOM_TILES; i++) {
fg[i] = (uint8_t)(rfg[i] & 0x1F); /* тип тайла (верх. биты — модиф., пока не нужны) */
bg[i] = rbg[i]; /* backtable как есть */
}
left = lnk[0]; /* L */
right = lnk[1]; /* R */
down = lnk[3]; /* D */
/* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0),
* а в [3..5] — ПРЕДпоследний (col8). Вторая колонка нужна коллизии: Kid,
* стоящий В шве (curr_col = −1), смотрит вперёд на колонку −2, и без неё
* там оказывалась мнимая стена (get_tile → TILE_WALL), которая выпихивала
* его сквозь решётку. Оригинал резолвит любую колонку через
* find_room_of_tile (seg006:005D). */
if (left >= 1 && left <= 24) {
const uint8_t *lf = room_fg_ptr(left);
const uint8_t *lb = room_bg_ptr(left);
for (i = 0; i < 3; i++) {
lcol_fg[i] = (uint8_t)(lf[i * 10 + 9] & 0x1F);
lcol_fg[3 + i] = (uint8_t)(lf[i * 10 + 8] & 0x1F);
lcol_bg[i] = lb[i * 10 + 9];
}
} else {
for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_fg[3 + i] = 20; lcol_bg[i] = 0; }
}
/* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9:
* коллизия на правом шве + переход вправо) */
if (right >= 1 && right <= 24) {
const uint8_t *rf = room_fg_ptr(right);
const uint8_t *rb = room_bg_ptr(right);
for (i = 0; i < 3; i++) {
rcol_fg[i] = (uint8_t)(rf[i * 10 + 0] & 0x1F);
rcol_fg[3 + i] = (uint8_t)(rf[i * 10 + 1] & 0x1F); /* колонка 11 (см. выше) */
rcol_bg[i] = rb[i * 10 + 0];
}
} else {
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
}
/* belowrow: верхний ряд (row0) комнаты снизу */
if (down >= 1 && down <= 24) {
const uint8_t *df = room_fg_ptr(down);
for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F);
} else {
for (i = 0; i < 10; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */
}
gfx_w0_unmap();
return 0;
}
int8_t pop_room_col_landing(uint8_t room, uint8_t col)
{
const uint8_t *rf;
int8_t row, res = -1;
if (!lvl_ok || room < 1 || room > 24 || col > 9) return -1;
gfx_w0_map(lvl_page);
rf = room_fg_ptr(room);
for (row = 0; row < 3; row++) { /* кусок пролетает пустые ряды */
uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
if (t == 0) continue; /* empty — дальше вниз */
if (t == 1) res = row; /* floor — приземление → debris */
break; /* первый не-пустой тайл — стоп */
}
gfx_w0_unmap();
return res;
}
uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; }
uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; }
void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos)
{
uint8_t v;
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
gfx_w0_map(lvl_page);
v = (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
gfx_w0_unmap();
return v;
}
/* Батч-доступ: смаппить страницу уровня в W0 ОДИН раз, прочитать много
* тайлов через pop_level_tile_raw, размаппить один раз. Экономит N свапов
* окна 0 в горячих циклах (pop_process_trobs звал pop_level_tile на каждый
* trob каждый кадр). Между begin/end НЕЛЬЗЯ рисовать/звать код, которому
* нужно окно 0 в исходном состоянии — только сырые чтения уровня. */
void pop_level_access_begin(void) { if (lvl_ok) gfx_w0_map(lvl_page); }
void pop_level_access_end(void) { if (lvl_ok) gfx_w0_unmap(); }
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
{
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
return (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
}
void pop_level_room_bg(uint8_t room, uint8_t *out30)
{
const uint8_t *rbg;
uint8_t i;
if (!out30) return;
if (!lvl_ok || room < 1 || room > 24) {
for (i = 0; i < ROOM_TILES; i++) out30[i] = 0;
return;
}
gfx_w0_map(lvl_page);
rbg = room_bg_ptr(room);
for (i = 0; i < ROOM_TILES; i++) out30[i] = rbg[i];
gfx_w0_unmap();
}
uint8_t pop_room_link(uint8_t room, uint8_t side)
{
uint8_t v;
if (!lvl_ok || room < 1 || room > 24 || side > 3) return 0;
gfx_w0_map(lvl_page);
v = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(uint16_t)(room - 1) * 4 + side];
gfx_w0_unmap();
return v;
}
static uint8_t start_byte(uint16_t off)
{
uint8_t v;
if (!lvl_ok) return 0;
gfx_w0_map(lvl_page);
v = ((const uint8_t *)(LVL_DATA_OFF))[off];
gfx_w0_unmap();
return v;
}
uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); }
uint8_t pop_level_start_pos(void) { return start_byte(BP_START + 1); }
int8_t pop_level_start_dir(void) { return (int8_t)start_byte(BP_START + 2); }
+80
View File
@@ -0,0 +1,80 @@
/*
* pop_level.h — данные уровня PoP из сырого файла res200N.bin (Фаза L1).
*
* Уровень загружается ОДИН раз в отдельную EMM-страницу (как атласы:
* данные с offset 0x100, ISR-стаб в первых байтах — страница безопасна
* для маппинга в W0). Файл — сырой blueprnt DAT 1.0 (2305 байт, БЕЗ
* checksum-байта; Table 6 спецификации POP-DAT):
* foretable(fg) @0 backtable(bg) @720 door I/II @1440/1696
* links @1952 (24×{L,R,U,D}) start @2112 ...
* Тайл-код = байт fg & 0x1F (верхние биты — модификатор, пока не нужны);
* bg передаётся как есть. См. applications/PoP/docs/POP-DAT-* .
*
* Рабочая копия ТЕКУЩЕЙ комнаты живёт в обычной памяти (W2) у приложения
* (мутабельная — loose→empty); pop_room_load извлекает её из страницы +
* тонкие срезы соседей для кромок (правый столбец left-комнаты, верхний
* ряд down-комнаты). Страница маппится в W0 только на время извлечения.
*/
#ifndef POP_LEVEL_H
#define POP_LEVEL_H
#include <stdint.h>
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
* (open/alloc/размер). Звать ПОСЛЕ первого atlas_load (pop_bg_load) —
* он снимает страницу ядра DSS для W0-unmap. */
int pop_level_load(const char *path);
void pop_level_free(void);
/* Извлечь комнату room (1..24) в массивы приложения (W2):
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
* lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
* (кромка col0), [3..5] предпоследний (col8) —
* для коллизии на колонке −2 (Kid стоит В шве);
* rcol_fg[6] симметрично (col0 / col1);
* нет комнаты → стена/0;
* below_fg[10] — верхний ряд (row0) комнаты СНИЗУ («стена
* вниз»); нет комнаты → стена.
* 0 — OK, -1 — уровень не загружен / room вне диапазона. */
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg);
/* Связь комнаты по стороне (0=L,1=R,2=U,3=D) → номер соседней (1..24) или
* 0, если стороны нет. Для переходов между комнатами (L2/L3). */
uint8_t pop_room_link(uint8_t room, uint8_t side);
/* Тайл-код (fg & 0x1F) по комнате (1..24) и позиции tilepos (0..29,
* row*10+col); вне диапазона → 0 (empty). Для trob-диспетчера (pop_trob):
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
* Между begin/end — только pop_level_tile_raw, без рисования. */
void pop_level_access_begin(void);
void pop_level_access_end(void);
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos);
/* Скопировать backtable (модификаторы) комнаты room в out[30]. Исходное
* состояние per-room modifier (openness ворот, состояние пик и т.п.) для
* ленивой инициализации room_modif в pop_trob. Вне диапазона → нули. */
void pop_level_room_bg(uint8_t room, uint8_t *out30);
/* Таблицы связей дверей (LINKLOC@1440 / LINKMAP@1696, по 256 Б). Копируются
* в W2-RAM при загрузке (LINKMAP мутабельна — таймеры кнопок). i — индекс
* цепочки (модификатор кнопки). Декод — в pop_trob (get_doorlink_*). */
uint8_t pop_doorlink1(uint8_t i);
uint8_t pop_doorlink2(uint8_t i);
void pop_doorlink2_set(uint8_t i, uint8_t v);
/* Ряд приземления падающего loose-куска в колонке col комнаты room: первый
* НЕ-пустой тайл сверху вниз; возвращает его ряд, если это floor (→ debris),
* иначе -1 (пролетел/крушение о стену). */
int8_t pop_room_col_landing(uint8_t room, uint8_t col);
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
uint8_t pop_level_start_room(void);
uint8_t pop_level_start_pos(void);
int8_t pop_level_start_dir(void);
#endif
+765 -22
View File
@@ -19,6 +19,8 @@
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
#include "pop_bg.h" /* pop_loose_shake_draw/bake_empty — перерисовка */
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
#define TILE_EMPTY 0
@@ -28,6 +30,11 @@
#define TILE_MIRROR 13
#define TILE_CHOMPER 18
#define TILE_WALL 20
#define TILE_LOOSE 11 /* проваливающийся пол */
#define TILE_DEBRIS 14 /* щебень (упавший loose) */
#define TILE_SPIKE 2 /* пики */
#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */
#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */
#define ACT_STAND 0
#define ACT_RUNJUMP 1
#define ACT_HANGCLIMB 2
@@ -35,6 +42,7 @@
#define ACT_FREEFALL 4
#define ACT_BUMPED 5
#define ACT_HANGSTRAIGHT 6
#define ACT_TURN 7
#define SEQ_7_FALL 7 /* stepfall (stand/run/step/crouch) */
#define SEQ_17_SOFT_LAND 17
@@ -55,6 +63,13 @@
#define SEQ_23_RELEASE_LEDGE_FALL 23 /* отпустить уступ и падать */
#define SEQ_68_CLIMB_DOWN 68 /* спуск через зацеп (не приседание) */
#define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */
#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */
/* Кадры анимации для check_spiked (seg006:0658). */
#define FRAME_7_RUN 7
#define FRAME_26_STANDJUMP_LAND 26
#define FRAME_34_RUNJUMP_START 34
#define FRAME_43_RUNJUMP_LAND 43
#define FRAME_NEEDS_FLOOR 0x40
#define FRAME_WEIGHT_X 0x1F
@@ -81,23 +96,132 @@ static const int8_t wall_dr[6] = {0, 0, 10, 13, 0, 0}; /* wall_dist_from_right
static int8_t g_infrontx; /* колонка «перед персонажем» (set в get_tile_infrontof) */
/* ---- Карта комнаты ------------------------------------------------- */
uint8_t pop_fell_out; /* 1 = Kid упал за низ комнаты (сброс приложением) */
static const uint8_t *g_fg; /* fg[30], row*10+col */
uint8_t pop_fell_out; /* 1 = Kid пересёк НИЖНЮЮ границу (см. pop_map.h) */
uint8_t pop_leave_dir; /* уход вбок/вверх: 0=нет; 1=left,2=right,3=up (side+1) */
uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заблокирован N кадров после входа в комнату */
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
/* Состояние edge-триггера бампа (порт prev_coll_flags, seg004): колонка, для
* которой сохранены флаги перекрытия, и сами флаги (бит0 = левая грань стены,
* бит1 = правая). BUMP_COL_NONE — данных нет (смена комнаты/старт). */
#define BUMP_COL_NONE ((int8_t)-128)
static int8_t bump_prev_col;
static uint8_t bump_prev_flags;
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
* соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично
* (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах —
* иначе Kid, войдя в соседнюю комнату у самой кромки (curr_col=-1/10), стоял
* бы «в стене» уровня и in_wall выбрасывал бы его. ВТОРАЯ колонка нужна,
* когда Kid стоит В шве и смотрит НАРУЖУ: передняя колонка тогда −2 (или 11),
* и мнимая стена там выпихивала его сквозь решётку (баг разворота у ворот).
* Оригинал резолвит любую колонку через find_room_of_tile (seg006:005D).
* Задаются pop_map_set_edges. */
static uint8_t g_link_l, g_link_r, g_link_u, g_link_d;
static uint8_t g_lcol[6], g_rcol[6];
void pop_map_set(const uint8_t *fg) { g_fg = fg; }
/* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- *
* В оригинале это тот же loose, просто get_tile ушёл в комнату сверху
* (find_room_of_tile) и tilepos получился отрицательным (set_redraw* уводит
* такие в redraw_frames_above). У нас ряд 2 верхнего соседа живёт отдельной
* копией (above_fg в приложении, наш «ряд −1»), поэтому и состояние
* отдельное — pop_ceil_modif[col] с той же семантикой (0 покой, 0x80.. —
* тряска, 1..11 — отсчёт до падения). */
uint8_t pop_ceil_modif[10]; /* публично: читает pop_bg при отрисовке */
static uint8_t ceil_rest[10]; /* «перерисовать ПОКОЙ» (2 = обе страницы) */
static uint8_t ceil_bake[10]; /* «запечь колодец» на второй странице */
static uint8_t land_bake, land_bake_pos; /* щебень от приземления — вторая страница */
static uint8_t *g_above; /* ряд 2 комнаты сверху (fg), мутабельный */
uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провалившейся плиты-потолка */
uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */
void pop_map_set_above(uint8_t *above_row2_fg) { g_above = above_row2_fg; }
void pop_map_set(uint8_t *fg) { g_fg = fg; }
void pop_map_set_room(uint8_t room) { g_room = room; }
void pop_kid_hp_reset(void) { hitp_curr = 3; pop_kid_dead = 0; }
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
static uint8_t take_hp(uint8_t count)
{
if (count >= hitp_curr) { hitp_curr = 0; return 1; }
hitp_curr = (uint8_t)(hitp_curr - count);
return 0;
}
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg)
{
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; }
if (rcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_rcol[i] = rcol_fg[i]; }
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и
* бампа на кадре входа нет). */
bump_prev_col = BUMP_COL_NONE;
}
static uint8_t get_tile(int8_t col, int8_t row)
{
if (col < 0 || col > 9 || row < 0 || row > 2)
g_curr_tile = TILE_WALL; /* край уровня = стена */
/* Ряд −1 — ряд 2 комнаты СВЕРХУ (порт find_room_of_tile, seg006:005D:
* отрицательный row уходит по links.up). Нужен для зацепа за край
* провалившейся плиты-потолка (колодец наверх) и выбора seq прыжка. */
if (row == -1 && g_above && g_link_u && col >= 0 && col <= 9)
g_curr_tile = (uint8_t)(g_above[col] & 0x1F);
else if (row < 0 || row > 2)
g_curr_tile = TILE_WALL;
else if (col == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */
g_curr_tile = g_lcol[row] & 0x1F;
else if (col == -2 && g_link_l) /* на тайл глубже в комнату слева (col8) */
g_curr_tile = g_lcol[3 + row] & 0x1F;
else if (col == 10 && g_link_r) /* правый шов: тайл col0 комнаты справа */
g_curr_tile = g_rcol[row] & 0x1F;
else if (col == 11 && g_link_r) /* на тайл глубже в комнату справа (col1) */
g_curr_tile = g_rcol[3 + row] & 0x1F;
else if (col < 0 || col > 9)
g_curr_tile = TILE_WALL; /* край уровня / нет соседа = стена */
else
g_curr_tile = g_fg[row * 10 + col] & 0x1F;
return g_curr_tile;
}
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
* tile_is_floor это ПОЛ), поэтому Kid НЕ падает на них. Раньше get_tile ради
* проходимости подменял ворота шва на empty — но подмену видел и check_on_floor
* → start_fall (Kid проваливался при проходе через ОТКРЫТЫЕ ворота шва). Теперь
* проходимость живёт ТОЛЬКО в bump-логике (как в SDLPoP), не трогая проверку пола.
* Модификатор: шов слева (col=-1) — сосед g_link_l col9; шов справа (col=10) —
* g_link_r col0; ВНУТРИ комнаты (0..9) — текущая комната g_room (иначе после
* перехода Kid встаёт на ту же створку уже как на внутрикомнатную и его отбивает
* обратно). Открыто ⇔ (modif>>2)+6 >= char_height (бары выше головы). */
/* Живой модификатор (openness) ворот в (col,row): своя комната или соседняя
* через шов. 0xFF — тайла нет (за краем уровня). */
static uint8_t gate_modif(int8_t col, int8_t row)
{
if (row < 0 || row > 2) return 0xFF;
if (col == -1 && g_link_l) return pop_trob_modif(g_link_l)[row * 10 + 9];
if (col == 10 && g_link_r) return pop_trob_modif(g_link_r)[row * 10 + 0];
if (col >= 0 && col <= 9) return pop_trob_modif(g_room)[row * 10 + col];
return 0xFF;
}
static uint8_t gate_passable(int8_t col, int8_t row)
{
uint8_t gm = gate_modif(col, row);
if (gm == 0xFF) return 0;
return (uint8_t)((gm >> 2) + 6) >= (uint8_t)kid_fp_height();
}
static uint8_t tile_is_floor(uint8_t t)
{
switch (t) {
@@ -200,13 +324,31 @@ static int distance_to_edge(int xpos)
static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); }
/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- *
* Передний край персонажа char_x_*_coll = char_dx_forward(cur_frame.dx)
* (obj_x/2+58 сокращается ровно к этому; ширина спрайта НЕ нужна), ∓4 для
* THIN-кадров. dist_from_wall_forward — расстояние переднего края до грани
* стены tiletype в колонке tcol: >0 зазор, <=0 достал/зашёл (насколько). */
* Передний край персонажа = char_x_right (взгляд вправо) / char_x_left
* (влево) из set_char_collision (seg006:0723): obj_x/2 + 58, где obj_x =
* 2*char_dx_forward(cur_frame.dx) - 116 и load_frame_to_obj (seg008:1728)
* добавляет +1 для кадров «чётного пикселя» (бит 0x80 флагов XOR знак
* направления). Деление obj_x/2 в C усечено К НУЛЮ, поэтому в зоне левого
* шва (e<=57, obj_x<0) поправка +1 сдвигает край на пиксель ВПРАВО: e -> e+1.
* Это НЕ мелочь: Kid, осевший после отскока от ворот шва на x=57, имеет
* char_x_right=58 и НЕ пересекает порог leave-left (<=57) — иначе пинг-понг
* drawn_room у закрытых ворот (BUG-SEAM-PINGPONG; подтверждено потиковой
* трассой живого SDLPoP 1.23). При e>=58 формула сокращается к чистому
* char_dx_forward. ∓4 для THIN-кадров поверх. dist_from_wall_forward —
* расстояние переднего края до грани стены tiletype в колонке tcol: >0
* зазор, <=0 достал/зашёл (насколько). */
static int char_x_forward_edge(void)
{
int e = char_dx_forward(kid_cur_dx());
uint8_t fl = kid_cur_flags();
if (Kid.direction < 0) fl ^= 0x80;
if (!(fl & 0x80) && e <= 57) e++; /* odd pixel + C-трунк к нулю */
return e;
}
static int char_front_coll(void)
{
int cx = char_dx_forward(kid_cur_dx());
int cx = char_x_forward_edge();
if (kid_cur_flags() & FRAME_THIN)
cx += (Kid.direction < 0) ? 4 : -4; /* THIN: край внутрь */
return cx;
@@ -245,10 +387,36 @@ static void in_wall(void)
/* ---- Падение ------------------------------------------------------- */
static void inc_curr_row(void) { Kid.curr_row++; }
/* Пики (определены ниже, перед pop_phys_tick) — нужны в land() для смерти
* при ПАДЕНИИ на выдвинутые пики (seg005 loc_5EE6). */
static int is_spike_harmful(uint8_t tilepos);
static void spiked(uint8_t tilepos);
/* fell_on_spikes (seg005 loc_5EE6): при приземлении тайл ПОД персонажем =
* пика (или пика СЗАДИ у самой кромки) и вредна → напороться (смерть).
* Возвращает 1, если умер (land прерывается). */
static uint8_t fell_on_spikes(void)
{
uint8_t tp;
if (get_tile_at_char() == TILE_SPIKE) {
tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
} else if (distance_to_edge_weight() >= 12 &&
get_tile_behind_char() == TILE_SPIKE) {
int8_t bcol = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col);
if (bcol < 0 || bcol > 9) return 0;
tp = (uint8_t)(Kid.curr_row * 10 + bcol);
} else {
return 0;
}
if (is_spike_harmful(tp)) { spiked(tp); return 1; }
return 0;
}
static void land(void)
{
uint8_t seq;
Kid.y = (uint8_t)y_land[Kid.curr_row + 1];
if (fell_on_spikes()) return; /* упал на вредные пики — смерть */
/* к краю пола — чуть назад (как оригинал) */
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3)
Kid.x = (uint8_t)char_dx_forward(-3);
@@ -274,6 +442,13 @@ static void start_fall(void)
kid_set_seq(seq);
play_seq();
determine_col();
/* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2).
* Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose)
* наш get_tile отдаёт WALL как СЕНТИНЕЛ кромки уровня, но в оригинале там
* КОМНАТА СНИЗУ (у дыры — пол/пусто, не стена). Без гарда in_wall() ложно
* срабатывал и откатывал X назад (6−d) → дайв падения смещался на тайл
* влево (Кид «клевал» рано над колонной вместо дыры). */
if (Kid.curr_row <= 2) {
if (get_tile_at_char() == TILE_WALL) {
in_wall();
return;
@@ -288,6 +463,7 @@ static void start_fall(void)
determine_col();
}
}
}
/* ---- Зацеп (seg006 can_grab/check_grab) --------------------------- *
* can_grab: можно ли зацепиться за g_curr_tile (curr_tile2) СКВОЗЬ through-
@@ -349,10 +525,9 @@ static void do_fall(void)
land();
else if (Kid.curr_row < 2)
inc_curr_row(); /* следующий ряд ВНУТРИ комнаты */
else
pop_fell_out = 1; /* упал за низ комнаты (нет пола,
комнаты снизу нет) — приложение
сбросит Kid (K-later: переход) */
/* curr_row==2, нет пола: продолжает падать (fall_speed двигает y);
* пересечение нижней границы комнаты ловит check_leave_below (y>=211,
* порт leave_room) — общий путь для провала И спуска-зацепа. */
}
}
@@ -488,13 +663,18 @@ int pop_wall_ahead(void)
/* grab_timer истёк — можно подтягиваться? */
uint8_t pop_hang_grab_ready(void) { return grab_timer == 0; }
/* can_climb_up: тайл над персонажем — обычный подъём (seq_10) либо подъём
* к закрытой калитке/зеркалу/чомперу (seq_73). */
/* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10)
* либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал
* обратно»). Для ворот условие включает ОТКРЫТОСТЬ:
* curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6
* — под поднятой решёткой (modif >> 2 >= 6) подъём обычный. Без проверки
* модификатора Kid срывался и с открытых ворот. */
uint8_t pop_hang_climb_up_seq(void)
{
uint8_t t = get_tile_above_char();
if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) ||
(t == TILE_GATE && Kid.direction != 0))
(t == TILE_GATE && Kid.direction != 0 &&
(uint8_t)(gate_modif(Kid.curr_col, (int8_t)(Kid.curr_row - 1)) >> 2) < 6))
return SEQ_73_CLIMB_UP_TO_GATE;
return SEQ_10_CLIMB_UP;
}
@@ -543,8 +723,11 @@ uint8_t pop_down_action(void)
if (!tile_is_floor(get_tile_behind_char()) && distance_to_edge_weight() >= 8) {
uint8_t through = get_tile_behind_char();
get_tile_at_char(); /* g_curr_tile = под персонажем */
/* seg005:482: спуск с тайла-ворот лицом ВЛЕВО запрещён только пока они
* ЗАКРЫТЫ (modif >> 2 < 6); под поднятой решёткой спуск обычный. */
if (can_grab(through) &&
(Kid.direction >= 0 || get_tile_at_char() != TILE_GATE)) {
(Kid.direction >= 0 || get_tile_at_char() != TILE_GATE ||
(uint8_t)(gate_modif(Kid.curr_col, Kid.curr_row) >> 2) >= 6)) {
Kid.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 9));
kid_set_seq(SEQ_68_CLIMB_DOWN);
return 1;
@@ -586,7 +769,7 @@ static void check_action(void)
* выравниваем, без рестарта. В падении/висе не трогаем. */
static void check_bumped(void)
{
uint8_t t; int8_t tcol; int d;
uint8_t t, wt; int8_t tcol; int d;
/* Не бампим в падении/висе/подтягивании (seg004 check_bumped guard'ы:
* action hang_climb/hang_straight + кадры подъёма 135..148 — иначе
* climb-up у стены прерывается отскоком). FREEFALL — наш guard: стену
@@ -602,9 +785,59 @@ static void check_bumped(void)
* dist_from_wall_forward/x_bump[] — мусор с большим отриц. сдвигом →
* underflow X и уход в стену. fell_out ловит do_fall. */
if (Kid.curr_row < 0 || Kid.curr_row > 2) return;
t = get_tile_at_char(); tcol = Kid.curr_col;
if (!wall_type(t)) { t = get_tile_infrontof_char(); tcol = g_infrontx; }
if (!wall_type(t)) return;
/* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются
* (bump_col_* сбрасываются и функция выходит) — bump'а в развороте нет. */
if (Kid.action == ACT_TURN) return;
/* Стена ищется по колонке, в которую упирается ПЕРЕДНИЙ КРАЙ персонажа
* (порт seg004: bump_col_left/right_of_wall — колонка, где край пересёк
* грань; НЕ curr_col!). Через curr_col было ДВА расхождения: (1) весовая
* точка кадра заезжает за грань раньше края, curr_col становился −1 на
* шве, ворота оказывались «под персонажем» и гард односторонней стены их
* снимал — Kid уходил в соседнюю комнату СКВОЗЬ ОПУЩЕННУЮ РЕШЁТКУ;
* (2) тот же гард был нужен только из-за отсутствия проверки разворота
* (см. выше). Колонка переднего края даёт и то, и другое даром: стоя
* СПРАВА от ворот лицом вправо, край в соседней колонке — ворота не
* бампят; заехав краем за их грань лицом влево — бампят. */
tcol = get_tile_div_mod_m7(char_front_coll());
t = get_tile(tcol, Kid.curr_row);
wt = wall_type(t);
/* EDGE-триггер (порт check_collisions + get_row_collision_data, seg004:0004).
* Оригинал считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ направления:
* 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены»)
* 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань»)
* и зовёт bumped() ТОЛЬКО когда нужный флаг перешёл 0 -> 1 (взгляд вправо
* смотрит на 0x0F, влево — на 0xF0). Без этого проверка «уровнем» ловит
* ЛЮБОЕ перекрытие: Kid, стоящий В проёме опущенной решётки, при развороте
* на месте получал ложный бамп и выпихивался сквозь неё в соседнюю
* комнату. С флагами от габарита разворот их не меняет — бампа нет. */
{
uint8_t f = 0, prev, need;
if (wt) {
int coll_left = x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
int lw = wall_dl[wt] + coll_left; /* get_left_wall_xpos */
int rw = coll_left - wall_dr[wt] + TILE_RIGHTX; /* get_right_wall_xpos */
int e = char_x_forward_edge(), wh = ((int)kid_fp_width() + 1) >> 1;
int xl, xr;
if (Kid.direction < 0) { xl = e; xr = e + wh; } /* габарит персонажа */
else { xr = e; xl = e - wh; }
if (kid_cur_flags() & FRAME_THIN) { xl += 4; xr -= 4; }
if (lw < xr) f |= 1;
if (rw > xl) f |= 2;
}
/* прошлый кадр: та же колонка — её флаги; другая — считаем, что не
* перекрывали; «нет данных» (смена комнаты) — считаем, что перекрывали
* всё, чтобы на кадре входа бампа не было (в оригинале это даёт
* несовпадение prev_coll_room и curr_row_coll_room). */
prev = (bump_prev_col == BUMP_COL_NONE) ? 3u
: (bump_prev_col == tcol ? bump_prev_flags : 0u);
bump_prev_col = tcol;
bump_prev_flags = f;
if (!wt) return;
need = (Kid.direction < 0) ? 2u : 1u;
if (!(f & need) || (prev & need)) return; /* не перекрыл / уже перекрывал */
}
if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row))
return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */
d = dist_from_wall_forward(t, tcol);
if (d >= 0) return; /* ещё не зашёл за грань */
Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */
@@ -616,12 +849,522 @@ static void check_bumped(void)
determine_col();
}
/* ---- Loose floors (seg007) ---------------------------------------- *
* Состояние на тайл: 0 = покой; 0x80..0x83 = тряска (do_knock); 1..11 =
* отсчёт до падения (на тайле стоят/пробили). Порт animate_loose/
* make_loose_fall/check_press. Перерисовка — в pop_bg (shake/bake). */
uint8_t pop_loose_modif[30]; /* публично: читает pop_bg при отрисовке */
static uint8_t loose_bake[30]; /* счётчик «запечь пусто» (2 = обе страницы) */
static uint8_t loose_rest[30]; /* счётчик «перерисовать ПОКОЙ» после тряски (2 = обе стр.) */
/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */
/* make_loose_fall (seg007:0EF6): взвести отсчёт падения, если ещё не идёт. */
static void make_loose_fall(int pos, uint8_t modifier)
{
if (pos < 0 || pos >= 30) return;
if ((g_fg[pos] & 0x1F) != TILE_LOOSE) return;
if ((g_fg[pos] & 0x20)) return; /* «solid» loose — не от шага */
if ((int8_t)pop_loose_modif[pos] <= 0) /* покой/тряска, не отсчёт */
pop_loose_modif[pos] = modifier;
}
/* check_press (seg006:0EC8, упрощ.): Kid стоит на loose → make_loose_fall;
* frame 79 (прыжок вверх) с loose СВЕРХУ → пробить сверху. */
static void check_press(void)
{
uint8_t frame = Kid.frame, action = Kid.action;
uint8_t above_press = 0; /* «нажимается» тайл СВЕРХУ */
/* Кадры виса (87..99) и начала подъёма (135..140): нажимается тайл, за
* КОТОРЫЙ ПЕРСОНАЖ ДЕРЖИТСЯ, т.е. верхний (seg006:1688). Отсюда падение
* плиты, за край которой Kid зацепился, — а не только той, по которой он
* ударил снизу прыжком (кадр 79). */
if ((frame >= 87 && frame < 100) || (frame >= 135 && frame < 141)) {
above_press = 1;
} else if (action == ACT_BUMPED || action == ACT_TURN || action < ACT_HANGCLIMB) {
if (frame == 79) above_press = 1; /* прыжок вверх — удар снизу */
} else {
return; /* seg006:1704 — прочее не жмёт */
}
if (above_press) {
if (Kid.curr_row == 0) {
/* Ряд 0: «сверху» — ряд 2 комнаты СВЕРХУ (в оригинале get_tile с
* row=1 уходит по links.up, find_room_of_tile seg006:005D).
* Плита-ПОТОЛОК; кнопок на потолке в этом тесте нет. */
int8_t c = Kid.curr_col;
if (g_above && g_link_u && c >= 0 && c <= 9 &&
(g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) &&
(int8_t)pop_ceil_modif[c] <= 0)
pop_ceil_modif[c] = 1; /* make_loose_fall(1) */
} else if (get_tile_above_char() == TILE_LOOSE) {
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
}
return;
}
/* Стояние: кадр обязан требовать пол; нажимается тайл ПОД персонажем.
* ACT_TURN обязателен (seg006:1691): при развороте на кнопке иначе
* trigger_button не зовётся кадр-два → таймер связи падает <2 → кнопка
* ложно отжимается и жмётся снова. */
if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return;
{
uint8_t t = get_tile_at_char();
uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
if (t == TILE_OPENER || t == TILE_CLOSER)
pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]);
else if (t == TILE_LOOSE)
make_loose_fall(tp, 1);
}
}
/* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от
* приземления/удара трясёт ВСЕ loose-полы в ряду tile_row (не роняя их —
* modif 0x80..0x83 = чистая тряска, сбрасывается в pop_loose_tick). Трясём
* только из ПОКОЯ (modif==0): если тайл уже дрожит/падает — не перебиваем. */
static void do_knock(int tile_row)
{
int col, pos;
if (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */
if (!g_above || !g_link_u) return;
for (col = 0; col < 10; col++)
if ((g_above[col] & 0x1F) == TILE_LOOSE && pop_ceil_modif[col] == 0)
pop_ceil_modif[col] = 0x80;
return;
}
if (tile_row < 0 || tile_row > 2) return;
for (col = 0; col < 10; col++) {
pos = tile_row * 10 + col;
if ((g_fg[pos] & 0x1F) != TILE_LOOSE) continue;
if (pop_loose_modif[pos] == 0)
pop_loose_modif[pos] = 0x80;
}
}
/* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl-
* командах KNOCK_UP/DOWN) тряхнуть ряд. KNOCK_DOWN (knock=-1, приземление) →
* ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */
void pop_check_knock(void)
{
if (knock) {
do_knock(Kid.curr_row - (knock > 0));
knock = 0;
}
}
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
* индексируются позицией тайла и не должны утечь из старой комнаты в новую.
* Плюс останавливает падающий кусок (pop_bg). */
void pop_loose_reset(void)
{
int i;
for (i = 0; i < 30; i++) {
pop_loose_modif[i] = 0;
loose_bake[i] = 0;
loose_rest[i] = 0;
}
for (i = 0; i < 10; i++) {
pop_ceil_modif[i] = 0;
ceil_rest[i] = 0;
ceil_bake[i] = 0;
}
pop_ceil_fell = 0;
pop_debris_at = 0;
pop_loose_landed = 0;
land_bake = 0;
pop_loose_mob_reset();
}
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
* (кадры 5..14) не бьёт (кроме уровня 13 — его тут нет); в висе/подъёме тоже
* (action >= hang_climb, кроме turn). Иначе −1 HP: смерть → seq_22 (crushed),
* живой → seq_52 (плита придавила: Kid приседает), с отступом на 2 назад,
* если сзади пусто. Кадр 109 (присед) последовательность не меняет. */
#define SEQ_22_CRUSHED 22
#define SEQ_52_LOOSE_FELL_ON_KID 52
#define FRAME_109_CROUCH 109
static void fell_on_your_head(void)
{
uint8_t frame = Kid.frame, action = Kid.action;
if ((frame < 5 || frame >= 15) &&
(action < ACT_HANGCLIMB || action == ACT_TURN)) {
Kid.y = (uint8_t)y_land[Kid.curr_row + 1];
pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */
if (take_hp(1)) {
kid_set_seq(SEQ_22_CRUSHED);
play_seq();
determine_col();
pop_kid_dead = 1;
} else if (frame != FRAME_109_CROUCH) {
if (get_tile_behind_char() == TILE_EMPTY)
Kid.x = (uint8_t)char_dx_forward(-2);
kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
play_seq();
determine_col();
}
}
}
/* check_loose_fall_on_kid (seg007:1192): падающий кусок в той же колонке и
* его низ уже в полосе 30px над ногами Kid → удар. */
static void check_loose_fall_on_kid(void)
{
int mcol, my;
if (pop_kid_dead) return;
if (!pop_loose_mob_pos(&mcol, &my)) return;
if (Kid.curr_col != (int8_t)mcol) return;
if (my < (int)Kid.y && (int)Kid.y - 30 < my)
fell_on_your_head();
}
/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ
* (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */
void pop_loose_tick(void)
{
int pos;
/* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80
* нет аппаратного деления, и SDCC выносил оба деления (__divuchar/
* __moduchar) в начало КАЖДОЙ итерации (общее подвыражение аргументов
* вызовов), т.е. 30*2=60 вызовов деления за кадр — даже в покое и в
* комнатах без loose-плит (условия if к тому моменту ещё не проверены).
* Инкремент+сравнение считают row/col без деления. */
uint8_t row = 0, col = 0;
for (pos = 0; pos < 30; pos++) {
if (loose_bake[pos]) { /* упал: запечь пусто (2 кадра=2 стр.) */
pop_loose_bake_empty(row, col);
loose_bake[pos]--;
} else if (loose_rest[pos]) { /* тряска кончилась: перерисовать ПОКОЙ */
/* modif уже 0 → shake_draw рисует базовый кадр (покой) обоих тайлов
* (col и col+1: правая грань loose живёт в 2,7). 2 кадра = обе
* страницы дабл-буфера — иначе на одной остаётся застывший
* дрожащий кадр правой грани → мерцание («бесконечная тряска»). */
pop_loose_shake_draw(row, col);
loose_rest[pos]--;
} else if (pop_loose_modif[pos] != 0) {
uint8_t m = ++pop_loose_modif[pos];
if (m & 0x80) { /* тряска (do_knock) */
if (m >= 0x84) { /* конец тряски: сброс + перерисовать покой */
pop_loose_modif[pos] = 0;
loose_rest[pos] = 2; /* покой на ОБЕИХ страницах */
} else {
pop_loose_shake_draw(row, col); /* дрожащий кадр */
}
} else if (m >= 11) { /* loose_floor_delay — падение */
/* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
* Debris появляется ТОЛЬКО там, где приземлился падающий
* кусок (mob), т.е. рядом ниже. Loose в нижнем ряду (2,6)
* — кусок улетает из комнаты вниз, на месте остаётся пусто
* (стена-вниз от тайла снизу + чёрный фон). */
g_fg[pos] = TILE_EMPTY;
pop_loose_modif[pos] = 0;
pop_loose_bake_empty(row, col); /* запечь пусто на ТЕКУЩЕЙ странице сразу */
loose_bake[pos] = 1; /* + вторая страница следующим кадром */
pop_loose_mob_spawn(row, col); /* отрыв падающего куска (L4) */
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению: кусок улетел */
} else { /* отсчёт 1..10 — дрожащий кадр */
pop_loose_shake_draw(row, col);
}
}
if (++col == 10) { col = 0; row++; } /* row/col без деления */
}
/* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры
* рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в
* НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */
for (col = 0; col < 10; col++) {
if (ceil_rest[col]) {
pop_ceil_shake_draw(col); /* modif уже 0 → кадр покоя */
ceil_rest[col]--;
} else if (pop_ceil_modif[col] != 0) {
uint8_t m = ++pop_ceil_modif[col];
if (m & 0x80) { /* тряска от сотрясения (do_knock) */
if (m >= 0x84) { pop_ceil_modif[col] = 0; ceil_rest[col] = 2; }
else pop_ceil_shake_draw(col);
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
pop_ceil_modif[col] = 0;
if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */
pop_ceil_bake_empty(col); /* колодец наверх на ТЕКУЩЕЙ странице */
ceil_bake[col] = 1; /* + вторая страница следующим кадром */
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
} else {
pop_ceil_shake_draw(col); /* отсчёт 1..10 — дрожащий кадр */
}
} else if (ceil_bake[col]) {
pop_ceil_bake_empty(col);
ceil_bake[col]--;
}
}
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
check_loose_fall_on_kid(); /* seg007:1192 — плита падает Киду на голову */
/* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД
* разбором нового: иначе при двух плитах, севших подряд, land_bake
* затирался новым и на одной странице оставался старый пол. */
if (land_bake && pop_loose_landed) {
uint8_t r = 0, c = land_bake_pos;
while (c >= 10) { c -= 10; r++; }
pop_floor_bake(r, c);
land_bake = 0;
}
if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */
uint8_t pos = (uint8_t)(pop_loose_landed - 1);
uint8_t r = 0, c = pos;
pop_loose_landed = 0;
while (c >= 10) { c -= 10; r++; }
g_fg[pos] = TILE_DEBRIS;
/* Перерисовка ПО ПЛОЩАДИ ПОЛА (не bake_empty: та стирает и верх стены
* ряда ниже — оставался чёрный прямоугольник под тайлом). */
pop_floor_bake(r, c);
land_bake_pos = pos; land_bake = 1; /* вторая страница дабл-буфера */
do_knock(r); /* удар плиты трясёт loose этого ряда */
pop_debris_at = (uint8_t)(pos + 1); /* сигнал приложению (персистентность) */
} else if (land_bake) {
uint8_t r = 0, c = land_bake_pos;
while (c >= 10) { c -= 10; r++; }
pop_floor_bake(r, c);
land_bake = 0;
}
}
/* y_to_row_mod4 (seg006): ряд по y с оборотом mod4 (TILE_SIZEY=63). */
static int8_t y_to_row(int16_t y) { return (int8_t)(((y + 60) / 63) % 4 - 1); }
/* ---- clip_char (seg006:1749), первый блок: обрезка спрайта СВЕРХУ ----- *
* Оригинал перед add_objtable кладёт в objtable прямоугольник клипа; блиттер
* рисует персонажа только внутри него. Верхняя граница: если тайл НАД
* головой (ряд char_top_row) — стена или пол, всё выше линии своей полосы
* (y_clip[curr_row+1]) не рисуется. Без этого Kid, ушедший головой под пол
* (спуск с кромки/кнопки, вис), просвечивает сквозь щели кладки.
*
* Хитрость исходника: `X == wall || tile_is_floor(curr_tile2)` — curr_tile2
* ставит сам get_tile, т.е. это просто «тайл X = стена ИЛИ пол».
*
* Метрики габарита — порт set_char_collision (seg006:0723); в отличие от
* check_spike_below колонки берутся get_tile_div_mod БЕЗ 7.
*
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h)
{
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
int wh = ((int)w + 1) >> 1; /* char_width_half */
int xl = obj_x / 2 + 58; /* char_x_left */
int xr, top_y, clip_y;
int8_t cL, cR, trow, clip_row;
uint8_t t;
if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; /* char_x_right */
top_y = obj_y - (int)h + 1; /* char_top_y */
if (top_y >= 192) top_y = 0; /* весь спрайт ниже комнаты */
trow = y_to_row((int16_t)top_y); /* char_top_row */
cL = get_tile_div_mod(xl); if (cL < 0) cL = 0;
cR = get_tile_div_mod(xr); if (cR > 9) cR = 9;
t = get_tile(cL, trow);
if (t != TILE_WALL && !tile_is_floor(t)) return 0;
/* кадр 79 (прыжок вверх) / 81 (зацеп) в стойке клипятся и по одной грани */
if (!(Kid.action == 0 && (Kid.frame == 79 || Kid.frame == 81))) {
t = get_tile(cR, trow);
if (t != TILE_WALL && !tile_is_floor(t)) return 0;
}
clip_row = (int8_t)(Kid.curr_row + 1);
if (clip_row < 0 || clip_row > 4) return 0;
clip_y = y_clip[clip_row];
/* ряд 0 — клип безусловный; иначе только когда линия реально режет спрайт */
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < top_y))
return clip_y;
return 0;
}
/* check_leave_below (порт leave_room, seg002: leave_dir=3 при chary>=211):
* Kid пересёк нижнюю границу комнаты (провал в дыру ИЛИ спуск-зацеп/climbdown
* — единый триггер по y). Репроекция в кадр комнаты снизу (goto_other_room
* down: y-=189, curr_row=y_to_row_mod4) + сигнал pop_fell_out — приложение
* решит: переход (links.down) или выпал из уровня (нет комнаты снизу). */
static void check_leave_below(void)
{
/* leave_room (seg002:0504) проверяет ВВЕРХ ПЕРВЫМ: персонаж не в отскоке/
* полёте и его y (как signed) в полосе −16..10 у верха комнаты — он вылез
* в комнату сверху (через колодец от упавшей плиты). Порядок важен: при
* подтягивании y ≈ 248, и проверка «ниже комнаты» (y>=211) иначе
* перехватывает подъём и роняет Kid из уровня.
* goto_other_room(up): y += 189, curr_row = y_to_row_mod4. */
if (g_link_u && Kid.action != ACT_BUMPED && Kid.action != ACT_FREEFALL &&
Kid.action != ACT_MIDAIR &&
(int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) {
Kid.y = (uint8_t)(Kid.y + 189);
Kid.curr_row = y_to_row((int16_t)Kid.y);
pop_leave_dir = 3;
return;
}
if (Kid.y >= 211) {
Kid.y = (uint8_t)(Kid.y - 189);
Kid.curr_row = y_to_row((int16_t)Kid.y);
pop_fell_out = 1;
}
}
/* check_leave вбок (порт leave_room, seg002): передний край персонажа
* (char_dx_forward(cur_frame.dx) = char_x_left при взгляде влево / char_x_right
* вправо) сравнивается с кромками комнаты. Взгляд вправо: >=201 → right,
* <=57 → left; взгляд влево: <=54 → left, >=198 → right. goto_other_room:
* x∓140 (ширина комнаты), пересчёт колонки; сигнал pop_leave_dir (2=right,
* 1=left). Уход только если сосед есть (иначе кромка = стена, check_bumped
* удержит у шва). */
static void check_leave(void)
{
int fx;
uint8_t f = Kid.frame;
/* seg002 leave_room: комнату НЕ покидаем во время разворота (action turn),
* подъёма-с-зацепа (climb, кадры 135..149) и вставания из приседа
* (110..119). Без этого гарда разворот у правого шва (взгляд флипается
* право→лево, char_x_left проскакивает >=198) телепортирует Kid сквозь
* закрытые ворота в соседнюю комнату (leave_room возвращает 1). */
if (Kid.action == ACT_TURN ||
(f >= 135 && f < 150) ||
(f >= 110 && f < 120))
return;
/* Передний край — ТОЛЬКО через char_x_forward_edge (obj_x-арифметика
* set_char_collision): в зоне шва (<=57) он на пиксель правее чистого
* Kid.x+dx, и Kid, осевший у ворот на x=57, порога leave-left НЕ достигает
* (эталон SDLPoP: экран не переключается, BUG-SEAM-PINGPONG). */
fx = char_x_forward_edge();
if (Kid.direction >= 0) { /* взгляд вправо: fx = char_x_right */
uint8_t t9 = get_tile(9, Kid.curr_row);
if (g_link_r && fx >= 201 &&
t9 != TILE_DOORTOP_FLOOR && t9 != TILE_DOORTOP) {
/* leave_room (seg002): под doortop у правой кромки выхода нет */
Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2;
} else if (g_link_l && fx <= 57) {
Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1;
}
} else { /* взгляд влево: fx = char_x_left */
if (g_link_l && fx <= 54) {
Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1;
} else if (g_link_r && fx >= 198) {
Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2;
}
}
}
/* ---- Пики (spike, seg006/seg007) ---------------------------------- *
* Состояние пики в room_modif[g_room][tilepos] (pop_trob), автомат —
* animate_spike (pop_trob). Здесь: триггер выдвижения (check_spike_below),
* решение о смерти (check_spiked) + spiked(). */
/* start_anim_spike (seg007:08E3): взвести выдвижение, если пика в покое
* (modif 0 → trob type1) или ушла не полностью (modif<0, кроме 0xFF disabled
* → мгновенно 0x8F «вышли»). */
static void start_anim_spike(uint8_t tilepos)
{
uint8_t *mod = pop_trob_modif(g_room);
int8_t old;
if (!mod) return;
old = (int8_t)mod[tilepos];
if (old <= 0) {
if (old == 0)
pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */
else if ((uint8_t)old != 0xFF)
mod[tilepos] = 0x8F;
}
}
/* is_spike_harmful (seg007:1178): 0/-1 → 0 (безопасно); <0 → 1; 1..4 → 2;
* >=5 → 0. */
static int is_spike_harmful(uint8_t tilepos)
{
int8_t m = (int8_t)pop_trob_modif(g_room)[tilepos];
if (m == 0 || m == -1) return 0;
if (m < 0) return 1;
if (m < 5) return 2;
return 0;
}
/* spiked (seg005:0888): напоролся — пика становится безвредной (0xFF),
* выровнять по колонке, take_hp(100) = смерть, seq_51 (импейл). */
static void spiked(uint8_t tilepos)
{
pop_trob_modif(g_room)[tilepos] = 0xFF;
Kid.y = (uint8_t)y_land[Kid.curr_row + 1];
Kid.x = (uint8_t)(x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
Kid.x = (uint8_t)char_dx_forward(8);
Kid.fall_y = 0;
take_hp(100); /* (TODO: sound_48_spiked) */
kid_set_seq(SEQ_51_SPIKED);
play_seq();
determine_col();
pop_kid_dead = 1;
}
/* check_spiked (seg006:0658): под персонажем пика И вредна И кадр
* бега(7..14)/старта прыжка(34..39) при harmful>=2, ЛИБО кадр приземления
* (43/26) при harmful!=0 → smert'. */
static void check_spiked(void)
{
uint8_t frame = Kid.frame, tp;
int h;
if (Kid.curr_col < 0 || Kid.curr_col > 9 || Kid.curr_row < 0 || Kid.curr_row > 2)
return;
if (get_tile_at_char() != TILE_SPIKE) return;
tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
h = is_spike_harmful(tp);
if ((h >= 2 && ((frame >= FRAME_7_RUN && frame < 15) ||
(frame >= FRAME_34_RUNJUMP_START && frame < 40))) ||
((frame == FRAME_43_RUNJUMP_LAND || frame == FRAME_26_STANDJUMP_LAND) && h != 0))
spiked(tp);
}
/* check_spike_below (seg006:1199): для каждой колонки РЕАЛЬНОГО габарита
* спрайта (char_x_left..char_x_right, порт set_char_collision) скан ВНИЗ от
* curr_row через НЕ-floor тайлы; встретил пику → start_anim_spike. Габарит,
* а не «curr_col + колонка спереди»: иначе пики выдвигаются на тайл раньше
* (когда тело Кида ещё не над шахтой). Метрики кадра — kid_fp (габарит
* последнего kid_draw; отставание в 1 кадр несущественно, ошибка в позднюю
* сторону). */
static void check_spike_below(void)
{
int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */
int xl = kid_fp_obj_x() / 2 + 58; /* char_x_left */
int xr;
int8_t c0, c1, col;
if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; /* char_x_right */
c0 = get_tile_div_mod_m7(xl);
c1 = get_tile_div_mod_m7(xr);
for (col = c0; col <= c1; col++) {
int8_t row = Kid.curr_row;
if (col < 0 || col > 9) continue;
for (;;) {
uint8_t t = get_tile(col, row);
if (t == TILE_SPIKE) { start_anim_spike((uint8_t)(row * 10 + col)); break; }
if (!tile_is_floor(t) && row <= 2) { row++; continue; }
break;
}
}
}
void pop_phys_tick(void)
{
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
if (grab_timer) grab_timer--;
fall_accel();
fall_speed();
determine_col();
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action();
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */
check_spiked(); /* seg006: напоролся на вредные пики → смерть */
if (pop_kid_dead) return; /* только что умер — без leave/knock в этот кадр */
pop_check_knock(); /* seg003: сотрясение приземления → тряска loose в ряду */
/* exit_room_timer (seg002 exit_room): 2 кадра ПОСЛЕ входа в комнату любой
* leave заблокирован. Без него транзиент отскока о ворота шва (seq_47
* роняет char_x за порог) сразу же выкидывает Kid в соседнюю комнату
* (ping-pong / баг #4). С таймером первый бамп попадает в окно блокировки,
* Kid оседает у грани (char_x стабильно > порога) — экран не перескакивает. */
if (pop_leave_timer) {
pop_leave_timer--;
} else {
check_leave_below(); /* seg002: y>=211 → переход в комнату снизу */
check_leave(); /* seg002: уход вбок → переход в соседнюю комнату */
}
}
+79 -4
View File
@@ -14,11 +14,79 @@
#include <stdint.h>
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). */
void pop_map_set(const uint8_t *fg);
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
* что и в pop_room_draw, чтобы отрисовка видела изменения. */
void pop_map_set(uint8_t *fg);
/* Установлен в 1, когда Kid упал ниже комнаты (комнаты снизу нет в этом
* тесте). Приложение должно сбросить Kid (kid_init) и снять флаг. */
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
* lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
* в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg);
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
* Звать в enter_room. */
void pop_map_set_room(uint8_t room);
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
* респавн). */
extern uint8_t pop_kid_dead;
extern uint8_t hitp_curr;
void pop_kid_hp_reset(void);
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
* Приложение переключает комнату по соответствующей связи и снимает флаг. */
extern uint8_t pop_leave_dir;
/* exit_room_timer (порт seg002 exit_room): пока >0, боковой/нижний leave
* заблокирован (декремент в pop_phys_tick). Приложение ставит =2 при входе
* в комнату (enter_room) — 2 кадра защиты от повторного/транзиентного leave
* (ping-pong у шва, транзиент отскока о ворота, баг #4/BUG-2). */
extern uint8_t pop_leave_timer;
/* Loose floors (проваливающиеся полы). pop_loose_modif[30] — состояние на
* тайл (0 покой / 0x80.. тряска / 1..11 отсчёт падения); читает pop_bg при
* отрисовке. pop_loose_tick() — анимация + перерисовка изменившихся тайлов
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
* рядом с kid-отрисовкой. */
extern uint8_t pop_loose_modif[30];
void pop_loose_tick(void);
void pop_loose_reset(void); /* сброс loose-состояния при смене комнаты */
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
extern uint8_t pop_loose_fell;
/* Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ (в оригинале — тот же loose с
* отрицательным tilepos, seg006 check_press frame 79 / seg007 animate_loose).
* pop_map_set_above даёт МУТАБЕЛЬНУЮ копию ряда 2 верхнего соседа (ту же, что
* ушла в pop_room_set_above): провалившаяся плита становится в ней EMPTY —
* сверху открывается колодец. pop_ceil_modif[col] — состояние (как
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
void pop_map_set_above(uint8_t *above_row2_fg);
extern uint8_t pop_ceil_modif[10];
extern uint8_t pop_ceil_fell;
extern uint8_t pop_debris_at;
/* Взводится в кадре, когда Kid получил урон (hitp_delta<0 оригинала) —
* приложение рисует «брызги» kid_draw_splash() поверх Kid и снимает флаг. */
extern uint8_t pop_kid_hurt;
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
* приземления/удара. Зовётся из pop_phys_tick. */
void pop_check_knock(void);
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
* комнаты снизу (y -= 189, curr_row = 0). Приложение решает: если есть
* links.down — сменить комнату (pop_room_load+перерисовка) и продолжить;
* иначе — выпал из уровня (респавн). Снять флаг после обработки. */
extern uint8_t pop_fell_out;
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
@@ -26,6 +94,13 @@ extern uint8_t pop_fell_out;
* Порядок как в оригинале (seg000 play_kid_frame). */
void pop_phys_tick(void);
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h);
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */
+429
View File
@@ -0,0 +1,429 @@
/*
* pop_trob.c — P0-каркас: room_modif + список trob + диспетчер анимаций.
* Порт SDLPoP seg007 (process_trobs/animate_tile/add_trob/animate_spike).
* См. pop_trob.h.
*/
#include "pop_trob.h"
#include "pop_level.h"
#include "pop_bg.h" /* pop_spike_redraw */
/* Коды тайлов (fg & 0x1F), которые анимируются. */
#define TILE_SPIKE 0x02
#define TILE_GATE 0x04
#define TILE_CLOSER 0x06
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
#define TILE_OPENER 0x0F
#define TILE_POTION 0x0A
#define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E
/* ---- Персистентное per-room modifier-состояние -------------------- */
static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES];
static uint8_t room_seen[POP_NROOMS]; /* modif проинициализирован из уровня */
/* ---- Список анимируемых тайлов ------------------------------------ */
typedef struct { uint8_t room, tilepos; int8_t type; } trob_t;
#define TROBS_MAX 24
static trob_t trobs[TROBS_MAX];
static uint8_t trobs_count;
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
/* Отложенная перерисовка «покоя» законченных пик/кнопок текущей комнаты
* (2 кадра = обе страницы дабл-буфера, как loose_rest). */
static uint8_t spike_rest[POP_ROOMTILES];
static uint8_t button_rest[POP_ROOMTILES];
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
* стоящим Кидом перерисовывалась КАЖДЫЙ кадр (2×draw_tile ≈ 50% кадра в
* статике), т.к. её trob висит, пока Кид стоит (это верно, как в SDLPoP —
* расхождение было именно в безусловном редрое). */
static uint8_t trob_drawn[POP_ROOMTILES];
/* Сколько тайлов ждут отложенного редроя (spike_rest/button_rest). Без
* него цикл по 30 тайлам крутился КАЖДЫЙ кадр впустую: замер в MAME
* (2026-07-27) — служебная часть pop_process_trobs ~50 000 тактов при
* кадре 430 080, то есть ~12 % кадра на пустой проход. */
static uint8_t rest_pending;
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
* выбора следующего кадра пламени. */
static unsigned long trob_seed;
static uint16_t trob_prandom(uint16_t maxv)
{
trob_seed = trob_seed * 214013UL + 2531011UL;
return (uint16_t)((uint16_t)(trob_seed >> 16) % (uint16_t)(maxv + 1));
}
void pop_trob_reset(void)
{
uint8_t i;
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; }
rest_pending = 0;
trobs_count = 0;
}
uint8_t *pop_trob_modif(uint8_t room)
{
if (room < 1 || room > POP_NROOMS) return 0;
if (!room_seen[room - 1]) {
uint8_t i;
pop_level_room_bg(room, room_modif[room - 1]);
/* seg009 load_level: у ЗЕЛЬЯ модификатор сдвигается влево на 3 —
* старшие биты = тип зелья, младшие 3 = фаза пузырька. */
pop_level_access_begin();
for (i = 0; i < POP_ROOMTILES; i++)
if ((pop_level_tile_raw(room, i) & 0x1F) == TILE_POTION)
room_modif[room - 1][i] = (uint8_t)(room_modif[room - 1][i] << 3);
pop_level_access_end();
room_seen[room - 1] = 1;
}
return room_modif[room - 1];
}
static int find_trob(uint8_t room, uint8_t tilepos)
{
uint8_t i;
for (i = 0; i < trobs_count; i++)
if (trobs[i].room == room && trobs[i].tilepos == tilepos) return i;
return -1;
}
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type)
{
int f = find_trob(room, tilepos);
if (f >= 0) { trobs[f].type = type; return; }
if (trobs_count >= TROBS_MAX) return;
trobs[trobs_count].room = room;
trobs[trobs_count].tilepos = tilepos;
trobs[trobs_count].type = type;
trobs_count++;
}
void pop_trob_room_changed(void)
{
uint8_t i;
for (i = 0; i < POP_ROOMTILES; i++) { spike_rest[i] = 0; button_rest[i] = 0; trob_drawn[i] = 0; }
rest_pending = 0;
}
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
* тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch,
* seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
void pop_trob_anim_room(uint8_t room)
{
uint8_t *mod = pop_trob_modif(room);
uint8_t i, code;
if (!mod) return;
pop_level_access_begin();
for (i = 0; i < POP_ROOMTILES; i++) {
code = (uint8_t)(pop_level_tile_raw(room, i) & 0x1F);
if (code == TILE_TORCH || code == TILE_TORCH_DEBRIS) {
mod[i] = (uint8_t)trob_prandom(8);
pop_add_trob(room, i, 1);
} else if (code == TILE_POTION) {
mod[i] = (uint8_t)((mod[i] & 0xF8) | (trob_prandom(6) + 1));
pop_add_trob(room, i, 1);
}
}
pop_level_access_end();
}
/* animate_spike (seg007:04D3): автомат по modifier. *m — modifier тайла,
* *type — тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4),
* на 5 → 0x8F (полностью вышли, безвредны), на 9 → 0 + конец. Убирание
* (флаг 0x80): --modif; когда 0x7F-часть обнулилась → modif=6 (начать
* визуальный уход 6..8). 0xFF = disabled. */
/* get_torch_frame (seg007:06CD): случайный кадр 0..8, иначе следующий. */
static uint8_t get_torch_frame(uint8_t curr)
{
uint8_t next = (uint8_t)trob_prandom(255);
if (next != curr && next < 9) return next;
next = (uint8_t)(curr + 1);
if (next >= 9) next = 0;
return next;
}
/* bubble_next_frame (seg007:0759): фазы пузырька 1..7 по кругу. */
static uint8_t bubble_next_frame(uint8_t curr)
{
uint8_t next = (uint8_t)(curr + 1);
return (next >= 8) ? 1 : next;
}
static void animate_spike(uint8_t *m, int8_t *type)
{
uint8_t mod = *m;
if (*type >= 0) {
if (mod == 0xFF) return; /* disabled — не двигать */
if (mod & 0x80) {
--mod;
if (mod & 0x7F) { *m = mod; return; }
mod = 6;
} else {
++mod;
if (mod == 5) mod = 0x8F;
else if (mod == 9) { mod = 0; *type = -1; }
}
}
*m = mod;
}
/* ---- Кнопки/ворота (seg007) --------------------------------------- *
* Таблицы связей LINKLOC/LINKMAP — в pop_level (мутабельная копия в W2).
* doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */
static uint8_t get_doorlink_timer(uint8_t i) { return (uint8_t)(pop_doorlink2(i) & 0x1F); }
static void set_doorlink_timer(uint8_t i, uint8_t v)
{
pop_doorlink2_set(i, (uint8_t)((pop_doorlink2(i) & 0xE0) | (v & 0x1F)));
}
static uint8_t get_doorlink_tile(uint8_t i) { return (uint8_t)(pop_doorlink1(i) & 0x1F); }
static uint8_t get_doorlink_next(uint8_t i) { return (uint8_t)(!(pop_doorlink1(i) & 0x80)); }
static uint8_t get_doorlink_room(uint8_t i)
{
return (uint8_t)(((pop_doorlink1(i) & 0x60) >> 5) + ((pop_doorlink2(i) & 0xE0) >> 3));
}
/* trigger_gate (seg007:092C): изменить openness ворот по типу кнопки.
* Возвращает тип анимации для add_trob (или -1 = ничего). */
static int8_t trigger_gate(uint8_t room, uint8_t tilepos, uint8_t button_type)
{
uint8_t *mod = pop_trob_modif(room);
uint8_t modifier = mod[tilepos];
if (button_type == TILE_OPENER) {
if (modifier == 0xFF) return -1; /* уже насовсем открыт */
if (modifier >= 188) { mod[tilepos] = 238; return -1; } /* открыт → держать */
mod[tilepos] = (uint8_t)((modifier + 3) & 0xFC);
return 1; /* обычное открытие */
} else if (button_type == TILE_DEBRIS) { /* насовсем (died_on_button) */
if (modifier < 188) return 2;
mod[tilepos] = 0xFF;
return -1;
} else { /* closer */
if (modifier != 0) return 3; /* быстро закрыть */
return -1;
}
}
/* trigger_1 (seg007:0999): цель = ворота → trigger_gate; иначе -1 (leveldoor
* и прочее — отложено). */
static int8_t trigger_1(uint8_t target_type, uint8_t room, uint8_t tilepos, uint8_t button_type)
{
if (target_type == TILE_GATE)
return trigger_gate(room, tilepos, button_type);
return -1;
}
/* do_trigger_list (seg007:09E5): обойти цепочку LINKLOC от index, для каждой
* цели добавить trob (если trigger_1 >= 0). */
static void do_trigger_list(uint8_t index, uint8_t button_type)
{
for (;;) {
uint8_t room = get_doorlink_room(index);
uint8_t tilepos = get_doorlink_tile(index);
uint8_t target_type = pop_level_tile(room, tilepos);
int8_t result = trigger_1(target_type, room, tilepos, button_type);
if (result >= 0) pop_add_trob(room, tilepos, result);
if (get_doorlink_next(index) == 0) break;
index++;
}
}
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier)
{
uint8_t link_timer = get_doorlink_timer(modifier);
if (link_timer != 0x1F) { /* не заклинено */
set_doorlink_timer(modifier, 5);
if (link_timer < 2)
pop_add_trob(room, tilepos, 1); /* кнопка нажимается (+ TODO sound) */
do_trigger_list(modifier, button_type);
}
}
/* animate_button (seg007:0D3A): отсчитать таймер связи; <2 → кнопка
* отжимается (trob кончился). idx = модификатор кнопки (индекс LINKLOC). */
static void animate_button(uint8_t idx, int8_t *type)
{
if (*type >= 0) {
uint8_t timer = get_doorlink_timer(idx);
timer = (uint8_t)(timer ? timer - 1 : 0);
set_doorlink_timer(idx, timer);
if (timer < 2) *type = -1;
}
}
/* animate_door (seg007:0522): openness по типу анимации. *m — modif ворот,
* *type — 0 закрытие / 1 открытие / 2 насовсем / 3..8 быстрое закрытие. */
static const uint8_t gate_close_speeds[9] = {0, 0, 0, 20, 40, 60, 80, 100, 120};
static const int8_t door_delta[3] = {-1, 4, 4};
static void animate_door(uint8_t *m, int8_t *type)
{
int8_t anim_type = *type;
uint8_t mod = *m;
if (anim_type >= 0) {
if (anim_type >= 3) { /* быстрое закрытие */
if (anim_type < 8) { anim_type++; *type = anim_type; }
{
int new_mod = (int)mod - gate_close_speeds[anim_type];
if (new_mod < 0) { mod = 0; *type = -1; }
else mod = (uint8_t)new_mod;
}
} else if (mod != 0xFF) {
mod = (uint8_t)(mod + door_delta[anim_type]);
if (anim_type == 0) { /* закрытие */
if (mod == 0) *type = -1; /* gate_stop */
} else { /* открытие */
if (mod >= 188) {
if (anim_type < 2) { mod = 238; *type = 0; } /* открыт → потом закрывать */
else { mod = 0xFF; *type = -1; } /* насовсем */
}
}
} else {
*type = -1;
}
}
*m = mod;
}
uint8_t pop_trob_room_active(uint8_t room)
{
uint8_t i;
for (i = 0; i < trobs_count; i++)
if (trobs[i].room == room) return 1;
return 0;
}
static uint8_t torch_tick; /* делитель скорости анимации пламени */
void pop_process_trobs(uint8_t cur_room)
{
uint8_t i, w = 0;
torch_tick++;
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
if (rest_pending) {
uint8_t row = 0, col = 0, left = 0;
for (i = 0; i < POP_ROOMTILES; i++) {
if (spike_rest[i]) {
pop_spike_redraw(row, col);
if (--spike_rest[i]) left++;
}
if (button_rest[i]) {
pop_button_redraw(row, col);
if (--button_rest[i]) left++;
}
if (++col == 10) { col = 0; row++; }
}
rest_pending = left;
}
if (trobs_count == 0) return;
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
* pop_level_tile() на каждый trob (тот свапал W0 каждый вызов = N свапов
* за кадр даже без анимации). Дальше цикл (с редроями) идёт с
* размапленным W0 — как раньше. */
{
uint8_t k;
pop_level_access_begin();
for (k = 0; k < trobs_count; k++)
trob_code[k] = pop_level_tile_raw(trobs[k].room, trobs[k].tilepos);
pop_level_access_end();
}
for (i = 0; i < trobs_count; i++) {
uint8_t room = trobs[i].room;
uint8_t tp = trobs[i].tilepos;
uint8_t code = trob_code[i];
uint8_t *mod = pop_trob_modif(room);
int8_t type = trobs[i].type;
switch (code) {
case TILE_SPIKE:
animate_spike(&mod[tp], &type);
break;
case TILE_OPENER:
case TILE_CLOSER:
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
break;
case TILE_GATE:
animate_door(&mod[tp], &type);
break;
case TILE_TORCH:
case TILE_TORCH_DEBRIS:
/* animate_torch (seg007:03C1): кадр пламени; анимация вечная —
* type остаётся 1, пока комната отрисована. Смена кадра — через
* ЛОГИЧЕСКИЙ кадр (torch_tick): у нас кадр короче игрового тика
* оригинала, и пламя «кипело» вдвое быстрее нужного. */
if (room == cur_room && (torch_tick & 1) == 0)
mod[tp] = get_torch_frame(mod[tp]);
break;
case TILE_POTION:
/* animate_potion (seg007:03E9): крутим младшие 3 бита (фаза
* пузырька), старшие 5 — тип зелья. */
if (room == cur_room)
mod[tp] = (uint8_t)((mod[tp] & 0xF8) | bubble_next_frame(mod[tp] & 7));
break;
default:
type = -1;
break;
}
trobs[i].type = type;
if (room == cur_room &&
(code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) {
/* Пламя/пузырёк анимируются КАЖДЫЙ кадр и в фон не запечены —
* рисуем поверх (сами делают heal своей области). */
uint8_t trow = tp / 10, tcol = tp % 10;
if (code == TILE_POTION) pop_potion_draw(trow, tcol, mod[tp]);
else pop_torch_draw(trow, tcol, mod[tp]);
}
if (room == cur_room && code == TILE_SPIKE) {
/* ПИКИ — редрой КАЖДЫЙ кадр, пока trob жив: animate_spike
* (seg007:0353) зовёт redraw_21h БЕЗУСЛОВНО, а не по смене кадра.
* Это не «лишняя работа оригинала»: выдвинутая пика живёт только в
* видео-ОЗУ (банк SPRITE, в фон не запечена), а kid_heal каждый кадр
* возвращает под спрайтом Кида ПЕЧЁНЫЙ фон — то есть стирает те
* части остриёв, что попали под его прямоугольник. При редрое
* по смене кадра пика держится 15 кадров (modif 0x8F..0x81) с
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
* странице дабл-буфера из двух). */
uint8_t trow = tp / 10, tcol = tp % 10;
pop_spike_redraw(trow, tcol);
/* Последний кадр (пика убралась, trob умирает) — на ВТОРУЮ страницу
* его уже никто не положит: ставим отложенный редрой. */
if (type < 0) { spike_rest[tp] = 1; rest_pending++; }
}
if (room == cur_room && (code == TILE_OPENER || code == TILE_CLOSER)) {
/* КНОПКА — редрой только при смене видимого состояния (нажата =
* таймер связи >1): пока Кид её держит, картинка стабильна.
* Рисуем ТЕКУЩУЮ страницу сразу + <rest>=1 (вторая страница
* дабл-буфера следующим кадром через rest-цикл). tp/10,tp%10
* считаем только в момент редроя (редко), а не каждый кадр. */
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
if (sig != trob_drawn[tp]) {
uint8_t trow = tp / 10, tcol = tp % 10;
pop_button_redraw(trow, tcol);
button_rest[tp] = 1; rest_pending++;
trob_drawn[tp] = sig;
}
}
}
/* compact: удалить завершённые (type < 0). */
for (i = 0; i < trobs_count; i++)
if (trobs[i].type >= 0) trobs[w++] = trobs[i];
trobs_count = w;
}
+65
View File
@@ -0,0 +1,65 @@
/*
* pop_trob.h — P0-каркас: персистентное per-room modifier-состояние +
* список анимируемых тайлов (trob). Порт SDLPoP seg007 (process_trobs/
* add_trob/animate_tile) + curr_room_modif.
*
* ЗАЧЕМ: тайлы-объекты (пики, ворота, кнопки) имеют состояние (modifier),
* которое живёт МЕЖДУ входами в комнату и может меняться КРОСС-КОМНАТНО
* (кнопка в одной комнате открывает ворота в другой). room_modif[room]
* — единственный источник модификаторов для отрисовки (pop_bg draw_tile
* читает его как bg), как curr_room_modif в оригинале.
*
* Список trob хранит (room, tilepos, type) активных анимаций; каждый кадр
* pop_process_trobs продвигает их (animate_spike/door/button по коду тайла),
* пишет результат в room_modif и перерисовывает изменившиеся тайлы ТЕКУЩЕЙ
* комнаты на back-странице (2-page, как loose). Невидимые комнаты
* анимируются в состоянии (без отрисовки) — при входе рисуются статически.
*/
#ifndef POP_TROB_H
#define POP_TROB_H
#include <stdint.h>
#define POP_NROOMS 24
#define POP_ROOMTILES 30
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
* очистить список trob. */
void pop_trob_reset(void);
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
* (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне
* диапазона → 0. */
uint8_t *pop_trob_modif(uint8_t room);
/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить
* type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type);
/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать
* modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице.
* Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */
void pop_process_trobs(uint8_t cur_room);
/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку
* LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button.
* room/tilepos — кнопка; button_type — её тайл-код; modifier — её modif
* (= индекс в LINKLOC). Звать из check_press (pop_map). */
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier);
/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через
* шов (roomtest не дёргает шов, пока сосед не анимируется). */
uint8_t pop_trob_room_active(uint8_t room);
/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты
* (trob-состояние в room_modif персистентно и НЕ трогается). Звать в
* enter_room. */
void pop_trob_room_changed(void);
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
* зелья) со случайной стартовой фазой. Звать в enter_room. */
void pop_trob_anim_room(uint8_t room);
#endif
-10
View File
@@ -1,10 +0,0 @@
/* room1_data.h — данные комнаты 1 ур.1 (res2001) + левый столбец
* соседа (room 5 col9) для границы-решётки. Сгенерировано
* gen из render_room; fg=foretable code, bg=backtable modifier. */
#ifndef ROOM1_DATA_H
#define ROOM1_DATA_H
static const unsigned char room1_fg[30] = {0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x14,0x14,0x13,0x13,0x01,0x03,0x00,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x0E,0x03,0x0B,0x01,0x01,0x14};
static const unsigned char room1_bg[30] = {0xFF,0xFF,0xFF,0x00,0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00};
static const unsigned char leftcol_fg[3] = {0x04,0x14,0x14};
static const unsigned char leftcol_bg[3] = {0x01,0x00,0x00};
#endif
+330 -15
View File
@@ -13,19 +13,177 @@
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "room1_data.h"
#include "pop_level.h"
#include "pop_trob.h"
__sfr __at (0xFE) io_border;
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2
#define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
/* Тест-старт для отладки пик (Фаза S): комната 6 = тестовая (пики в col3/col4
* row2, шахта col3). Kid на полу row0 col1, идёт вправо → падает в шахту →
* пики. TODO: вернуть на pop_level_start_room (комната 1) после отладки. */
#define START_ROOM 1 /* обход комнат: старт с 1-й, дальше +/- (ROOMNAV) */
#define START_COL 3
#define START_ROW 0
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
* ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (1..24, с обёрткой),
* Kid ставится на первый попавшийся пол. Нужен, чтобы пройти все комнаты и
* собрать список багов отрисовки, включая недостижимые обычным путём.
* Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */
static uint8_t room_fg[30], room_bg[30];
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
* смотрит наружу (колонка −2 / 11). См. pop_level.h. */
static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[10];
static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid
* слева от отрисованной → −140 (спрайт за левой кромкой), справа → +140, та
* же → 0. Звать каждый кадр перед kid_draw. */
static void update_kid_render_dx(void)
{
int dx = 0;
if (kid_room != cur_room) {
if (kid_room == pop_room_link(cur_room, 0)) dx = -140; /* left neighbour */
else if (kid_room == pop_room_link(cur_room, 1)) dx = 140; /* right neighbour */
}
pop_kid_set_render_dx(dx);
}
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
* → 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен
* — тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера. */
static uint8_t seam_sig; /* последняя нарисованная openness гейта в шве */
static uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */
/* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая):
* sig = последняя показанная комната, redraw = остаток кадров (2 = обе страницы). */
static uint8_t room_label_sig = 0xFF;
static uint8_t room_label_redraw;
/* Персистентные переопределения тайлов уровня: упавший loose-кусок оставил
* щебень (floor → debris), а на месте самой плиты — пусто (в т.ч. в комнате
* СВЕРХУ: провалившийся потолок = колодец). Минимальный per-room override:
* живёт между переходами, применяется в enter_room поверх загруженных тайлов
* (и для ряда 2 верхнего соседа). (Полноценное per-room modifier-состояние —
* в docs/gates_spikes_plan.md, P0.) */
#define TILE_DEBRIS 0x0E
#define TILE_EMPTY 0x00
#define OVR_MAX 8
static uint8_t ovr_room[OVR_MAX], ovr_pos[OVR_MAX], ovr_tile[OVR_MAX], ovr_cnt;
static void add_tile_override(uint8_t room, uint8_t pos, uint8_t tile)
{
uint8_t i;
for (i = 0; i < ovr_cnt; i++)
if (ovr_room[i] == room && ovr_pos[i] == pos) { ovr_tile[i] = tile; return; }
if (ovr_cnt < OVR_MAX) {
ovr_room[ovr_cnt] = room; ovr_pos[ovr_cnt] = pos;
ovr_tile[ovr_cnt] = tile; ovr_cnt++;
}
}
/* Применить переопределения комнаты room к массиву тайлов: base = tilepos
* первого элемента (0 для полной комнаты, 20 для ряда 2 верхнего соседа). */
static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t n)
{
uint8_t i;
for (i = 0; i < ovr_cnt; i++)
if (ovr_room[i] == room && ovr_pos[i] >= base && ovr_pos[i] < base + n)
tiles[ovr_pos[i] - base] = ovr_tile[i];
}
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
* активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
static void enter_room(uint8_t room)
{
uint8_t pg;
uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */
pop_map_set(room_fg);
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
pop_room_link(room, 2), pop_room_link(room, 3),
lcol_fg, rcol_fg);
pop_room_set_below(below_fg);
/* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика
* уровня, mod — живой room_modif (ленивая инициализация). */
{
uint8_t up = pop_room_link(room, 2), c;
if (up) {
uint8_t *um = pop_trob_modif(up);
pop_level_access_begin();
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
pop_level_access_end();
apply_overrides(up, above_fg, 20, 10); /* провалившийся потолок = колодец */
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
pop_room_set_above(above_fg, above_mod);
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
} else {
pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */
pop_map_set_above(0);
}
}
pop_map_set_room(room);
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
* а не по свежему backtable. Фаза P0. */
room_mod = pop_trob_modif(room);
/* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот,
* видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6).
* pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */
{
uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1);
if (lft) { uint8_t *m = pop_trob_modif(lft);
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
seam_sig = m[9]; } /* шов рисуется в обе страницы ниже — синхр. сигнатуру */
if (rgt) { uint8_t *m = pop_trob_modif(rgt);
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; }
}
seam_redraw = 0; /* шов свежий на обеих страницах — редрой не нужен */
for (pg = 0; pg < 2; pg++) {
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
}
cur_room = room;
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
}
int main(void)
{
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
* по умолчанию (без мерцания); SPACE переключает в однобуфер для
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
uint8_t pg, front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint16_t dead_frames = 0;
#ifdef ROOMNAV
uint8_t nav_prev = 0;
#endif
if (pop_bg_load() != 0) { /* атласы — ДО initgraph */
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
puts("pop_bg_load failed");
return 1;
}
@@ -33,6 +191,11 @@ int main(void)
puts("kid atlas load failed");
return 1;
}
if (pop_level_load("res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */
pop_level_load("a:\\res2001.bin") != 0) {
puts("level load failed");
return 1;
}
initgraph();
gfx_set_visible_page(0);
@@ -44,18 +207,17 @@ int main(void)
* ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
* страницы, в которую рисуем. Дальше каждый тик восстанавливаем
* ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
for (pg = 0; pg < 2; pg++) {
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
pop_room_draw(1, room1_fg, room1_bg, leftcol_fg, leftcol_bg);
}
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
pop_trob_reset();
enter_room(START_ROOM);
gfx_set_draw_page(0);
/* K2b: Kid стоит на уступе (row1), управляется с клавиатуры.
* ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. */
pop_map_set(room1_fg); /* карта для коллизии/падения (K3) */
kid_init(SEQ_STAND, 1, 1, 0);
* ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход.
* Карта коллизии (pop_map_set) уже задана в enter_room. */
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
pop_kid_hp_reset();
kid_draw();
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(),
@@ -70,6 +232,9 @@ int main(void)
while (!kbd_raw_down(KBD_ESC)) {
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */
io_border = 2; /* синий - начало */
uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) {
dbuf ^= 1;
@@ -86,30 +251,179 @@ int main(void)
if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0;
if (frozen) {
io_border = 0; /* черный: ждём vsync */
gfx_wait_vsync();
continue;
}
#ifdef ROOMNAV
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
* комната. pop_trob_reset ДО enter_room — каждая комната смотрится в
* исходном состоянии (пики убраны, ворота закрыты). */
{
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
if (nav && !nav_prev) {
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
: (uint8_t)(r == 1 ? 24 : r - 1);
pop_trob_reset();
enter_room(r);
/* Предпочитаем обычный пол; иначе — любой tile_is_floor
* (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
for (i = 0; i < 30; i++) {
c = (uint8_t)(room_fg[i] & 0x1F);
if (c == 1) { any = i; break; }
if (any == 0xFF && c && c != 9 && c != 12 && c != 20 &&
!(c >= 26 && c <= 29)) any = i;
}
if (any != 0xFF) pos = any;
kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0);
pop_kid_hp_reset();
dead_frames = 0;
}
nav_prev = nav;
}
#endif
/* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0. */
back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back);
pop_ctrl_tick(); /* ввод -> control(): смена seq */
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */
if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0;
kid_init(SEQ_STAND, 1, 1, 0);
if (down) { /* L2: переход в комнату снизу — продолжает падать */
enter_room(down); /* координаты Kid уже репроецированы pop_map */
gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */
} else { /* выпал из уровня (нет комнаты снизу) — респавн */
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */
enter_room(START_ROOM);
gfx_set_draw_page(back);
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
pop_kid_hp_reset();
}
kid_draw(); /* нарисовать новый кадр */
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
pop_leave_dir = 0;
if (nb) {
enter_room(nb);
gfx_set_draw_page(back);
}
}
io_border = 4; /* зеленый */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
io_border = 6; /* голубой */
pop_process_trobs(cur_room); /* пики/ворота: продвинуть + перерисовать (тоже */
/* слой фона — ДО Kid) */
io_border = 4; /* синий */
{ /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) {
uint8_t *m = pop_trob_modif(lft);
uint8_t g = m[9], prev = seam_sig;
seam_sig = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */
/* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */
/* не g — sdcc_z80_cmp_store_a_bug) */
if (g != prev) seam_redraw = 2; /* openness изменился → обе страницы дабл-буфера */
if (seam_redraw) {
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
pop_room_redraw_seam_left();
seam_redraw--;
}
}
}
io_border = 2; /* зеленый */
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++;
if (dead_frames > RESPAWN_SETTLE &&
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
dead_frames = 0;
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */
enter_room(START_ROOM);
gfx_set_draw_page(back);
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
pop_kid_hp_reset();
}
}
io_border = 6; /* голубой */
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0;
io_border = 2; /* синий */
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) {
int8_t lrow = pop_room_col_landing(down, fcol);
if (lrow >= 0) add_tile_override(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
}
}
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
uint8_t up = pop_room_link(cur_room, 2);
pop_ceil_fell = 0;
if (up) add_tile_override(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
}
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
add_tile_override(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
pop_debris_at = 0;
}
io_border = 4; /* зеленый */
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash();
pop_kid_hurt = 0;
}
io_border = 0; /* черный */
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action);
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже
* сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */
io_border = 0; /* черный: ждём vsync */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
#ifdef ROOMNAV
{
uint8_t r = cur_room, prev = room_label_sig;
room_label_sig = r; /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */
if (r != prev) room_label_redraw = 2;
if (room_label_redraw) {
/* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный
* знакогенератор (_gfx_font_buf, 2 КБ статики в W2) и сажает
* кучу с 3867 до 576 Б. Слева десятки, справа единицы:
* || |||| = 24. bar/setfillstyle уже используются. */
uint8_t i;
setfillstyle(SOLID_FILL, BLACK);
bar(100, 0, 175, 5);
/* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная
* (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */
setfillstyle(SOLID_FILL, 0x57);
for (i = 0; i < (uint8_t)(r / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5);
for (i = 0; i < (uint8_t)(r % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5);
room_label_redraw--;
}
}
#endif
gfx_wait_vsync();
gfx_wait_vsync();
gfx_wait_vsync();
@@ -123,5 +437,6 @@ int main(void)
closegraph();
pop_bg_free();
pop_kid_free();
pop_level_free();
return 0;
}
+140 -4
View File
@@ -50,6 +50,66 @@ FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100}
# попадает в used render_room, добавляем явно в env-фон. (32 обычно уже есть.)
CLIMB_OVERLAY_ENV_IDS = {32, 150, 151}
# Дрожащие/падающие кадры loose-плиты (loose_fram_left/right/bottom, seg008):
# 41/42/43 = покой (уже в used из render_room), 69-74 = вибрация/раздвоение —
# используются ТОЛЬКО динамикой (shake/fall), в статический render_room НЕ
# попадают, поэтому добавляем явно. Без них дрожащая плита невидима.
LOOSE_ANIM_ENV_IDS = {69, 70, 71, 72, 73, 74}
# Кадры ВЫДВИЖЕНИЯ пик (seg008): три части — left (128-132, своя ячейка,
# draw_tile_anim), right (134-138, правый сосед, draw_tile_anim_right), fore
# (139-143, поверх Kid, draw_tile_fore). Рисуются ТОЛЬКО динамикой
# (animate_spike); в уровнях пики статически убраны (modif=0 → кадр 0),
# поэтому в used render_room НЕ попадают. Без них выдвинутые пики невидимы.
SPIKE_ANIM_ENV_IDS = {128, 129, 130, 131, 132, # left
134, 135, 136, 137, 138, # right
139, 140, 141, 142, 143} # fore
# Кадры ОТКРЫТИЯ ворот (portcullis, DOOR_FRAM_SLICE + gate pieces 50/51,
# seg008 draw_gate): 50-59 + 67. В уровнях ворота статически ЗАКРЫТЫ
# (низкий modif → используются только нижние срезы 50-53); срезы более
# открытых состояний (54-59, 67) нужны анимации открытия — добавляем явно.
GATE_ANIM_ENV_IDS = {50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 67}
# НАЖАТАЯ кнопка-closer (get_tile_to_draw seg008:2FE: при таймере связи >1
# tiles_6_closer рисуется как tiles_5_stuck). tile_table[5] = база 41 (уже
# есть), правая грань 35, нижняя 36 — оба id встречаются ТОЛЬКО в этом
# динамическом состоянии, статически тайла 5 в уровнях нет, значит в used
# render_room они не попадают. Без них у нажатой кнопки чёрный провал на
# месте нижней грани и потерянная правая грань.
STUCK_ENV_IDS = {35, 36}
# Левая половина кнопки-opener, когда СЛЕВА пусто (draw_tile_base seg008:0A8E:
# curr_tile==15 && tile_left==0 && level_type==0 → id 148 вместо базы 147).
# render_room этой ветки не знает, поэтому добавляем явно.
OPENER_NOFLOOR_ENV_IDS = {148}
# ---- chtab_1 (flame/sword/potion), база ресурса 150, папка PRINCE ----
# Зелье (seg008 draw_tile_fore/draw_tile_anim): склянка id 12 (малая) / 13
# (большая, типы 2..4), кадры пузырька 16..22 (potion_fram_bubb) и его маска
# 23. Пузырёк и маска рисуются MONO-блитом (одним цветом), поэтому пакуем их
# силуэтами уже в нужном цвете: маска — чёрный env[0], пузырёк — env[12]
# (красный, тип «лечение»). Цвета других типов (9 синий / 10 зелёный) —
# TODO, когда появятся такие зелья.
POT_DIR_NAME = "PRINCE"
POT_FLAME_IDS = tuple(range(1, 10)) # пламя факела: modifier+1 (seg008:560)
POT_FLASK_IDS = (12, 13)
POT_BUBBLE_IDS = (16, 17, 18, 19, 20, 21, 22)
POT_MASK_ID = 23
POT_PAL_BASE = 0x40 # 16 цветов chtab_1 -> слоты 0x40..0x4F
# MONO-блит (method_3_blit_mono, seg009:3040) красит спрайт цветом из ОБЩЕЙ
# 16-цветной палитры экрана, а не из палитры chtab. Кладём её в 0x30..0x3F.
VGA16_PAL_BASE = 0x30
VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(42,21,0),
(42,42,42),(21,21,21),(21,21,63),(21,63,21),(21,63,63),(63,21,21),
(63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal
POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета)
POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение»)
# Основание факела (seg008:489 draw_tile_right: при левом соседе-факеле
# рисуется chtab_6 id 146). В статический render_room не попадает.
TORCH_ENV_IDS = {146}
ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31
ENV_MASK = (1 << ENV_SHIFT) - 1
@@ -96,9 +156,22 @@ def collect_used():
# ---- 2. Загрузка спрайта в 8bpp-блоб ---------------------------------
def load_indexed(chtab, id):
# Спрайты chtab_6, которые оригинал рисует БЕЗ прозрачности
# (blitters_0_no_transp): нижние грани тайлов (draw_tile_bottom, seg008:570) и
# нижние кадры loose (draw_loose, seg008:600). У 96/149 (грани кнопок) и 73/74
# (кадры loose) ПОСЛЕДНЯЯ строка — пиксель 0; в оригинале она заливается ЧЁРНЫМ
# поверх того, что сзади, а у нас 0 -> 0xFF делал её прозрачной, и сквозь неё
# просвечивал Kid (спуск/подъём у кромки кнопки). 43/36 полностью непрозрачны —
# для них флаг ничего не меняет, оставлены для полноты списка.
NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149}
def load_indexed(chtab, id, opaque=False):
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60)."""
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).
opaque=True пиксель 0 остаётся ЦВЕТОМ (base+0 = чёрный), а не ключом
прозрачности: порт blitters_0_no_transp (см. NO_TRANSP_ENV_IDS)."""
base_res = 360 if chtab == R.CHTAB_WALL else 200
pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50
fname = f"res{base_res + id}.png"
@@ -114,10 +187,35 @@ def load_indexed(chtab, id):
if im.mode != "P":
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
src = im.tobytes()
if opaque:
out = bytes((pal_base + v) for v in src)
else:
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
return im.width, im.height, out
def load_potion(id, mono=None, opaque=False):
"""chtab_1: res(150+id).png из data/PRINCE. mono=None — обычный спрайт
(палитра chtab_1 -> слоты POT_PAL_BASE+i); mono=цвет силуэт этим цветом
(порт blitters_40h_mono); opaque=True пиксель 0 остаётся ЦВЕТОМ
(порт blitters_0_no_transp, см. POT_FLAME_IDS)."""
path = os.path.join(os.path.dirname(R.VDUNGEON_DIR), POT_DIR_NAME,
f"res{150 + id}.png")
if not os.path.exists(path):
die(f"нет файла {path}")
im = Image.open(path)
if im.mode != "P":
die(f"res{150+id}: mode {im.mode}, ожидался P")
src = im.tobytes()
if mono is not None:
out = bytes((0xFF if v == 0 else mono) for v in src)
elif opaque:
out = bytes((POT_PAL_BASE + v) for v in src)
else:
out = bytes((0xFF if v == 0 else POT_PAL_BASE + v) for v in src)
return im.width, im.height, out
def blob(w, h, pix):
"""getimage-блоб: u16 w, u16 h (LE) + пиксели."""
return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix
@@ -185,6 +283,8 @@ def build_palette(out_path):
env = read_pal("res200.pal", R.VDUNGEON_DIR)
wall = read_pal("res360.pal", R.VDUNGEON_DIR)
pot = read_pal("res150.pal", os.path.join(os.path.dirname(R.VDUNGEON_DIR),
POT_DIR_NAME))
pal = bytearray(256 * 4)
for i, (r, g, b) in enumerate(env):
o = (0x50 + i) * 4
@@ -192,6 +292,12 @@ def build_palette(out_path):
for i, (r, g, b) in enumerate(wall):
o = (0x60 + i) * 4
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
for i, (r, g, b) in enumerate(pot):
o = (POT_PAL_BASE + i) * 4
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
for i, (r, g, b) in enumerate(VGA16):
o = (VGA16_PAL_BASE + i) * 4
pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0))
with open(out_path, "wb") as f:
f.write(pal)
@@ -228,6 +334,8 @@ def write_header(out_path, env_pages, wall_name, fore_name):
"};",
'#define POP_WALL_ATL "%s"' % wall_name,
'#define POP_FORE_ATL "%s"' % fore_name,
'#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья */',
"#define POP_PAL_POT 0x%02X" % POT_PAL_BASE,
'#define POP_BG_PAL "pop_bg.pal"',
"",
"#endif",
@@ -241,7 +349,13 @@ def main():
os.makedirs(OUT_DIR, exist_ok=True)
used = collect_used()
env_ids = sorted(set(id for ch, id in used if ch != R.CHTAB_WALL)
| CLIMB_OVERLAY_ENV_IDS) # + пол-оверлей подъёма
| CLIMB_OVERLAY_ENV_IDS # + пол-оверлей подъёма
| LOOSE_ANIM_ENV_IDS # + дрожащие кадры loose
| SPIKE_ANIM_ENV_IDS # + кадры выдвижения пик
| GATE_ANIM_ENV_IDS # + кадры открытия ворот
| TORCH_ENV_IDS # + основание факела
| STUCK_ENV_IDS # + нажатая кнопка-closer
| OPENER_NOFLOOR_ENV_IDS) # + opener без пола слева
wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL)
env_bg = [i for i in env_ids if i not in FORE_ENV_IDS]
@@ -250,7 +364,7 @@ def main():
# ENV фон -> страницы по id>>SHIFT
pages = {}
for id in env_bg:
w, h, pix = load_indexed(R.CHTAB_ENV, id)
w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS)
pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
max_pg = max(pages) if pages else -1
env_page_names = []
@@ -278,6 +392,28 @@ def main():
total_bytes += sz
print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б")
# POTION (chtab_1) -> idx=id: склянки + кадры пузырька + маска
# chtab_1 целиком (res151..res173 = id 1..23): пламя факелов, меч-предмет,
# склянки зелий. Пузырьки и их маска рисуются MONO-блитом — пакуем
# силуэтами в нужном цвете (см. POT_*_COLOR).
pot_entries = {}
for id in range(1, 24):
if id in POT_BUBBLE_IDS:
pot_entries[id] = load_potion(id, POT_BUBBLE_COLOR)
elif id == POT_MASK_ID:
pot_entries[id] = load_potion(id, POT_MASK_COLOR)
elif id in POT_FLAME_IDS:
# Пламя оригинал рисует blitters_0_no_transp (seg008
# draw_tile_anim_right) — непрозрачным боксом поверх всего, что
# сзади. Все 9 кадров лежат на одном канвасе 16x18, поэтому
# непрозрачный блит сам стирает предыдущий кадр: heal не нужен.
pot_entries[id] = load_potion(id, opaque=True)
else:
pot_entries[id] = load_potion(id)
cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_pot.atl"), pot_entries)
total_bytes += sz
print(f" pop_pot.atl (chtab_1: факелы+зелья): {len(pot_entries)} спрайтов, каталог {cnt}, {sz} Б")
build_palette(os.path.join(OUT_DIR, "pop_bg.pal"))
write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"),
env_page_names, "pop_wall.atl", "pop_fore.atl")
+8 -1
View File
@@ -69,14 +69,21 @@ def _read16(path):
def build_palette(out_path):
"""ЕДИНАЯ игровая палитра (gfx_pal_fload пишет с индекса 0, перезатирая
всё bg и Kid обязаны жить в одном файле): env@0x50, wall@0x60, kid@0x70.
всё bg и Kid обязаны жить в одном файле): chtab_1@0x40 (пламя/зелья),
env@0x50, wall@0x60, kid@0x70.
"""
pal = bytearray(256 * 4)
groups = [
(0x50, os.path.join(B.R.VDUNGEON_DIR, "res200.pal")), # env
(0x60, os.path.join(B.R.VDUNGEON_DIR, "res360.pal")), # wall
(PAL_BASE, os.path.join(KID_DIR, "res400.pal")), # kid 0x70
(B.POT_PAL_BASE, os.path.join(os.path.dirname(B.R.VDUNGEON_DIR),
B.POT_DIR_NAME, "res150.pal")), # chtab_1 0x40
]
# 16 базовых цветов экрана (mono-блит красит ими: пламя, пузырьки зелья)
for i, (r, g, bl) in enumerate(B.VGA16):
o = (B.VGA16_PAL_BASE + i) * 4
pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0))
for base, path in groups:
for i, (r, g, bl) in enumerate(_read16(path)):
o = (base + i) * 4
+81 -10
View File
@@ -15,8 +15,8 @@
# 0xA2 to auto-detect whether DSS already mapped W2
# (program > 16 KB) or whether we need to allocate
# it ourselves (program < 16 KB). Covers 0..30 KB.
# big: tiny + banked code in W1 [TODO]
# huge: small + banked code in W3 [TODO]
# big: tiny + banked code in W1
# huge: small + banked code in W3
# manual: explicit via --memory-manual / --code-loc / --data-loc
# --memory-manual SPEC placement spec, only with --memory manual.
# SPEC = comma-separated KEY=VAL list:
@@ -33,6 +33,23 @@
# -Wl FLAG extra linker flag (repeatable)
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
# automatically and adds -Wl-b_BANKN=0x{N}C000
# --w3 FILE.c place FILE.c resident in window 3 (0xC000), called
# DIRECTLY (no trampoline). Repeatable. Defaults to
# --memory small; works in tiny|small|big|huge. DSS maps
# all image pages incl. W3. Compiled with --codeseg/
# --constseg W3CODE (writable statics stay in W2). In huge
# the resident page SHARES W3 with trampoline banks: crt0
# captures/restores it (W3_RESIDENT), mkexe gets -W.
# Rules: W3 code must NOT switch the W3 page — this includes
# opening a graphics batch bracket itself (_bgi_begin/_bgi_end
# map the video bank via the SAME port 0xE2; doing that from
# W3 code executes from video RAM -> white screen). Calling
# HOME graphics primitives is fine: they open and close the
# bracket internally and restore the page before returning.
# W1/W2 code that swaps W3 to another page must restore it;
# no writable data belongs in W3 (code + rodata only). From a
# __banked context the resident W3 code is unreachable (but
# resident -> __banked via the W1 trampoline is fine).
# --mkexe FLAG extra mkexe flag (repeatable; e.g. --mkexe -p --mkexe 0)
# --max-allocs N SDCC --max-allocs-per-node (default: 100000 — smaller/
# faster code at the cost of compile time; pass a lower
@@ -88,6 +105,11 @@ SOURCES=()
LD_EXTRA=()
MKEXE_EXTRA=()
BANK_SPECS=() # entries like "1=engine.c"
W3_SPECS=() # entries like "mod.c" — резидентные модули окна W3 (--w3)
W3_RELS=() # заполняется при компиляции W3-модулей
W3_LD_FLAGS=() # -Wl-b_W3CODE=0xC000, если есть --w3
USER_SET_MEMORY="" # непусто, если --memory задан явно (для --w3 авто-small)
W3_RESIDENT_HUGE=0 # 1, если --w3 в режиме huge (резидент делит W3 с банками)
MAX_ALLOCS="100000" # sdcc --max-allocs-per-node; дефолт 100000 — агрессивная
# регистровая аллокация (медленнее компиляция, меньше/
# быстрее код; как fast-сборки библиотек). --max-allocs N
@@ -112,8 +134,9 @@ while [[ $# -gt 0 ]]; do
-S) STACK_ADDR="$2"; shift 2;;
--code-loc) USER_CODE_LOC="$2"; shift 2;;
--data-loc) USER_DATA_LOC="$2"; shift 2;;
--memory) MEMORY_MODE="$2"; shift 2;;
--memory) MEMORY_MODE="$2"; USER_SET_MEMORY=1; shift 2;;
--memory-manual) MEMORY_MANUAL="$2"; shift 2;;
--w3) W3_SPECS+=("$2"); shift 2;;
--stack-size) STACK_SIZE="$2"; shift 2;;
-Wl) LD_EXTRA+=("$2"); shift 2;;
--bank) BANK_SPECS+=("$2"); shift 2;;
@@ -132,6 +155,29 @@ done
[[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; }
[[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; }
# ------- --w3: резидентный код окна W3 (подход A) ----------------------------
# W3-модули едут в область W3CODE @0xC000 и вызываются напрямую (без
# трамплинов). Работает поверх small-лейаута: main-код с 0x4100 (W1/W2),
# W3 свободно, DSS маппит все страницы образа сам (проверено tests/w3probe).
# --w3 подразумевает small ПО УМОЛЧАНИЮ, но поддержан во всех режимах
# tiny|small|big|huge. В huge резидент ДЕЛИТ окно W3 с трамплин-банками
# (порт 0xE2): crt0_banked запоминает резидентную страницу и возвращает её
# дефолтом после загрузки банков, а трамплин bank.s сохраняет/восстанавливает
# W3 на каждый __banked вызов (см. W3_RESIDENT ниже). Следствие: резидент ->
# __banked работает, обратное — НЕТ (из банка резидентной страницы в W3 нет).
if [[ ${#W3_SPECS[@]} -gt 0 ]]; then
[[ -z "$USER_SET_MEMORY" ]] && MEMORY_MODE="small" # --w3 → small по умолчанию
case "$MEMORY_MODE" in
tiny|small|big|huge) : ;; # поддержаны (huge — резидент делит W3 с банками)
*) echo "sprinter-cc: --w3 поддержан для tiny|small|big|huge (дано: $MEMORY_MODE)" >&2; exit 1;;
esac
# huge: резидентный W3-код (0xC000, HOME) сосуществует с трамплин-банками
# W3 — crt0_banked захватывает резидентную страницу и возвращает её
# дефолтом после загрузки банков (см. crt0_banked.s, W3_RESIDENT).
W3_RESIDENT_HUGE=0
[[ "$MEMORY_MODE" == "huge" ]] && W3_RESIDENT_HUGE=1
fi
# ------- BGI graphics driver selection (--gfx) -------------------------------
# graphics.h — mode-agnostic слой; конкретный видеорежим задаёт driver-
# архив. Одновременно только один. drv16 пока не реализован.
@@ -186,7 +232,8 @@ case "$MEMORY_MODE" in
huge)
# small layout + banked code in W3. crt0_banked auto-detects W2
# the same way crt0_small does, then loads banks from the .EXE.
MODE_CODE_LOC="0x4100"; MODE_DATA_LOC="0x8000"
# MODE_CODE_LOC="0x4100"; MODE_DATA_LOC="0x8000"
MODE_CODE_LOC="0x4100"; MODE_DATA_LOC="0"
;;
manual)
# Defaults if SPEC omits a key.
@@ -280,6 +327,7 @@ asm_runtime() {
local prefix=""
[[ $DEBUG_RT -eq 1 ]] && prefix+="DEBUG_RT = 1"$'\n'
[[ $BANK_W1 -eq 1 ]] && prefix+="BANK_W1 = 1"$'\n'
[[ $W3_RESIDENT_HUGE -eq 1 ]] && prefix+="W3_RESIDENT = 1"$'\n'
if [[ -n "$prefix" ]]; then
local patched="$WORK/$(basename "$src" .s)_patched.s"
{ printf "%s" "$prefix"; cat "$src"; } > "$patched"
@@ -305,9 +353,14 @@ if [[ -n "$STACK_SIZE" ]]; then
printf " .module sprinter_heap_top\n ___sdcc_heap_end = 0x%04X\n .globl ___sdcc_heap_end\n" \
"$_heap_top" > "$WORK/heap_top.s"
HEAP_TOP_SRC="$WORK/heap_top.s"
[[ $VERBOSE -eq 1 ]] && echo " heap_top: custom HEAP_TOP=$(printf '0x%04X' $_heap_top) (stack reserve $_stack_sz bytes)"
HEAP_TOP_VAL=$(printf '0x%04X' "$_heap_top")
[[ $VERBOSE -eq 1 ]] && echo " heap_top: custom HEAP_TOP=$HEAP_TOP_VAL (stack reserve $_stack_sz bytes)"
else
HEAP_TOP_SRC="$RUNTIME/heap_top.s"
# Дефолт из runtime/heap_top.s (для отчёта раскладки) — обычно 0xBB00.
HEAP_TOP_VAL=$(grep -oiE '___sdcc_heap_end[[:space:]]*=[[:space:]]*0x[0-9A-Fa-f]+' "$RUNTIME/heap_top.s" \
| grep -oiE '0x[0-9A-Fa-f]+' | head -1)
HEAP_TOP_VAL="${HEAP_TOP_VAL:-0xBB00}"
fi
run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
@@ -322,6 +375,20 @@ for src in "${SOURCES[@]}"; do
USER_RELS+=("$rel")
done
# 2b. --w3 resident modules → .rel. --codeseg/--constseg W3CODE кладёт код
# и rodata в W3 (0xC000); --dataseg НЕ трогаем — писучие статики
# остаются в обычном _DATA (W2). Прямые вызовы, без трамплинов.
if [[ ${#W3_SPECS[@]} -gt 0 ]]; then
for src in "${W3_SPECS[@]}"; do
rel="$WORK/w3_$(basename "$src" .c).rel"
run "$SDCC" "${CC_FLAGS[@]}" --codeseg W3CODE --constseg W3CODE \
-c -o "$rel" "$src"
W3_RELS+=("$rel")
done
W3_LD_FLAGS+=("-Wl-b_W3CODE=0xC000")
[[ $VERBOSE -eq 1 ]] && echo " w3: ${#W3_SPECS[@]} module(s) resident @0xC000 (direct call)"
fi
# 3. bank infrastructure — required whenever crt0_banked is in play.
# Always assemble bank.s for _bank_pages + the bcall/bjump trampolines.
# If the user did not pass any --bank, generate a tiny stub providing
@@ -369,6 +436,7 @@ IHX="$WORK/$(basename "$OUT" .exe).ihx"
LINK_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size
--code-loc "$CODE_LOC" --data-loc "$DATA_LOC")
LINK_FLAGS+=("${BANK_LD_FLAGS[@]}")
LINK_FLAGS+=("${W3_LD_FLAGS[@]}")
for f in "${LD_EXTRA[@]}"; do LINK_FLAGS+=("$f"); done
# libsprinter.lib via -l/-L (sdcc passes -lsprinter through to sdldz80).
#
@@ -379,12 +447,12 @@ for f in "${LD_EXTRA[@]}"; do LINK_FLAGS+=("$f"); done
# the warning is just noise. In verbose mode show everything.
if [[ $VERBOSE -eq 1 ]]; then
run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \
"$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \
"$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" "${W3_RELS[@]}" \
"-L$LIB_DIR" "${GFX_LD[@]}" "$LIBC_LD"
else
# Drop the warning line + its two follow-up "Library:" lines.
run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \
"$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \
"$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" "${W3_RELS[@]}" \
"-L$LIB_DIR" "${GFX_LD[@]}" "$LIBC_LD" 2>&1 \
| awk '
/^\?ASlink-Warning-Definition of public symbol/ { skip = 3 }
@@ -393,9 +461,11 @@ else
'
fi
# Quick bank-size check (only meaningful if there are banks).
if [[ ${#BANK_SPECS[@]} -gt 0 ]] && [[ -f "${IHX%.ihx}.map" ]]; then
python3 "$CHECK_BANKS" "${IHX%.ihx}.map" || true
# Отчёт по раскладке памяти + проверка лимитов — для ЛЮБОЙ модели памяти
# (сколько свободно в W1/W2, W3 при --w3, и в каждом банке при --bank).
if [[ -f "${IHX%.ihx}.map" ]]; then
python3 "$CHECK_BANKS" "${IHX%.ihx}.map" \
--mode "$MEMORY_MODE" --heap-top "$HEAP_TOP_VAL" --stack "$STACK_ADDR" || true
fi
# 5. mkexe → .exe. In BIG mode tell mkexe banks live at 0x4000 (W1).
@@ -404,6 +474,7 @@ fi
MK_PREFIX=()
[[ $VERBOSE -eq 1 ]] && MK_PREFIX+=(-v)
[[ $BANK_W1 -eq 1 ]] && MK_PREFIX+=(-B 0x4000)
[[ $W3_RESIDENT_HUGE -eq 1 ]] && MK_PREFIX+=(-W)
MK_PREFIX+=("${MKEXE_EXTRA[@]}")
MK_PREFIX+=(-L "$LOAD_ADDR" -E "$ENTRY_ADDR" -S "$STACK_ADDR" -o "$OUT")
run "$MKEXE" "${MK_PREFIX[@]}" "$IHX"
+12
View File
@@ -276,6 +276,18 @@ Quick wins:
CBL-хука для ЭТОГО прерывания — при движении мыши часть тиков,
которые должны были обслужить CBL-насос, уходят мимо, кольцо
недоливается. Нужно исследование (MAME-дамп/лог трамплина).
- [ ] **kbd_raw: частые Rx-overrun в MAME при тапах** (roomtest,
2026-07-22) — замеры watchpoint-счётчиками: при стабильном
удержании клавиши overrun'ов ноль, но каждый быстрый тап (5 байт:
E0 74 + E0 F0 74 при FIFO 3) даёт overrun. Подозрение: dev-MAME
не эмулирует прерывание SIO на каждый принятый байт (байты
вычерпываются только кадровым IRQ 50 Гц) и/или подаёт пачку без
реальных ~1 мс/байт — на железе per-byte INT должен делать
overrun'ы редкостью. Проверить mame/sources/MAME/src/mame/
sinclair/sprinter.cpp (путь байта клавиатуры → INT) и при
желании поправить dev-MAME. Recovery-политика уже терпима к
частым overrun'ам (селективный wipe, docs/kbd-games.md), но
аккорд «держу →, тапнул ↑» остаётся уязвим.
## Known quirks (зафиксированы, обходы в libc)
+133
View File
@@ -0,0 +1,133 @@
# Клавиатура в играх на Sprinter (raw-канал, held-state)
Как правильно читать клавиатуру в play-цикле: что даёт платформа, чего
она НЕ даёт, и какие паттерны использовать. Выжато из порта Prince of
Persia (applications/PoP/roomtest) и отладки багов «залипание клавиш»
(2026-07-16) и «отвал Shift» (2026-07-22).
## Почему не ESTEX
ESTEX-функции (kbhit/getch/getkey, WAITKEY/SCANKEY) — **событийные**:
нажатие кладётся в буфер, отпускание не видно вообще. Live-состояние
есть только у модификаторов (CTRLKEY/kbd_mod_state) — и то с багом BIOS:
для стрелок/Home/End/PgUp/PgDn признак Shift не выставляется
(docs/converted/bugs.txt §4). Игре же нужно «клавиша X зажата ПРЯМО
СЕЙЧАС» для каждой клавиши. Отсюда raw-канал `<kbd_raw.h>`.
## Матчасть: как клавиатура устроена снизу
- AT/PS-2 клавиатура сидит на SIO-A Z84C015: порт 0x18 — данные
(деструктивное чтение!), 0x19 — статус (RR0, бит 0 = «байт принят»).
- Прерывание клавиатуры приходит с тем же IM2-вектором 0xFF, что и
кадровое; различаются битом 0 порта 0x19.
- Приёмный FIFO — **3 байта**. Обработчик ОБЯЗАН вычерпывать его в
цикле «пока бит 0 установлен» (docs/converted/IvanMak.txt §9.4), иначе
пачка байт переполнит FIFO и байты потеряются.
- Поток — PS/2 Scan Code Set 2: `код` = нажатие (make), `F0 код` =
отпускание (break), `E0` — префикс расширенных клавиш (стрелки и
т.п.), т.е. отпускание стрелки = `E0 F0 код`. Быстрый тап стрелки =
5 байт подряд.
- **Typematic (автоповтор): повторяется только ПОСЛЕДНЯЯ нажатая
клавиша** — make-код шлётся снова и снова без break между ними.
Модификаторы, зажатые вместе с другой клавишей, не шлют НИЧЕГО.
Это ключевой факт для дизайна recovery (см. ниже).
## API libc
```c
#include <kbd_raw.h>
kbd_raw_open(); // забрать клавиатуру у DSS (весь поток наш)
...
while (!kbd_raw_down(KBD_ESC)) { // выход проверяем САМИ — DSS слеп
kbd_raw_sync(); // раз в кадр, ДО чтения клавиш
if (kbd_raw_down(KBD_RIGHT)) ...
if (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)) ...
}
kbd_raw_close(); // вернуть клавиатуру DSS (есть и на atexit)
```
- Пока канал открыт, DSS клавиатуру **не видит**: kbhit/getch/getkey/
kbd_mod_state заморожены. Перед экраном с консольным вводом —
`kbd_raw_close()`.
- Декодер живёт прямо в IM2-трамплине (libc/irq/_irq_tramp.c): drain-
цикл FIFO + FSM префиксов F0/E0 + битмап `_kbdraw_down[512]`
(0..255 обычные, 256..511 расширенные, `KBD_EXT`).
- `kbd_raw_down(code)` — O(1) чтение битмапа, зовите сколько угодно.
- Требование памяти: BSS модуля должен быть в W2 (в `--memory small`
у крошечных программ может уехать в W1 → kbd_raw_open вернёт EINVAL;
huge/big — всегда ок).
## Rx-overrun и политика восстановления
Если прерывания запрещены дольше ~3 байт-тактов (длинные DI-окна,
тяжёлый кадр), FIFO переполняется, SIO теряет байты и взводит Rx Overrun
(RR1 бит 5). Потерянный break = залипшая клавиша. Трамплин ловит это и
взводит флаг; `kbd_raw_sync()` раз в кадр делает восстановление:
- сбрасываются ВСЕ обычные клавиши — реально зажатые перечитаются
ближайшим typematic-повтором (~0.1 с), а залипшие исчезнут;
- **модификаторы (Shift/Ctrl/Alt) НЕ сбрасываются** — их перечитать
нечем (typematic по ним не идёт), сброс превращался в «отвал» Shift
при каждом overrun (баг PoP: Shift+→ давал 1-4 осторожных шага, после
чего Кид начинал бежать — Shift пропадал из битмапа навсегда).
Цена компромисса (осознанная):
- залипший модификатор (если overrun потерял именно его break) живёт до
следующего нажатия этого модификатора — редкий случай;
- дырка с аккордами: держим →, тапаем ↑ (run-jump) — теперь typematic
идёт по ↑, и если overrun случится ДО отпускания →, стрелка сбросится
и не перечитается (повтор к предыдущей клавише не возвращается).
По замерам в MAME overrun'ы при стабильном удержании не возникают
вовсе (они кластеризуются в момент пачек make/break при тапах),
поэтому на практике окно узкое.
## Паттерны игрового цикла
- **held-state против «свежего нажатия»**: битмап отвечает только на
«зажата ли». Для действий «одно нажатие = одно срабатывание» нужен
edge-detect: `if (sp && !sp_prev) toggle(); sp_prev = sp;`
(roomtest.c, тумблер дабл-буфера по SPACE).
- **Подавление автоповтора действий** — конечный автомат
RELEASED/HELD/IGNORE в стиле SDLPoP (pop_ctrl.c,
read_user_control): действие срабатывает на переходе RELEASED→HELD,
затем переводится в IGNORE и не повторяется, пока клавишу физически
не отпустят. Битмап при этом остаётся level-triggered.
- **Оси**: собирать `control_x/control_y` из пар клавиш каждый кадр из
битмапа заново, не копить дельты.
- `kbd_raw_sync()` звать строго один раз в кадр и строго ДО опроса
клавиш этого кадра.
## Тестирование в MAME (bridge)
- Держать клавиши через `set_input` на `:kbd:ms_naturl:*` **защёлкой**:
`value=1` без `frames`, потом явный `value=0`. Вариант с
`frames=N` для удержаний ненадёжен (холд может не породить ни одного
скан-кода). Комбинации вида Shift+стрелка так подаются нормально
(LShift = `:kbd:ms_naturl:P1.7` mask 0x0002, Cursor Right =
`:kbd:ms_naturl:P2.4` mask 0x0040) — проверено 2026-07-22.
- Наблюдать состояние — по символам map-файла: `_kbdraw_down` (+0x12 =
LShift, +0x174 = Right и т.д.), `_kbdraw_overrun`; счётчики событий —
watchpoint с действием `{tempN=tempN+1; g}` (не останавливает
эмуляцию).
- **Не тестировать вдвоём одновременно** (человек за клавиатурой +
бридж-эмуляция): незакрытая защёлка `set_input` выглядит как
«залипшая» клавиша и съедает часы отладки.
- Overrun'ы в MAME заметно чаще, чем ожидается на железе (пачка тапа
прилетает плотнее реальных ~1 мс/байт); открытый вопрос — эмулирует
ли MAME прерывание на каждый принятый байт SIO (см. TODO).
## История багов (чтобы не повторять)
1. **Залипание клавиш** (2026-07-16): трамплин читал 1 байт за
прерывание → FIFO(3) переполнялся пачкой break-кодов → break терялся
→ клавиша зажата навсегда. Фикс: drain-цикл в ISR (как эталоны
docs/samples/sprinterKeybLib.asm, SIO_CTC_KEY.asm).
2. **Отвал Shift** (2026-07-22): recovery по overrun сбрасывал ВЕСЬ
битмап; модификаторы не перечитываются typematic'ом → Shift
«отпускался» до перенажатия. Фикс: селективный сброс (модификаторы
сохраняются), см. libc/kbd/kbd_raw_sync.c.
Связанные документы: docs/mame-autotest.md, docs/im2_isr_design.md,
docs/converted/IvanMak.txt §9.4, applications/PoP/docs/PORT_PLAN.md §2.
+10
View File
@@ -259,6 +259,14 @@ putsprite новой (save-буфер не нужен). Низкий урове
рисовать банком 0x50, спрайты/оверлеи — putsprite/0x5C; буферы
образов — вне W3 (< 0xC000). Тесты: tests/sprites, tests/gfxbanks,
tests/bgi_img.
Скролл региона из НЕактивной страницы в активную (<sprite.h>):
`gfx_scroll_h(area, dx, dirty)` — горизонтальный (dx>0 = вправо; быстрый
построчный accel-скролл без страйдов, DI-банды по 16 строк),
`gfx_scroll_v(area, dy, dirty)` — вертикальный (dy>0 = вниз; через
строку-буфер на стеке, ПОКА не оптимален — Port_Y один на burst, разный
read/write Y невозможен без ломающего accel OUT). Копия = скролл + heal
цели (банк 0x50). Открывшуюся полосу |d| не заполняют — возвращают в
*dirty (NULL = не нужно). Пример: examples/scroll.
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
drawpoly.
@@ -304,8 +312,10 @@ Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс
| `int kbd_raw_open(void)` | включить: с этого момента ВЕСЬ поток клавиатурных байт достаётся нам, DSS его не видит. 0 / -1+errno (EBUSY повтор) |
| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit |
| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 |
| `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) |
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
| `KBD_LCTRL/LALT/RCTRL/RALT` | коды модификаторов (R* — расширенные, с KBD_EXT); добавлены 2026-07-22 |
**ИСПРАВЛЕНО 2026-07-15 (был неверный вывод, ниже — то, что реально
подтвердилось):** изначально скриншот-тестирование (снимок экрана
-67
View File
@@ -1,67 +0,0 @@
1) если я и DATA и CODE размещаю в одном окне (W1 - #4000 или W2 - #8000, неважно),
то при вызове set_videomode глобальные переменные (errno, g_text_attr) не меняют
своих значений.
если же DATA и CODE находятся в разных окнах (не важно где DATA - в W1 или W2, главное
что не в том где CODE) - то при вызове se_videomode значения глобальных переменных меняются
То есть похоже что для DATA не назначается отдельный блок памяти а назначается только для CODE
Это полностью соответствует документации - если приложение менее 16К (как у нас) то ему выделяется
только одна страница. И получается что работа со второй страницей идет несанкционированно (ей память
не выделена).
Потому предлагается
1) сейчас размещать ВСЕ в одной странице (и DATA и CODE и стек) - в W2.
2) дальше - добавить в нашу обертку sprinter-cc режимы памяти -
-tiny - все приложение помещается в одну страницу - в W2 (и DATA и CODE и стек)
-small - приложение помещается в две страницы - CODE в W1, DATA и стек - в W2.
в этом режиме над отдельно выделять и маппить страницу в W2 для DATA и стека
-big - DATA, CODE и стек помещаются в одну страницу W2 как в -tiny, добавляется поддержка banked в W1,
страница W3 остается служебной и для работы с граффикой из banked code
-huge - приложение помещается в двух страницах как и -small но так же добавляется поддержка banked но
уже в страницу W3
Из документации -
> Теперь адресса #4000..#7FFF,#8000..#BFFF,#C000..#FFFF, когда ДСС
передаёт управление эти прогораммам, какие банки там нахадятся по
умолчанию?
В зависимости от адреса загрузки и размера приложения DSS выделяет
необходимое число страниц памяти. Так при размере меньше 16К будет
выделена
одна страница, при размере больше 16К - две, и т.д. В окна с
"неиспользуемым" адресном пространством будет подключатся
специальная страница #FF.Если приложению требуется памяти больше чем
зарезервировано в exe-файле, оно должно выделить себе дополнительный
блок памяти самостоятельно.
> В конфигурации спринтер
> по #0000..#3FFF, при работе ДСС находится сама ДСС с её Резетами,
> чтоб использывать когда сюда подставленна страница пользователся
> резеты не доступны!.
Это так в нижних 16K находится DSS / BIOS в остальных 48К
приложение, но с
определенными особенностями. Стек не должен быть выше #BFFF при
вызове DSS и ниже #8000 при вызове некоторых функций BIOS.
Так же посмотри вот сюда - возможно нам придется для режимов -small и -huge делать свой первичным загрузчиком -
Из документации -
Выполнение EXE-файла осуществляется по следующим пунктам:
1) Открывает exe-файл на чтение;
2) Считывает в рабочую область префикс exe-файла;
3) Выделяет блок памяти, требуемый для загрузки всего файла или первичного
загрузчика, если его размер не равен нулю;
4) Сохраняет стек;
5) Подключает страницы из выделенного блока;
6) Строит префикс запуска программы и устанавливает на него регистр IX;
7) Считывает файл по адресу указанному в смещении 16 (Адрес расположения кода в
памяти);
8) Закрывает exe-файл, если это не первичный загрузчик;
9) Устанавливает стек равным значению из смещения 20 (Адрес расположения стека);
10) Передает управление по адресу указанному в смещении 18 (Адрес запуска);
+308
View File
@@ -0,0 +1,308 @@
# Управление памятью в sprinter-cc
Документ описывает модель памяти Sprinter (Sp2000), режимы памяти обёртки
`sprinter-cc` и способы размещения кода/данных: single-page, split, банки
(трамплины) и резидентный код окна W3 (`--w3`).
Всё в этом документе подтверждено сборкой и прогоном в MAME v3.06 / DSS 1.71.57
(см. `tests/w3probe`, `tests/banktest`, `tests/banklocl`).
---
## 1. Аппаратная модель памяти
Z80 видит 64 КБ, разбитые на **четыре окна по 16 КБ**. Каждое окно независимо
маппится на физическую страницу через порт-регистр страницы:
| Окно | Адреса | Порт страницы | Назначение по умолчанию |
|------|---------------|---------------|--------------------------|
| W0 | `0x0000-0x3FFF` | `0x82` | **DSS / BIOS** (RST-ы, системные вызовы) |
| W1 | `0x4000-0x7FFF` | `0xA2` | приложение |
| W2 | `0x8000-0xBFFF` | `0xC2` | приложение |
| W3 | `0xC000-0xFFFF` | `0xE2` | приложение / графика / банки |
- Запись в порт `0x{8/A/C/E}2` меняет физ-страницу окна; чтение возвращает
текущую страницу.
- **W0 занят DSS/BIOS** — там живут RST-обработчики (`RST #08` BIOS, `RST #10`
ESTEX). Пока в W0 стоит системная страница, вызовы доступны; подменять W0
нельзя без потери RST-ов.
- «Неиспользуемое» окно маппится на **специальную страницу `#FF`**: чтение даёт
`0xFF`, запись игнорируется. Это ключевая причина «молчаливой» порчи данных —
см. §7.
### Порты в C
`<sprinter.h>` даёт SFR-обёртки: `_io_page_w0..w3` (чтение/запись порта),
`sprinter_page_w0..w3(page)`.
---
## 2. Как DSS загружает EXE
Из документации DSS, последовательность `EXEC`:
1. Открыть exe-файл на чтение.
2. Считать префикс exe в рабочую область.
3. **Выделить блок памяти** размером под весь файл (если `loader==0`) или под
первичный загрузчик (`loader>0`).
4. Сохранить стек.
5. **Подключить страницы** выделенного блока в окна (последовательно от окна
адреса загрузки: W1→W2→W3…).
6. Построить префикс запуска → регистр `IX`.
7. Считать файл по адресу загрузки (смещение 16 в заголовке).
8. Закрыть exe, **если это не первичный загрузчик** (`loader==0`).
9. Установить `SP` = значение из смещения 20 (адрес стека).
10. Передать управление по адресу из смещения 18 (entry).
**Следствия, на которых стоит вся схема памяти:**
- DSS выделяет **число страниц по размеру образа**: `<16 КБ` → 1 страница,
`16..32 КБ` → 2, `32..48 КБ` → 3. Лишние окна = страница `#FF`.
- Страницы маппятся **подряд** начиная с окна адреса загрузки. Образ, тянущийся
`0x4100..0xFFFF` (3 страницы), даёт W1+W2+W3 замапленными автоматически —
**это и есть база для резидентного кода W3** (§6, подход A).
- `loader>0` (multi-bank .exe) → DSS грузит только HOME-часть и **оставляет
файл открытым** (handle в `IX-3`), а crt0 дочитывает банки сам (§5).
Упаковкой в этот формат занимается `toolchain/mkexe`.
---
## 3. Правила стека (критично)
- **`SP ≤ 0xBFFF`** при вызовах DSS (ESTEX, `RST #10`).
- **`SP ≥ 0x8000`** при вызовах некоторых функций BIOS (`RST #08`).
- Пересечение этих требований → **стек обязан жить в W2** (`0x8000-0xBFFF`).
По умолчанию `SP` инициализируется в `0xBFFE`.
Проверено (`tests/w3probe`): во всех режимах на входе `main` `SP ≈ 0xBFFC`, т.е.
в W2.
**Chicken-and-egg для split-режимов** (small/huge): DSS ставит `SP=0xBFFE` из
заголовка, но для программ `<16 КБ` окно W2 ещё не выделено (там `#FF`). Пуши
туда теряются, первый `call` возвращается в мусор. Поэтому `crt0_small`/
`crt0_banked` сначала работают на **загрузочном стеке в W1** (реальное ОЗУ),
маппят W2 и только потом переставляют `SP=0xBFFE`. Маппинг W2 делается через
**ESTEX `$3A SETWIN2`** (не BIOS `$C4`+OUT — тому нужен стек уже в W2).
---
## 4. Режимы памяти
Выбираются флагом `sprinter-cc --memory MODE` (по умолчанию `tiny`). Режим задаёт
адрес кода, размещение данных, crt0 и наличие банков.
| Режим | CODE | DATA/BSS | Стек | Банки | crt0 | Первичный загрузчик |
|--------|------|----------|------|-------|------|----------------------|
| `tiny` | W2 `0x8100` | за кодом (W2) | W2 | — | `crt0.s` | нет (1 страница) |
| `small` | W1 `0x4100` | за кодом (W1→W2) | W2 | — | `crt0_small.s` | да (сам маппит W2) |
| `big` | W2 `0x8100` | за кодом (W2) | W2 | W1 (трамплины) | `crt0_banked.s` (BANK_W1) | да |
| `huge` | W1 `0x4100` | за кодом (W1→W2) | W2 | W3 (трамплины) | `crt0_banked.s` | да |
| `manual` | явно | явно | W2 | — | `crt0.s` | зависит |
Во всех режимах **DATA цепляется линкером сразу за кодом** (`--data-loc 0`), а не
кладётся по фиксированному адресу. Раньше `huge` использовал фиксированный
`DATA=0x8000`, что ломалось при коде `>16 КБ` (код перетекал в W2 и накрывал
DATA); сейчас DATA динамически идёт за концом кода.
### 4.1 `tiny` — всё в одной странице
```
0x8000..0x80FF зарезервировано (startup-prefix)
0x8100 _start / _CODE … _DATA … _BSS … _HEAP
0xBB00 heap top (по умолчанию)
0xBFFE стек ↓
```
- Один блок 16 КБ, DSS маппит его в W2. W1 и W3 = `#FF`.
- Ничего выделять/маппить не надо; `crt0.s` предполагает, что W2 уже дан DSS.
- Практический потолок кода+данных+кучи+стека ≈ 14 КБ.
- `--code-loc 0x8100 --data-loc 0`; mkexe `-L 0x8100 -E 0x8100 -S 0xBFFE`.
### 4.2 `small` — CODE в W1, данные перетекают в W2
```
0x4100 _CODE … (W1)
… за кодом → _DATA _BSS _HEAP (W1, перетекает в W2)
0xBB00 heap top
0xBFFE стек ↓ (W2)
```
- Покрывает ~0..30 КБ (код+данные вместе).
- `crt0_small.s` авто-определяет W2: читает порт `0xC2`. Если `≠0xFF` — DSS уже
дал W2 (образ `>16 КБ`), маппить не надо. Если `=0xFF` — сам выделяет страницу
(`ESTEX $3D GETMEM`) и маппит (`ESTEX $3A SETWIN2`).
- `--code-loc 0x4100 --data-loc 0`.
### 4.3 `big` — tiny + банки в W1
- База как `tiny` (CODE+DATA+стек в W2, `0x8100`).
- **Свапаемые банки в W1** (`0x4000-0x7FFF`, порт `0xA2`), вызываются через
трамплины (§5). `crt0_banked.s` собирается с `BANK_W1=1`.
- mkexe получает `-B 0x4000` (банки живут в W1). Виртуальный адрес банка N =
`0x{N}4000`.
- W3 свободно — доступно под графику или резидентный код (`--w3`).
### 4.4 `huge` — small + банки в W3
- База как `small` (CODE `0x4100`, DATA за кодом, авто-детект W2).
- **Свапаемые банки в W3** (`0xC000-0xFFFF`, порт `0xE2`), через трамплины.
Виртуальный адрес банка N = `0x{N}C000`.
- `crt0_banked.s` (без `BANK_W1`) грузит банки из .exe после старта.
### 4.5 `manual` — явное размещение
`--memory manual --memory-manual SPEC`, где SPEC = список `KEY=VAL`:
`CODE=W1|W2`, `DATA=W1|W2|SAME`, `BANKED=W1|W3`. Плюс прямые `--code-loc` /
`--data-loc` / `-L` / `-E` / `-S` перекрывают дефолты любого режима.
---
## 5. Банки и трамплины (`--bank`)
Для `big`/`huge`. Модуль-банк собирается в отдельную область и линкуется по
**виртуальному 24-битному адресу** (`bank_id` в старшем байте):
```
sprinter-cc --memory huge --bank 1=engine.c --bank 2=audio.c -o app.exe main.c
```
- Банк N компилируется `--codeseg/--constseg/--dataseg BANKN`, линкуется
`-Wl-b_BANKN=0x{N}C000` (huge) или `0x{N}4000` (big).
- `main.c` обязан объявить `const uint8_t n_banks = N;``crt0_banked` читает
это **до gsinit**, поэтому только `const` (инициализатор ещё не скопирован).
- Функции банка помечаются `__banked` — SDCC генерирует вызов через трамплин
`___sdcc_bcall_ehl``_CODE`/W1, всегда замаплен): он сохраняет текущую
страницу окна, маппит нужный банк (`_bank_pages[id]`), `jp` в функцию, по
возврату восстанавливает страницу.
- Загрузка: mkexe пакует `header + HOME + bank1(16К) + bank2(16К)…`,
`loader=размер HOME`; `crt0_banked` выделяет страницы (`GETMEM`), маппит и
дочитывает каждый банк `ESTEX READ` из открытого файла.
- Проверка размеров банков — `toolchain/check_banks.py` (часть отчёта
раскладки, см. §10).
Writable bank-local данные возможны, но с оговорками — см.
`memory/bank_local_data_pattern`.
---
## 6. Резидентный код окна W3 (`--w3`)
**Альтернатива банкам без трамплинов.** Модуль размещается резидентно в W3
(`0xC000`) и вызывается **прямым `call`** — как обычная функция. Работает во
**всех режимах** (`tiny|small|big|huge`); по умолчанию подразумевает `small`.
```
sprinter-cc --w3 render.c -o app.exe main.c # → small
sprinter-cc --memory huge --w3 render.c --bank 1=lvl.c -o app.exe main.c
```
**Как работает (подход A):** образ с областью `W3CODE`@`0xC000` тянется до `0xC0xx`
(≥3 страницы), и DSS сам маппит W3 при загрузке (§2) — **загрузчик в crt0 не
нужен** (кроме huge). Прямые вызовы резолвятся линкером в реальные `0xC0xx`.
**Механизм сборки:**
- W3-модуль компилируется `--codeseg W3CODE --constseg W3CODE` — код и rodata в
W3. **`--dataseg` НЕ переопределяется**: писучие статики уходят в обычный
`_DATA` (W2). Отсюда правило «в W3 только код + rodata».
- Линк `-Wl-b_W3CODE=0xC000`.
**Раскладка страниц по режимам (проверено MAME):**
| Режим | W1 | W2 | W3 |
|-------|----|----|----|
| tiny | `#FF` (не исп.) | код+данные | **резидент** |
| small | код | данные | **резидент** |
| big | банк (трамплин) | код+данные | **резидент** |
| huge | код | данные | **резидент делит окно с трамплин-банками** |
**huge — особый случай (резидент + банки в одном окне W3):**
- `crt0_banked` под `.ifdef W3_RESIDENT` захватывает физ-страницу резидента
(`in a,(0xE2)`) сразу после загрузки DSS и **возвращает её дефолтом** после
цикла загрузки банков (иначе в W3 остался бы последний банк).
- Трамплин на каждый `__banked`-вызов сам сохраняет/восстанавливает страницу
W3 — поэтому дефолтная страница обязана быть резидентной.
- mkexe получает флаг `-W` (разрешить HOME тянуться в W3 при наличии W3-банков —
намеренное совмещение).
**Правила разработчика:**
- Код в W3 **не переключает** страницу W3.
- Код в W1/W2, свапающий W3 на другую страницу (скретч, графика), обязан
**вернуть исходную** (`in a,(0xE2)` → работа → `out (0xE2),a`); оборачивать в
`DI/EI`, если есть ISR, дёргающий W3.
- В W3 — **только код + rodata**, писучих переменных там быть не должно.
- Из `__banked`-контекста (пока в W3 замаплен банк) резидентный W3-код
**недостижим** транзитивно. Обратное — резидент → `__banked` через трамплин
W1 — **работает** (трамплин вернёт резидентную страницу перед `ret`).
Подробности и артефакты — `memory/w3_resident_code`, тест `tests/w3probe`.
---
## 7. Куча и стек
- **Стек**: init `SP=0xBFFE`, растёт вниз. Меняется через `-S 0xADDR`.
- **Куча**: от конца `_BSS` вверх до `___sdcc_heap_end` (по умолчанию `0xBB00`
~1278 байт под стек). `malloc` берёт `&___sdcc_heap_end` как потолок.
- `runtime/heap.s` — динамическая куча (авто-размер = зазор BSS…heap_top), НЕ
фиксированный `.ds`.
- `--stack-size N` регенерирует `heap_top` как `HEAP_TOP = 0xBFFF - N`, зажимая
рост кучи ради стека.
---
## 8. Семейство crt0
Выбирается режимом; `--crt0=TYPE` перекрывает.
| crt0 | Файл | Для чего |
|------|------|----------|
| `default` | `runtime/crt0.s` | tiny/manual; парсит argv, argv[0] через APPINFO |
| `minimal` | `runtime/crt0_minimal.s` | tiny без argv (меньше размер) |
| `small` | `runtime/crt0_small.s` | small; авто-детект/выделение W2 |
| `banked` | `runtime/crt0_banked.s` | big/huge; авто-детект W2 + загрузка банков |
`crt0`/`bank.s` собираются **пер-сборка** внутри `sprinter-cc` (с префиксами
`BANK_W1` / `W3_RESIDENT` / `DEBUG_RT`), а не бандлятся в библиотеку.
---
## 9. Типовые грабли
- **`static`-переменные читаются как `0xFF` / не меняются** — DATA попала в
невыделенное окно (`#FF`). Причина: код и данные в разных окнах при образе
`<16 КБ`, где DSS дал только одну страницу. Лечится правильным режимом
(`small`/`huge`) или единым окном (`tiny`). См.
`memory/sprinter_memory_modes`.
- **Крэш после первого `call` в split-режиме** — стек ещё в невыделенном W2.
Решает загрузочный стек в W1 (уже в crt0).
- **Банк «прыгает в мусор»** — таблица `_bank_pages[]` в `_DATA` занулилась
gsinit’ом; она обязана жить в `_CODE`. Уже исправлено в `bank.s`.
- **`--w3`: резидент недоступен из банка** — ожидаемо (см. §6); держи вход в
резидент только из W1/W2 или из самого W3-кода.
---
## 10. Отчёт по раскладке памяти (после линковки)
`sprinter-cc` печатает отчёт `toolchain/check_banks.py` для **любой** модели
памяти (по `.map`). Показывает, где легли код/данные и **сколько свободно**
в каждом окне и банке:
```
memory: huge — CODE в W1 (0x4100), DATA→W2, банки в W3
_CODE @ 0x4100 size 3767 (W1) → 0x4FB7
данные @ 0x4FB7 size 336 (W1) → 0x5107 [_DATA/_BSS/_INITIALIZED/…]
статика до 0x5107 — куча 0x5107..0xBB00 (27129 Б), стек 0xBB00..0xBFFE (1279 Б)
_W3CODE @ 0xC000 size 249 (резидент W3) → 0xC0F9, 16135 Б свободно до 0x10000 OK
_BANK1 @ 0x0001C000 size 219 / 16384 ( 1.3%) → 16165 Б свободно OK
```
- `_CODE` / `данные` — окно (W1/W2), размер, конец; строка `статика … куча …
стек …` = свободное место в W1/W2 (под кучу malloc до `___sdcc_heap_end` и
под стек).
- `_W3CODE` — только при `--w3`: остаток окна W3.
- `_BANKn` — только при `--bank`: занятость/остаток каждого 16 КБ-банка.
- Ненулевой выход (проглатывается `|| true`), если банк > 16 КБ или статика
заехала за `heap_top`/`0xC000`.
```
Binary file not shown.
+721
View File
@@ -0,0 +1,721 @@
;-------------------------------------------------------------------------------
; Balls mini-demo
; (c) Sayman 2015
;-------------------------------------------------------------------------------
; ïðîöåäóðà ìîäóëÿ ñîâñåì íå îïòèìàëüíà, âçÿòà èç èñõîäíèêà áèáëèîòåêè C.
;-------------------------------------------------------------------------------
tbuf_addr: equ 4000h
screen_addr: equ 0xc000
balls_pgn equ 1
bgspr_pgn equ 6
savescr_pgn equ 6
tbuf_pgn equ 1
MAX_BALLS equ 32 ; ïðè 96 øàðèêàõ ñòðóêòóðà balls_cfg áóäåò 576 áàéò
balls_pal_size equ 128
bgspr_pal_size equ 128
balls_struct:
.x equ 0 ; u16_t
.y equ 2 ; u8_t
.dx equ 3 ; i8_t
.dy equ 4 ; i8_t
.radius equ 5 ; u8_t
balls_struct_bkp:
.x equ 0 ; u16_t
.y equ 2 ; u8_t
include "dss.inc"
include "head.inc"
entry: push ix
call init
call read_bgspr
ld a,(pg_tbl.tbuf)
out (cpu_w1),a
ld a,(tmp_hndl)
call close
call read_bgpal
call read_balls_spr
call read_balls_pal
call vinit
call balls_reindex
call balls_init
call draw_bgspr
call CopySCR_1to2
ld a,0xc0
out (port_y),a
ld a,(pg_w3)
out (cpu_w3),a
call balls_main
sub a
jp quit0
include "misc.asm"
include "math.inc"
rand16 ld de,.seed ; Seed is usually 0
ld a,d
ld h,e
ld l,253
or a
sbc hl,de
sbc a,0
sbc hl,de
ld d,0
sbc a,d
ld e,a
sbc hl,de
jr nc,.rand
inc hl
.rand ld (rand16+1),hl
ret
.seed: dw 0
inc_pg1: ld a,(ix)
out (cpu_w1),a
inc ix
ret
inc_pg3: ld a,(ix)
out (cpu_w3),a
inc ix
ret
init: ld c,balls_pgn
ld hl,pg_tbl.balls
.loop0: push bc
push hl
call gmem
pop hl
pop bc
jp c,gmem_err
ld (hl),a
inc hl
dec c
jr nz,.loop0
ld c,bgspr_pgn
.loop1: push bc
push hl
call gmem
pop hl
pop bc
jp c,gmem_err
ld (hl),a
inc hl
dec c
jr nz,.loop1
ld c,savescr_pgn
.loop2: push bc
push hl
call gmem
pop hl
pop bc
jp c,gmem_err
ld (hl),a
inc hl
dec c
jr nz,.loop2
ld c,tbuf_pgn
.loop3: push bc
push hl
call gmem
pop hl
pop bc
jp c,gmem_err
ld (hl),a
inc hl
dec c
jr nz,.loop3
ex af,af
in a,(cpu_w1)
ld (pg_w1),a
in a,(cpu_w3)
ld (pg_w3),a
in a,(port_y)
ld (old_y),a
ex af,af
out (cpu_w1),a
ret
set_scr_w3: out (cpu_w3),a
ret
set_scr_w1: out (cpu_w1),a
ret
vinit: ld a,81h
ld bc,50h
rst 10h
call cls
call set_global_pal
call CopySCR_1to2
ld a,norm_scr
call set_scr_w3
ret
CopySCR_1to2: sub a
out (port_y), a
ld hl, 0xc000
ld de, 0xc140
ld bc, 320
ld d, d
ld a, 0 ; 256 bytes
ld a, a
; ldir
.loop: REPT 64
ldi
ENDM
jp pe,.loop
ld b, b
ld hl, 0xc3E0
ld de, 0xc3E4
; ld bc, 0004h
ld a, a
ldi
ldi
ldi
ldi
; ldir
ld b, b
ret
cls: ld hl,0xc3E0
ld de,0
ld d,d
ld a,0
ld b,b
ld b,4
.loop1: ld a,d
out (port_y),a
ld e,e
ld (hl),e
ld b,b
inc hl
djnz .loop1
ld hl,0xc000
ld de,0
ld bc,320
ld d,d
ld a,0
ld b,b
.loop2: ld a,d
out (port_y),a
ld e,e
ld (hl),e
ld b,b
inc hl
dec bc
ld a,b
or c
jr nz,.loop2
ret
set_global_pal: ld bc,0ffa4h
ld hl,pal
ld de,0
sub a
rst 8
ret
read_bgspr: ld hl,data_files.bgspr
push hl
call open
pop hl
ld (open_err.err_file+1),hl
jp c,open_err
ld (rd_err.err_file+1),hl
ld (tmp_hndl),a
ld ix,pg_tbl.bgspr
.loop: call inc_pg1
ld a,(tmp_hndl)
ld hl,tbuf_addr
ld de,16320
push ix
call read
pop ix
jp c,rd_err
or a
ret nz
jr .loop
read_bgpal: ld hl,data_files.bgpal
push hl
call open
pop hl
ld (open_err.err_file+1),hl
jp c,open_err
ld (rd_err.err_file+1),hl
ld (tmp_hndl),a
ld hl,pal
ld de,bgspr_pal_size*4
call read
jp c,rd_err
ld a,(tmp_hndl)
call close
ret
read_balls_spr: ld hl,data_files.balls
push hl
call open
pop hl
ld (open_err.err_file+1),hl
jp c,open_err
ld (rd_err.err_file+1),hl
ld (tmp_hndl),a
ld hl,balls_spr
ld de,1024
call read
jp c,rd_err
ld a,(tmp_hndl)
call close
ret
read_balls_pal: ld hl,data_files.balls_pal
push hl
call open
pop hl
ld (open_err.err_file+1),hl
jp c,open_err
ld (rd_err.err_file+1),hl
ld (tmp_hndl),a
ld hl,pal+bgspr_pal_size*4
ld de,balls_pal_size*4
call read
jp c,rd_err
ld a,(tmp_hndl)
call close
ret
balls_reindex: ld hl,balls_spr
ld c,128
ld de,1024
.loop0: ld a,(hl)
inc a
jr z,.loop1
dec a
add a,c
ld (hl),a
.loop1: inc hl
dec de
ld a,e
or d
jr nz,.loop0
ret
balls_init: ld hl,balls_cfg
ld de,balls_cfg+1
ld bc,MAX_BALLS*6
xor a
ld (hl),a
ldir
ld hl,balls_bkp
ld de,balls_bkp+1
ld bc,MAX_BALLS*3
xor a
ld (hl),a
ldir
ld iy,MAX_BALLS
ld ix,balls_cfg
.loop0: ld de,320-16 ; ðàçìåðû êàæäîãî øàðèêà 16íà16
push de
call rand16 ; áåð¸ì ñëó÷àéíóþ êîîðäèíàòó X
pop de
inc hl
push de
call lmod
pop de
ex de,hl
.next0: ld (ix+balls_struct.x),e
ld (ix+balls_struct.x+1),d
ld de,256-16
push de
call rand16
pop de
inc hl
push de
call lmod
pop de
ex de,hl
.next1: ld (ix+balls_struct.y),e
call rand16
ld a,l
and 1
jr nz,.next2
ld a,-1
.next2: ld (ix+balls_struct.dx),a
call rand16
ld a,l
and 1
jr nz,.next3
ld a,-1
.next3: ld (ix+balls_struct.dy),a
ld de,6
add ix,de
dec iy
ld a,iyl
or iyh
jr nz,.loop0
ret
draw_bgspr: ld ix,pg_tbl.bgspr
call inc_pg1
ld hl,tbuf_addr
ld c,51
ld a,0
.loop: ld de,screen_addr
out (port_y),a
ex af,af
di
ld d,d
ld a,0
ld l,l
ld a,(hl)
ld (de),a
ld b,b
inc h
inc d
ld d,d
ld a,64
ld l,l
ld a,(hl)
ld (de),a
ld b,b
ld a,64
add a,l
ld l,a
adc a,h
sub l
ld h,a
dec c
call z,nextpage
ex af,af
inc a
jr nz,.loop
ei
halt
di
ret
nextpage: call inc_pg1
ld hl,tbuf_addr
ld c,51
ret
balls_main:
.loop1: ld iy,MAX_BALLS
ld ix,balls_cfg
call save_coords
push ix
push iy
.loop0: ld e,(ix+balls_struct.x)
ld d,(ix+balls_struct.x+1)
ld hl,320-16
call check_x
ld c,(ix+balls_struct.y)
ld hl,256-16
call check_y
ld a,(ix+balls_struct.dx)
ld h,0
ld l,a
rlca
jr nc,.a1
dec h
.a1: rrca
add hl,de
ex de,hl
ld (ix+balls_struct.x),e
ld (ix+balls_struct.x+1),d
ld a,(ix+balls_struct.dy)
add a,c
ld (ix+balls_struct.y),a
ld de,6
add ix,de
dec iy
ld a,iyl
or iyh
jr nz,.loop0
pop iy
pop ix
ld a,norm_scr|trans_scr|tmp_scr
call set_scr_w3
.loop2: ld hl,balls_spr
ld a,iyl
and 3
ld e,(ix+balls_struct.x)
ld d,(ix+balls_struct.x+1)
ld c,(ix+balls_struct.y)
call draw_balls
ld de,6
add ix,de
dec iy
ld a,iyl
or iyh
jr nz,.loop2
ei
halt
di
in a,(rgmod)
and 1
xor 1
out (rgmod),a
ld a,norm_scr
call set_scr_w3
ld iy,MAX_BALLS
ld ix,balls_bkp
.loop3: ld e,(ix+balls_struct.x)
ld d,(ix+balls_struct.x+1)
ld c,(ix+balls_struct.y)
call restore_bg
ld de,3
add ix,de
dec iy
ld a,iyl
or iyh
jr nz,.loop3
.blah: call check_key
jr c,._ret_
jp .loop1
._ret_: ret
save_coords: push ix
push iy
ld hl,balls_bkp
.loop: ld e,(ix+balls_struct.x)
ld d,(ix+balls_struct.x+1)
ld c,(ix+balls_struct.y)
ld (hl),e
inc hl
ld (hl),d
inc hl
ld (hl),c
inc hl
ld de,6
add ix,de
dec iy
ld a,iyh
or iyl
jr nz,.loop
pop iy
pop ix
ret
restore_bg:
in a,(rgmod)
or a
ld a,0xc0
jr nz,.restore_to_0
ld a,0xc1
jr .restore_to_1
.restore_to_0: add a,d
ld d,a
ld h,d
ld l,e
jr .restore
.restore_to_1: add a,d
ld d,a
ld a,0x40
add a,e
ld e,a
adc a,d
sub e
ld d,a
ld h,d
ld l,e
.restore: ld a,c
out (port_y),a
ld b,16
di
.loop: ld d,d
ld a,16
ld l,l
ld a,(hl)
ld (de),a
ld b,b
in a,(port_y)
inc a
out (port_y),a
djnz .loop
ret
draw_balls: ld b,a
in a,(rgmod)
or a
ld a,0xc1
jr z,.draw_to_1 ; åñëè âêëþ÷¸í 0é ýêðàí, ðèñóåì íà 1é
ld a,0xc0 ; èíà÷å (1é) ðèñóåì â 0é
.draw_to_0: add a,d
ld d,a
jr .draw
.draw_to_1: add a,d
ld d,a
ld a,0x40
add a,e
ld e,a
adc a,d
sub e
ld d,a
.draw: ld a,b
add a,a
add a,a
add a,a
add a,a
add a,l
ld l,a
adc a,h
sub l
ld h,a ; íîìåð ñïðàéòà*256 = àäðåñ ñïðàéòà â êàðòèíêå
ld a,c
out (port_y),a
ld b,16
di
.loop:
ld d,d
ld a,16
ld l,l
ld a,(hl)
ld a,a
ld (de),a
ld b,b
ld a,c
out (port_y),a
ld a,l
add a,64
ld l,a
ld a,0
adc a,h
ld h,a
inc de
djnz .loop
ret
check_x: ld a,e
or d
jr z,.next0
sbc hl,de
jr z,.next1
jr c,.next1
ret
.next0: ld a,1
ld (ix+balls_struct.dx),a
ret
.next1: ld a,-1
ld (ix+balls_struct.dx),a
ret
check_y: ld a,c
or a
jr z,.next0
ld a,l
sub c
jr z,.next1
jr c,.next1
ret
.next0: ld a,1
ld (ix+balls_struct.dy),a
ret
.next1: ld a,-1
ld (ix+balls_struct.dy),a
ret
sync: ei
halt
di
ret
check_key: push hl
push de
push bc
push ix
push iy
ld c,scankey
rst 10h
pop iy
pop ix
pop bc
pop de
pop hl
xor 1bh ; êîä Esc...
ret nz
.esc: scf
ret
pg_tbl:
.balls: db 0
.bgspr: ds 6
.savescr: ds 6
.tbuf: db 0
pg_w0: db 0
pg_w1: db 0
pg_w2: db 0
pg_w3: db 0
old_y: db 0
data_files:
.balls: db "balls.spr",0
.balls_pal: db "balls.act",0
.bgspr: db "bgspr.spr",0
.bgpal: db "bgspr.act",0
tmp_hndl: db 0
open_err_str: db "Can't open file ",0
read_err_str: db "Failed to read file ",0
gmem_err_str: db "PANIC: Can not allocate memory!",cr,lf,0
vm_err_str: db "PANIC: Unable to set videomode!",cr,lf,0
crlf0: db " ",cr,lf,0
pal: equ ($/80h)*80h+80h
balls_spr: equ ((pal+1024)/80h)*80h+80h
balls_cfg: equ ((balls_spr+1024)/80h)*80h+80h
balls_bkp: equ (((balls_cfg)+6*MAX_BALLS)/80h)*80h+80h
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
+58
View File
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 

  

  

  

  

  
 
 
+38
View File
@@ -0,0 +1,38 @@
Демка была написана как альтернатива аналогичной на evo-sdk (язык C) для zx-evo,
но написано на асме. При этом так же пока писал, разбирался в особенностях работы
графических режимов. Содержание архива:
balls.asm - исходник
dss.inc - различные константы
head.inc - заголовок для формирования exe файла
misc.inc - некоторые "прочие" процедурки.
math.inc - математика, причём тормозная (сдёрнутая из исходника либы для HTC)
make.bat - собиралка. Использование как make src_name.
sjasm.exe - компилятор. Этот меня устраивает.
balls.exe - основной exeшник. На экране 32 шарика.
balls64.exe - 64 шарика.
balls128.exe - 128 шариков.
balls225.exe - 225 шариков.
balls.ect - файл с палитрой для шариков. Хранится в формате 32бита. Выдернуто
из заголовка для bmp шариков.
balls.spr - собсвтенно файл с шариками (спрайты). Ранее это был обычный bmp.
bgspr.act - палитра для фоновой картинка. Так же 32бита выдернутые из bmp.
bgspr.spr - фоновая картинка, тоже бывшая bmp.
Спрайт шариков имеет разрешение 64*16 (т.е. 4 шарика в линию, каждый шарик
16*16), каждый шарик по 32 цвета, т.е. сама картнка с шариками 128 цветов.
Фоновая картинка имеет разрешение 320*256, 128 цветов.
Чтобы получить эти файлы я в фотошопе что нужно было вырезал, отмасштабировал,
"перецветовал" в 8бит на точку, 128 цветов. Шарики собирал по отдельности из
фоновой картинки (просто вырезал из оригинального файла "молекулы", масштабировал,
собирал в одну картинку и потом извращался с цветами. Потом выдернул палитру
через winhex для шариков и фона из их bmp файлов, а из самих bmp потом вырезал
весь заголовок, оставив только данные спрайтов.
Можно спокойно подсунуть свои спрайты фона и шариков.
В демке есть процедура переиндексирования цветов. Первым файлом загружается
фоновая картинка и палитра. Цвета в файле имеют индексы от 0 до 7fh.
Шарики загружаются уже после. Поскольку палитра 256 цветов, а первые 128 уже
заняты под фон, то после загрузки спрайта шариков я делаю переиндексацию. Просто
по всему размеру файла если цвет != 0xff, тогда цвет +=128.
за тормоза в математике сильно не пинать!!!
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long
+68
View File
@@ -0,0 +1,68 @@
;-------------------------
;dss functions defines
;file functions
fopen equ 11h
fclose equ 12h
fread equ 13h
fwrite equ 14h
move_fp equ 15h
fgetattr equ 16h
fgetdt equ 17h
fsetdt equ 18h
fcreate equ 0ah
fcreaten equ 0bh
chdir equ 1dh
curdir equ 1eh
;memory functions
setwin1 equ 39h
getmem equ 3dh
setmem equ 3fh
freemem equ 3eh
;keyb functions
waitkey equ 30h
scankey equ 31h
quit equ 41h
getarg equ 43h
;vmode functions
setvmode equ 50h
getvmode equ 51h
selvpage equ 54h
;screen and text functions
pchar equ 5bh
pchars equ 5ch
;other
getver equ 0
;end dss defines
;-------------------------
;hardware defines
port_y equ 89h ;port for Y coord
_320p equ 81h ;320 pixels mode
rgmod equ 0c9h
border equ 0feh
rgscr equ 0e9h
rgacc equ 0a9h
norm_scr equ 50h
;trans_scr equ 54h
trans_scr equ 00001000b
tmp_scr equ 00000100b
e_cache equ 0fbh
d_cache equ 7bh
sys_port3c equ 3ch
sys_port7c equ 7ch
cpu_w0 equ 82h ;cpu window 0 = addr 0000h
cpu_w1 equ 0a2h ;... 1 = 4000h
cpu_w2 equ 0c2h ;... 2 = 8000h
cpu_w3 equ 0e2h ;... 3 = 0c000h
;end hardware defines
;-------------------------
;characters
cr equ 0dh
lf equ 0ah
tab equ 9
space equ 20h
;------------------------
+20
View File
@@ -0,0 +1,20 @@
; .Z80
; ASEG
; org 100h
org 8100h-512
db "EXE"
db 0
dw 200h
dw 0
dw 0
dw 0
dw 0
dw 0
dw entry
dw entry
dw 0bfffh
ds 490
; .PHASE 8100h
+20
View File
@@ -0,0 +1,20 @@
@echo off
if "%1" == "" goto error
if EXIST %1.exe (
del %1.exe
)
sjasm.exe -L %1.asm %1.exe %1.lst
if errorlevel 1 goto ERR
echo Ok!
goto END
:ERR
del %1.exe
pause
echo ®è¨¡ª¨ ª®¬¯¨«ï樨...
goto END
:error
echo usage: make sourcefile
:END
+89
View File
@@ -0,0 +1,89 @@
;Input: H = Multiplier, E = Multiplicand, L = 0, D = 0
;Output: HL = Product
mul8_8: ld b,7
sla h ; optimised 1st iteration
jr nc,$+3
ld l,e
.loop: add hl,hl ; unroll 7 times
jr nc,$+3 ; ...
add hl,de ; ...
djnz .loop
ret
lmod: call ldiv
ex de,hl
ret
ldiv: xor a
ex af,af'
ex de,hl
jr dv1
adiv: ld a,h
xor d ;set sign flag for quotient
ld a,h ;get sign of dividend
ex af,af'
call negif
ex de,hl
call negif
dv1: ld b,1
ld a,h
or l
ret z
dv8: push hl
add hl,hl
jr c,dv2
ld a,d
cp h
jr c,dv2
jp nz,dv6
ld a,e
cp l
jr c,dv2
dv6: pop af
inc b
jp dv8
dv2: pop hl
ex de,hl
push hl
ld hl,0
ex (sp),hl
dv4: ld a,h
cp d
jr c,dv3
jp nz,dv5
ld a,l
cp e
jr c,dv3
dv5: sbc hl,de
dv3: ex (sp),hl
ccf
adc hl,hl
srl d
rr e
ex (sp),hl
djnz dv4
pop de
ex de,hl
ex af,af'
call m,negat
ex de,hl
or a ;test remainder sign bit
call m,negat
ex de,hl
ret
negif: bit 7,h
ret z
negat: ld b,h
ld c,l
ld hl,0
or a
sbc hl,bc
ret
+113
View File
@@ -0,0 +1,113 @@
; îáùàÿ ïðîöåäóðà âûõîäà.
; â A ïîìåñòèòü êîä âîçâðàòà.
; Åñëè áûëà îøèáêà, òî ëó÷øå -1 (255)
; Åñëè îøèáêè íåáûëî, òî 0.
quit0: pop ix
ld b,a
ld c,quit
rst 10h
jp $ ; çàãëóøêà
;-[]------------------------------------------------------------
; misc procedures and functions...
;---------------------------------------------------------------
open: ld a,1
ld c,fopen
rst 10h
ret
close: ld c,fclose
rst 10h
ret
read: ld c,fread
rst 10h
ret
;dir_ret: ld hl,cur_dir
; ld c,chdir
; rst 10h
; ret
gmem: ld c,getmem
ld b,1
rst 10h
ret
; ïðîöåäóðà ñîõðàíåíèÿ ñòðàíèöû â óêàçííîì îêíå.
; C = îêíî (ïîðò)
; HL = êóäà ñîõðàíÿòü.
save_pg: in a,(c)
ld (hl),a
ret
; ïðîöåäóðà âîññòàíîâëåíèÿ ñòðàíèöû â óêàçííîì îêíå.
; C = îêíî (ïîðò)
; HL = îò êóäà âîññòàíîâèòü.
restore_pg: ld a,(hl)
out (c),a
ret
; îáðàáîòêà îøèáêè îòêðûòèÿ ôàéëà
open_err:
ld hl,open_err_str
ld c,pchars
rst 10h
.err_file: ld hl,0
ld c,pchars
rst 10h
ld hl,crlf0
ld c,pchars
rst 10h
ld a,-1
jp quit0
; îáðàáîòêà îøèáêè ÷òåíèÿ ôàéëà
rd_err:
ld hl,read_err_str
ld c,pchars
rst 10h
.err_file: ld hl,0
ld c,pchars
rst 10h
ld hl,crlf0
ld c,pchars
rst 10h
ld a,-1
jp quit0
; îáðàáîòêà îøèáêè çàïðîñà ïàìÿòè
gmem_err: ld hl,gmem_err_str
ld c,pchars
rst 10h
ld a,-1
jp quit0
; îáðàáîòêà îøèáêè îòêðûòèÿ âèäåîðåæèìà
vm_err: ld hl,vm_err_str
ld c,pchars
rst 10h
jp quit0
IFDEF debug
debug_print: ld c,pchars
rst 10h
ret
ENDIF
; îáðàáîòêà îøèáêîê òîêåíîâ
;tok_err:
; ld hl,token_err_str
; ld c,pchars
; rst 10h
;.token_ptr: ld hl,0
; ld c,pchars
; rst 10h
; ld hl,crlf0
; ld c,pchars
; rst 10h
; ld a,-1
; jp quit0
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := scroll
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
make clean
make run
+223
View File
@@ -0,0 +1,223 @@
# Руководство: скроллинг-примитивы для libbgi
Самодостаточное ТЗ на реализацию двух функций скролла в libbgi. Написано так,
чтобы реализовать с нуля без остального контекста. Итог изысканий по железу
Sprinter (см. §1) и разбора существующего кода libbgi (см. §3).
**Что делаем:** `gfx_scroll_h` (горизонтальный — нужен Loom для комнат шире
320 px) и `gfx_scroll_v` (вертикальный — для симметрии API; Loom пока не
требует, высота комнат 144 < 256).
**Где живёт:** расширение `C-Compiler/libbgi` — accel-leaf в `bgi256/`,
обёртка в `common/`, объявление в `include/gfx.h`.
**СНАЧАЛА прочти §7 (блокеры).** Два пункта надо проверить в dev-MAME до
написания рабочего кода — если они не пройдут, схема «чистого фона» едет.
---
## 1. Аппаратная модель (всё, что нужно знать)
**Режим `0x81`: 320×256×256, chunky — 1 байт/пиксель.** Пиксель (x,y): CPU-адрес
`0xC000 + x`, строку выбирает **Port_Y** (порт `0x89`) `= y`. Горизонталь — в
адресе, вертикаль — в Port_Y. (Ссылка: `libbgi/include/gfx.h` шапка.)
**Две видеостраницы различаются ТОЛЬКО базой строки:** page0 → `0xC000`,
page1 → `0xC140` (шаг `0x140` = 320). Обе адресуемы в W3 **одновременно**;
Port_Y и номера банков — общие для обеих. Значит:
> `dst_addr = src_addr + 0x140` копирует page0→page1 без ремаппинга W3.
> Со сдвигом: `dst = src ± 0x140 + dx`.
**Банки (значение W3-страницы, порт `0xE2` = `0x50..0x5F`)** — режим ЗАПИСИ,
действует на все примитивы до смены (`libbgi/include/gfx.h`):
| Банк | Имя | Запись | Чтение |
|---|---|---|---|
| `0x50` | `GFX_BANK_NORMAL` | VRAM **+ ОЗУ-копия** | ОЗУ-копия |
| `0x54` | `GFX_BANK_NOSHADOW` | только VRAM | ОЗУ-копия |
| `0x58` | `GFX_BANK_TRANSPARENT` | VRAM, байт `0xFF` не пишется | ОЗУ-копия |
| `0x5C` | `GFX_BANK_SPRITE` | VRAM, `0xFF` не пишется | ОЗУ-копия |
Чтение `#50..#5F` **всегда** отдаёт ОЗУ-копию (видео-ОЗУ write-only). `0xFF`
аппаратно-прозрачный цвет (`GFX_TRANSPARENT`).
**Акселератор** (`C-Compiler/docs/converted/accel_r.txt`): внутр. ОЗУ-буфер
1..256 байт. Управление опкодами-«NOP»:
- `LD B,B` — стоп; `LD D,D` затем `LD A,n` — размер блока (`n=0` → 256);
- `LD L,L`**горизонтальная** копия (буфер ← `LD A,(HL)`; буфер → `LD (DE),A`);
- `LD A,A` (`0x7F`) — **вертикальная** копия (линии экрана, Port_Y шагает внутри
burst'а);
- `LD C,C` / `LD E,E` — fill (гориз./верт.).
Скорость ≈ **байт / 7 мкс**. Полный экран 320×256 ≈ **26 мс** (это **>1 кадра**
@ 50 Гц — держать в уме, §9). На время работы акселератора — **DI** (меняется
система команд, ISR сломается).
---
## 2. Ключевой инсайт: чистый фон достаётся бесплатно
- Спрайты рисуются банком с битом 2 (`0x54`/`0x5C`) → **только VRAM**,
ОЗУ-копию не трогают.
- ⇒ ОЗУ-копия (`0x50`) любой страницы — это фон **без спрайтов**, всегда.
- ⇒ скролл-копия из `0x50` в `0x50` читает чистый фон и пишет VRAM+тень — это
**одновременно скролл И heal** целевой страницы (её старые спрайты затираются
сдвинутым фоном).
Ровно так работает `gfx_heal` (частный случай `src==dst`, §3). Скролл — тот же
механизм, но `src != dst`.
---
## 3. Что уже есть в libbgi — образцы для копирования
| Файл | Что | Зачем образец |
|---|---|---|
| `bgi256/_bgi_copy_rows_raw.c` | гориз. leaf (`LD L,L`), src/dst + страйды, Port_Y на строку | ядро всех блитов; страйды, SMC, DI |
| `bgi256/_bgi_heal_rows_raw.c` | `src==dst==экран`, страйды 0 | спец-случай heal |
| `bgi256/_bgi_blit_cols_raw.c` | **верт. leaf** (`LD A,A`), колонка=burst, Port_Y сброс на колонку, `dst+=1` | **прямой образец для `scroll_h`** |
| `common/_gfx_heal_full.c` | клип по экрану + нарезка полос >256 + вызов leaf | образец обёртки-ядра |
| `common/gfx_heal.c` | save-банк → `0x50``_bgi_begin` → ядро → `_bgi_end` → restore | образец верхней обёртки |
| `docs/converted/accel_r.txt` | screen→screen верт. копия LDIR-идиомом, `dst=src+0x140` | канонический скролл-паттерн |
Правило из `_bgi_copy_rows_raw`: **одна сторона копии — видео (в W3, строку даёт
Port_Y), другая может быть линейным буфером ВНЕ W3** (`< 0xC000`). Для скролла
обе стороны — видео (см. §4).
---
## 4. `gfx_scroll_h` — горизонтальный (наш, приоритет)
**Идиом: вертикальный режим `LD A,A`, проход по колонкам.** Высота playfield
(144) ≤ 256 → одна колонка = один burst, **резать 320-строку на 256+64 НЕ надо**
(главная причина брать верт. режим). Это доковый screen→screen паттерн
(`accel_r.txt`) + `_bgi_blit_cols_raw`.
Параметры на колонку:
- `src = base(src_page) + x` — банк `0x50`, чтение = ОЗУ-копия = чистый фон;
- `dst = base(dst_page) + x + dx` — банк `0x50`, запись = VRAM + тень dst;
- `base`: page0 `0xC000`, page1 `0xC140`;
- размер блока = высота playfield (`h`, для Loom 144);
- Port_Y start = верх playfield (`y0`, для Loom 0);
- копируем `(w |dx|)` колонок; вакантные `|dx|` колонок **не трогаем**.
**Знак:** зафиксировать `dx > 0` = сдвиг вправо (открывается слева), `dx < 0` =
влево; вернуть открывшийся край в `*dirty`.
**Порядок (по образцу докового LDIR-примера + `_bgi_blit_cols_raw`):** банда
колонок под DI → `LD D,D`/`LD A,h` (block) → `LD A,A` (верт. режим) → на колонку
`{ read col в accel; write col из accel; src++; dst++ }``EI`. При straight-copy
Port_Y авто-шагает (доковый LDIR его не трогает) — но `_bgi_blit_cols_raw`
сбрасывает Port_Y=`y0` на каждую колонку; **проверить, нужен ли сброс** (§7.1
покажет).
**Возврат:** `*dirty` = прямоугольник `|dx|` колонок с открывшегося края;
полосу не чистим — её дозаполняет вызывающий (в Loom — декодер EGA-страйпов).
```c
/* Горизонтальный скролл playfield. Копирует чистый фон (ОЗУ-копия src_page)
со сдвигом dx в dst_page (обе стороны банк 0x50: запись бьёт VRAM+тень).
dx>0 — вправо. Вакантную полосу НЕ заполняет — возвращает в *dirty. */
void gfx_scroll_h(uint8_t src_page, uint8_t dst_page,
const gfx_rect_t *area, int dx, gfx_rect_t *dirty);
```
---
## 5. `gfx_scroll_v` — вертикальный (симметрия; Loom не нужен)
**Асимметрия железа:** горизонтальный сдвиг — это смещение CPU-адреса (src и dst
независимы) → легко. **Вертикальный сдвиг — это смещение Port_Y, а Port_Y один
на burst** → в простом LDIR-идиоме read и write идут с одного Port_Y, сдвига по
Y нет. Поэтому `scroll_v` сложнее `scroll_h`. Два пути:
- **(A) менять Port_Y между fill и flush одного burst'а**: `OUT Port_Y=y_src`;
read col в accel; `OUT Port_Y=y_src+dy`; write col из accel. Требует, чтобы
accel-буфер пережил `OUT` между чтением и записью — **не доказано** доком/
тестами (§7.3). Если пройдёт — симметрично `scroll_h`, верт. режим.
- **(B) безопасный fallback**: grab (video→RAM-буфер вне W3, гориз. режим),
затем blit (буфер→video со сдвигом Y — буферная сторона CPU-адресуема, сдвиг
любой). Два прохода + буфер, но заведомо работает.
**Рекомендация:** реализовать (A), если §7.3 пройдёт; иначе (B). Делать
**последним** — для Loom не критично. API симметричный (`dy` вместо `dx`):
```c
void gfx_scroll_v(uint8_t src_page, uint8_t dst_page,
const gfx_rect_t *area, int dy, gfx_rect_t *dirty);
```
---
## 6. Общие требования реализации
- **DI по банде, НЕ один DI на весь проход.** Полный playfield ~14 мс под DI
порвёт будущий CBL-звук. Одна колонка (144 Б ≈ 20 мкс) — безопасное DI-окно;
резать проход на банды колонок/строк с `EI` между (как `_bgi_copy_rows_raw`
режет ≤16 строк). Пока звука нет — не критично, но заложить сразу.
- **Клип по `area`** (образец — `_gfx_heal_full`).
- **Буфер (если путь (B))** — вне W3 (`< 0xC000`).
- **Верхняя обёртка**: save банк → `gfx_set_bank(GFX_BANK_NORMAL)`
`_bgi_begin()` → ядро → `_bgi_end()` → restore (образец — `gfx_heal.c`).
- **Размещение**: leaf `bgi256/_bgi_scroll_cols_raw.c` (+ `_rows_raw` для (A)),
обёртки `common/gfx_scroll_h.c` / `gfx_scroll_v.c`, объявления в
`include/gfx.h`. `gfx_rect_t` — если ещё нет, определить там же
(`int x,y,w,h`).
- **Для Loom (не требование к примитиву, но контекст)**: вызывающий держит
`dx` кратным 8 (ширина EGA-страйпа) → `dirty` = целые страйпы. Обёртка этому
не мешает, но и не навязывает.
---
## 7. ПРОВЕРИТЬ ПЕРВЫМ — блокеры (dev-MAME)
1. **Верт. режим (`LD A,A`) в банке `0x50` читает ОЗУ-копию (чистый фон) и
пишет VRAM+тень.** Гориз. heal это доказывает построчно; для верт. прохода —
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
2. **ОЗУ-копия per-page или общая.** `C-Compiler/applications/PoP/docs/double_buffer_plan.md`
пишет «теневая копия одна — общая». Если тень физически одна (а не адресуется
по базе `0xC000/0xC140` как VRAM), page0 и page1 не удержат фон **разных**
положений камеры во время скролла → пинг-понг (§8) сломается. Проверить:
записать разный фон в page0 и page1 банком `0x50`, сверить чтение обеих.
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
---
## 8. Порядок кадра в Loom (контекст использования `scroll_h`)
Пинг-понг двух страниц; спрайты в VRAM-only банк, поэтому тень каждой страницы
остаётся чистым фоном:
1. `gfx_set_draw_page(B)` — B = скрытая;
2. `gfx_scroll_h(A, B, area, dx, &dirty)` — B получает сдвинутый чистый фон в
VRAM+тень; старые спрайты B затёрты (copy = heal);
3. декодировать EGA-страйпы фона в `dirty`, **банк `0x50`** (иначе тень B
останется с дырой — следующий скролл из B прочитает мусор);
4. вывести актора/спрайты в B, банк `0x5C`;
5. `gfx_wait_vsync()``gfx_set_visible_page(B)`.
Следующий кадр: роли A/B меняются, читаем уже чистую тень B. Смаза нет by design
(§2). При `dx==0` (камера стоит) — обычный per-sprite `gfx_heal`, а не полная
копия.
---
## 9. Бюджет и проверка корректности
- 320×256 ≈ 26 мс (**>кадр**); playfield 320×144 ≈ **1415 мс** (<кадр); скролл
копирует `(320dx)` колонок → чуть меньше. Итого копия ~кадр + докод полосы +
спрайты; целевой бюджет ~2 кадра/шаг (plan.md §6). **Замерить на PoC-1**
маркерами `OUT (0xFE)` (до появления звука порт свободен).
- Корректность — визуально/дифф-тестом в dev-MAME. Фон-декодер уже проверен
диффом против ScummVM; скролл проверять на реальной комнате шире 320.
---
## 10. Связанные документы
`plan.md §6` (графика, скроллинг), `toolchain-requirements.md §1` (этот
примитив), `decisions.md`. Железо: `C-Compiler/docs/converted/accel_r.txt`,
`libbgi/include/gfx.h`, `C-Compiler/docs/sprite-api-design.md`,
`C-Compiler/docs/memory-management.md`.
+87
View File
@@ -0,0 +1,87 @@
/*
* scroll визуальная проверка скролл-примитивов libbgi (gfx_scroll_h /
* gfx_scroll_v). Копирует регион из НЕактивной страницы в активную со
* сдвигом и переключает страницы.
*
* ДИАГНОСТИКА. Страница 0 диагональные полосы color=((x+y)>>3)&15
* (любой сдвиг ломает непрерывность диагоналей; фаза по Y ловит
* вертикальный «съезд» Port_Y-квирка). Страница 1 сплошной серый
* фон-маркер: после скролла на нём проступают ровно скопированные
* прямоугольники, по их границам видно корректность (позицию/высоту).
*
* Два региона в одном кадре: слева ГОРИЗОНТАЛЬНЫЙ скролл (dx>0 картинка
* вправо, открывается серая полоса слева), справа ВЕРТИКАЛЬНЫЙ (dy>0
* вниз, открывается серая полоса сверху). Если верт. Port_Y-квирк не
* вылечен правый прямоугольник съедет вниз/опустеет сверху. Esc
* выход.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
static uint8_t egapal[16 * 4];
__sfr __at (0xFE) io_border;
/* Диагональные полосы 16-цветной палитры (8-пиксельные ступени). */
static void draw_diagonals(void)
{
int bx, by;
for (by = 0; by < 256; by += 8)
for (bx = 0; bx < 320; bx += 8) {
setfillstyle(SOLID_FILL, (((bx + by) >> 3) & 15));
bar(bx, by, bx + 7, by + 7);
}
}
/* Регион горизонтального скролла (слева) и вертикального (справа). */
static gfx_rect_t area_h = { 0, 56, 320, 144 };
// static gfx_rect_t area_h = { 16, 48, 80, 80 };
// static gfx_rect_t area_v = { 176, 48, 80, 80 };
int main(void)
{
uint8_t hidden;
initgraph();
/* Палитра страницы 1 = палитре страницы 0 (initgraph грузит EGA
* только в палитру 0). */
gfx_pal_get(0, 0, 16, egapal);
gfx_pal_load(1, 0, 16, egapal);
/* Страница 0 — диагонали (источник фона). */
gfx_set_draw_page(0);
draw_diagonals();
gfx_set_draw_page(1);
draw_diagonals();
gfx_set_visible_page(0);
/* Рисуем на скрытой странице (1), потом флип. */
hidden = gfx_get_visible_page() ^ 1; /* = 1 */
gfx_set_draw_page(hidden);
for (;;) {
if (kbhit() && getch() == 27)
break;
io_border = 2;
gfx_scroll_v(&area_h, 2, 0); /* вправо на 48 px */
// gfx_scroll_v(&area_v, 2, 0); /* вниз на 48 px */
io_border = 0;
gfx_wait_vsync();
gfx_wait_vsync();
hidden = gfx_get_visible_page() ^ 1;
gfx_set_draw_page(hidden ^ 1);
gfx_set_visible_page(hidden);
}
closegraph();
return 0;
}
+25
View File
@@ -37,6 +37,7 @@ extern uint8_t _bgi_prevmode; /* видеорежим до initgraph */
/* CPU-адрес колонки 0 текущей draw-страницы: 0xC000 (page 0) или
* 0xC140 (page 1); каждый примитив использует его вместо константы. */
extern uint16_t _gfx_addr_base;
extern uint16_t _gfx_addr_shadow_base;
/* ---- Низкоуровневый видеорежим (libc/video) ---------------------- */
uint8_t _videomode_raw_get(void);
@@ -108,6 +109,30 @@ void _bgi_heal_rows_raw(uint8_t *scr, int y0, uint8_t w, uint8_t h);
void _bgi_hspan (int x, int y, int len, uint8_t color);
void _bgi_clearall(uint8_t color);
/* Скролл-ядра video->video (банк 0x50: копия = скролл + heal цели; без
* скобки/клиппинга; DI/EI бандами по 16 ВНУТРИ; перекрытия нет стороны
* в разных страницах). Контракты в шапках bgi256/_bgi_scroll_*_raw.c.
*
* _bgi_scroll_rows_raw ядро gfx_scroll_h: сдвиг ТОЛЬКО по X (разница
* адресов src/dst), построчно, БЕЗ страйдов один OUT Port_Y на строку
* (~54Т/строку, на 5-7% быстрее cols). w байт/строка (0=256), h
* строк 1..256 (0=256, конвенция accel), y0 стартовый Port_Y (++ на строку). */
void _bgi_scroll_rows_raw(uint16_t src, uint16_t dst,
uint8_t w, uint8_t h, uint8_t y0);
/* _bgi_scroll_cols_raw — ядро gfx_scroll_v: сдвиг по X И/ИЛИ Y за один
* проход БЕЗ буфера. Колонка = вертикальный accel-burst (LD A,A): читаем
* с Port_Y=y0, пишем с Port_Y=y1 (сдвиг dy = y1y0; dx в адресах). STOP
* между read и write делает промежуточный OUT Port_Y безопасным. w
* число КОЛОНОК 1..255 (0=выход), h высота колонки = размер accel-блока
* (0=256), y0/y1 Port_Y чтения/записи. */
void _bgi_scroll_cols_raw(uint16_t src, uint16_t dst,
uint8_t w, uint8_t h, uint8_t y0, uint8_t y1);
/* Ядро блиттинга gfx_blit_part/gfx_blit/gfx_heal (Фаза B) — публичные,
* прототипы в <gfx.h>; спрайтовые обёртки putsprite/movesprite в
* <graphics.h>. Реализации: common/gfx_blit_part.c и соседи. */
+102
View File
@@ -0,0 +1,102 @@
/*
* _bgi_scroll_cols_raw accel-скролл КОЛОНКАМИ video->video (mode 0x81).
* УНИВЕРСАЛЕН: делает и горизонтальный, и ВЕРТИКАЛЬНЫЙ сдвиг за один
* проход, БЕЗ буфера-посредника. Ядро gfx_scroll_v (вертикаль); может
* служить и горизонтали (gfx_scroll_h держит свой rows-вариант он
* на ~5-7% быстрее, т.к. на строку нужен один OUT Port_Y, а не два).
*
* КАК РАБОТАЕТ. Колонка = один вертикальный accel-burst (LD A,A, 0x7F
* «копия блока для вертикальных линий»): read набирает h байт колонки src
* с Port_Y=y0 (Port_Y авто-инкрементится внутри burst'а: y0y0+h), write
* выгружает их в колонку dst с Port_Y=y1 (y1y1+h). Вертикальный сдвиг =
* разница y0/y1; горизонтальный = разница адресов src/dst (dx). Оба сразу
* = произвольный (dx,dy). dst/src двигаются на +1 (следующий x) за колонку.
*
* ПОЧЕМУ РАБОТАЕТ смена Port_Y между read и write (в отличие от наивного
* rows-варианта, где это ломало accel). Ключ STOP (LD B,B) ПЕРЕД
* OUT Port_Y=y1: он разоружает accel (m_acc_dir=0), поэтому fetch операнда
* OUT (immediate байт 0x89 из программной памяти) НЕ перезапускает
* read-burst. Затем LD A,A снова взводит вертикальный режим для записи.
* Оба OUT'а (y0 и y1) выполняются только пока accel РАЗОРУЖЁН. Порядок на
* колонку: {OUT y0 · arm · read · STOP · OUT y1 · arm · write · STOP}.
* (Подробно про квирк перезапуска memory/accel_operand_fetch_retrigger.)
*
* DI/EI БАНДАМИ по 16 колонок ВНУТРИ (как _gfx_recthfill256): держать DI
* на весь скролл нельзя (порвёт CBL-звук). Размер блока (h) армируется в
* КАЖДОЙ скобке (в окне EI CBL-ISR мог бы переармировать accel). Единая
* тело-петля для полных банд и хвоста (счётчик колонок decrement'ится в
* стек-слоте); y0 держим в C (LD A,C 4Т вместо LD A,#imm 7Т на колонку),
* y1 SMC-immediate (грузится только пока разоружены безопасно).
*
* ВХОД (__sdcccall(1)): src->HL, dst->DE; стек: w 4(ix) число КОЛОНОК
* 1..255 (0 = тихий выход; ровно 256 режет вызывающий), h 5(ix) высота
* колонки = размер accel-блока 1..256 (0 = 256, конвенция accel),
* y0 6(ix) Port_Y чтения, y1 7(ix) Port_Y записи. Callee-pop 4 байта.
*
* Raw: клиппинга нет (вызывающий), W3 замаплен (_bgi_begin/end снаружи),
* банк 0x50 (запись бьёт VRAM+тень скролл И heal цели). Перекрытия
* src/dst нет by design стороны в РАЗНЫХ видеостраницах. Колонок >255 и
* колонки >256 строк режет вызывающий. Клоббер: AF/BC; HL/DE/IX
* сохраняются (LD A,A/LD D,D accel-опкоды-no-op'ы, регистры не трогают).
*/
#include "../_bgi.h"
void _bgi_scroll_cols_raw(uint16_t src, uint16_t dst,
uint8_t w, uint8_t h, uint8_t y0, uint8_t y1) __naked {
(void)src; (void)dst; (void)w; (void)h; (void)y0; (void)y1;
__asm
push ix
ld ix, #0
add ix, sp
;; HL = src, DE = dst (адреса-колонки; строку задаёт Port_Y)
ld a, 5 (ix)
ld (sc_col_len), a ; SMC: размер accel-блока <- h (0 = 256)
ld a, 7 (ix)
ld (sc_col_y1), a ; SMC: Port_Y записи <- y1
ld c, 6 (ix) ; C = Port_Y чтения (y0) LD A,C дешевле imm
sc_col_band:
ld a, 4 (ix) ; колонок осталось
or a, a
jr Z, sc_col_done ; 0 => выход
cp a, #16
jr NC, sc_col_full ; >=16 полная банда
ld b, a ; <16 хвост: B = остаток
jr sc_col_go
sc_col_full:
ld b, #16 ; 16 колонок в банде
sc_col_go:
ld a, 4 (ix)
sub a, b
ld 4 (ix), a ; колонок -= B
di
ld d, d ; 0x52 режим размера блока (no-op для D)
ld a, #0
sc_col_len = . - 1
ld b, b ; выключить размер сохранён (B=счётчик цел)
sc_col_next:
ld a, c ; Port_Y чтения (y0)
out (#0x89), a ; accel РАЗОРУЖЁН операнд OUT безопасен
ld a, a ; 0x7F арм вертикальной копии
ld a, (hl) ; read-burst: колонка src -> буфер (Port_Y y0..)
ld b, b ; 0x40 STOP (разоружить перед OUT y1)
ld a, #0
sc_col_y1 = . - 1
out (#0x89), a ; Port_Y записи (y1) accel разоружён
ld a, a ; 0x7F арм вертикальной копии
ld (de), a ; write-burst: буфер -> колонка dst (Port_Y y1..)
ld b, b ; 0x40 STOP
inc hl ; src += 1 (следующий x)
inc de ; dst += 1
djnz sc_col_next
ei ; окно прерываний между бандами
jr sc_col_band
sc_col_done:
pop ix
;; callee-pop 4 байта (w, h, y0, y1)
pop hl ; ret-адрес
pop af
pop af
jp (hl)
__endasm;
}
+129
View File
@@ -0,0 +1,129 @@
/*
* _bgi_scroll_rows_raw БЫСТРАЯ построчная копия video->video со сдвигом
* ТОЛЬКО по X для gfx_scroll_h (mode 0x81). Специализация _bgi_copy_rows_raw
* для скролла: обе стороны видео, ОБА страйда НУЛЕВЫЕ (CPU-адрес строки
* фиксирован, строку выбирает Port_Y), поэтому адресной арифметики в цикле
* НЕТ ВООБЩЕ ни страйдовых add (как у copy_rows), ни даже inc адресов
* (как у heal). От _bgi_heal_rows_raw отличается тем, что src != dst
* (HL колонка-старт src-страницы, DE dst-страницы со сдвигом dx):
* горизонтальный сдвиг целиком в разнице адресов, задаётся вызывающим.
*
* Вертикального сдвига НЕТ (read и write одной строки на одном Port_Y)
* для сдвига по Y есть _bgi_scroll_cols_raw (колонки, вертикальный режим).
* Зато rows на ~5-7% быстрее cols: на строку один OUT Port_Y (54Т/строку),
* а cols нужен OUT на чтение И на запись колонки.
*
* ПОРЯДОК на строку: {OUT Port_Y · arm(LD L,L) · read(LD A,(HL)) ·
* write(LD (DE),A) · STOP(LD B,B)}. Между read- и write-триггером
* НИЧЕГО: на Sprinter accel перехватывает любое чтение программной памяти,
* а fetch операнда любой инструкции там (immediate у OUT/ADD) спурьезно
* перезапустил бы read-burst и набил буфер кодом. Port_Y ставится ДО
* arm'а (accel разоружён STOP'ом прошлой строки операнд OUT безопасен).
* (memory/accel_operand_fetch_retrigger.)
*
* DI/EI БАНДАМИ по 16 строк ВНУТРИ (как _gfx_recthfill256): накрывать DI
* весь сдвиг нельзя (порвёт CBL-звук). Армирование размера блока в
* КАЖДОЙ скобке (в окне EI CBL-ISR армирует accel своим размером). Число
* полных 16-банд и хвост считаем ОДИН раз на входе (precompute) в петле
* банд остаётся дешёвый dec (без per-band `sub 16` через IX-слот).
* Регистры упёрты (HL src, DE dst, B djnz, C бегущий Port_Y) быстрее уже
* некуда: out(c) без стопов потребовал бы Port_Y-регистра, которого нет.
*
* ВХОД (__sdcccall(1)): src->HL, dst->DE; стек: w 4(ix) байт в строке
* (размер accel-блока, 1..256, 0=256), h 5(ix) строк 1..256 (0 = 256,
* КОНВЕНЦИЯ accel как у w), y0 6(ix) стартовый Port_Y (инкремент на
* строку). Callee-pop 3 байта.
*
* Raw: клиппинга нет (вызывающий), W3 замаплен (_bgi_begin/end снаружи),
* банк 0x50 (запись бьёт VRAM+тень скролл И heal цели). Перекрытия
* src/dst нет by design стороны в РАЗНЫХ видеостраницах. Строку >256
* байт режет вызывающий. Клоббер: AF/BC; HL/DE/IX сохраняются
* (адреса-константы не трогаются LD L,L/LD D,D это accel-опкоды-no-op'ы).
*/
#include "../_bgi.h"
void _bgi_scroll_rows_raw(uint16_t src, uint16_t dst,
uint8_t w, uint8_t h, uint8_t y0) __naked {
(void)src; (void)dst; (void)w; (void)h; (void)y0;
__asm
push ix
ld ix, #0
add ix, sp
;; HL = src, DE = dst (адреса-константы; строку задаёт Port_Y)
ld a, 4 (ix)
ld (sc_row_len), a ; SMC: размер блока <- w (0 = 256)
ld (sc_row_len2), a ; ...и в хвостовую скобку
ld c, 6 (ix) ; C = бегущий Port_Y (y0)
;; --- precompute: полных 16-банд (ix+5) + хвост (SMC); h=0 => 256 ---
ld a, 5 (ix)
and #0x0F
ld (sc_row_tail), a ; хвост = h & 15 (0 и при h=0, и при h=256)
ld a, 5 (ix)
or a, a
jr NZ, sc_row_shr
ld a, #16 ; h=0 => 256 => ровно 16 полных банд
jr sc_row_setfull
sc_row_shr:
rrca
rrca
rrca
rrca
and #0x0F ; full = h >> 4
sc_row_setfull:
ld 5 (ix), a ; ix+5 = число полных 16-строчных банд
sc_row_band:
ld a, 5 (ix)
or a, a
jr Z, sc_row_tailband ; полные банды кончились
dec 5 (ix) ; full -= 1
ld b, #16 ; 16 строк в банде
di
ld d, d ; 0x52 режим размера блока (no-op для D)
ld a, #0
sc_row_len = . - 1
ld b, b ; выключить размер сохранён (B=16 цел)
sc_row_next:
ld a, c
out (#0x89), a ; Port_Y = y (accel ВЫКЛЮЧЕН операнд безопасен)
inc c ; y++
ld l, l ; 0x6D арм гориз. копии (no-op для L)
ld a, (hl) ; read-burst: src-строка -> буфер
ld (de), a ; write-burst: буфер -> dst-строка (вплотную!)
ld b, b ; 0x40 стоп
djnz sc_row_next
ei ; окно прерываний между бандами
jr sc_row_band
;; --- хвост h&15 строк: отдельная скобка со СВОИМ армированием ---
sc_row_tailband:
ld a, #0
sc_row_tail = . - 1
or a, a
jr Z, sc_row_done ; хвоста нет
ld b, a ; B = остаток строк (1..15)
di
ld d, d
ld a, #0
sc_row_len2 = . - 1
ld b, b
sc_row_trow:
ld a, c
out (#0x89), a
inc c
ld l, l
ld a, (hl)
ld (de), a
ld b, b
djnz sc_row_trow
ei
sc_row_done:
pop ix
;; callee-pop 3 байта (w, h, y0)
pop hl ; ret-адрес
pop af
inc sp
jp (hl)
__endasm;
}
+1
View File
@@ -18,6 +18,7 @@ uint8_t _gfx_bank = 0x50;
* (CPU 0xC000+), page 1 320..639 (CPU 0xC140+), остаток дескрипторы
* режима/палитра, их не трогаем. */
uint16_t _gfx_addr_base = 0xC000;
uint16_t _gfx_addr_shadow_base = 0xC140;
/* Клип спрайтов по экрану: 1 = включён (безопасно, дефолт), 0 = выключен
* (приложение само гарантирует, что спрайты не выходят за края даёт
+4 -36
View File
@@ -16,46 +16,14 @@
* ПОЛНОЙ высоте (колонки в ОЗУ лежат full_h друг от друга), длина accel-
* блока по обрезанной. Спрайты Kid < 256 по обеим осям нарезка на
* полосы >256 не нужна (raw принимает uint8_t w,h; 0 => 256). img вне W3.
*
* Тело (вместе с клипом) в gfx_blit_cols_part.c: этот вызов = «весь кадр,
* без обрезки сверху».
*/
#include <stdint.h>
#include "../_bgi.h"
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
{
const uint8_t *p = (const uint8_t *)img;
int full_w = p[0] | (p[1] << 8);
int full_h = p[2] | (p[3] << 8);
const uint8_t *px = p + 4;
int w = full_w, h = full_h;
int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */
int sy = 0; /* пропуск строк сверху колонки */
int cx = 0; /* пропуск колонок слева экрана */
const uint8_t *src;
uint8_t *dst;
if (full_w <= 0 || full_h <= 0)
return;
/* Клип по экрану — только если хоть один край вылезает. */
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
if (x < 0) { cx = -x; w += x; x = 0; }
if (y < 0) { sy = -y; h += y; y = 0; }
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
if (w <= 0 || h <= 0) return;
}
/* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка.
* cx экранных колонок слева пропускаем шагом sstride (в флипе он
* отрицателен уходим к более ранним колонкам источника); sy строк
* сверху простым +sy внутри колонки (порядок строк флип не меняет). */
src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px;
if (cx)
src += cx * sstride;
src += sy;
_bgi_begin();
dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x);
_bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y);
_bgi_end();
gfx_blit_cols_part(x, y, img, flip, 0, 0);
}
+62
View File
@@ -0,0 +1,62 @@
/*
* gfx_blit_cols_part блит спрайта column-major с ОБРЕЗКОЙ СВЕРХУ: рисуются
* только строки [skip, skip+rows) кадра, и (x,y) экранная позиция ПЕРВОЙ
* нарисованной строки (как у gfx_blit_part: координата назначения под-
* прямоугольника). rows <= 0 «до низа кадра».
*
* Нужно для порта clip_char (PoP): персонаж, чья голова ушла под пол/стену,
* рисуется обрезанным по линии пола (obj_clip_top), а не целиком. Обрезка
* сверху для column-major бесплатна: колонка = непрерывный кусок ОЗУ, старт
* просто сдвигается на skip байт, длина accel-блока уменьшается.
*
* Здесь же живёт полное тело блита колонками gfx_blit_cols() тонкая
* обёртка (skip=0, rows=0), чтобы не дублировать клип-логику.
* Подробности зеркала/клипа см. шапку gfx_blit_cols.c.
*/
#include <stdint.h>
#include "../_bgi.h"
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows)
{
const uint8_t *p = (const uint8_t *)img;
int full_w = p[0] | (p[1] << 8);
int full_h = p[2] | (p[3] << 8);
const uint8_t *px = p + 4;
int w = full_w, h;
int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */
int sy; /* пропуск строк сверху колонки */
int cx = 0; /* пропуск колонок слева экрана */
const uint8_t *src;
uint8_t *dst;
if (full_w <= 0 || full_h <= 0)
return;
if (skip < 0) skip = 0;
if (skip >= full_h)
return; /* обрезано целиком */
sy = skip;
h = full_h - skip;
if (rows > 0 && rows < h)
h = rows;
/* Клип по экрану — только если хоть один край вылезает. */
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
if (x < 0) { cx = -x; w += x; x = 0; }
if (y < 0) { sy -= y; h += y; y = 0; }
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
if (w <= 0 || h <= 0) return;
}
/* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. */
src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px;
if (cx)
src += cx * sstride;
src += sy;
_bgi_begin();
dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x);
_bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y);
_bgi_end();
}
+35
View File
@@ -0,0 +1,35 @@
/*
* gfx_blit_noclip блит ЦЕЛОЙ картинки getimage-формата БЕЗ клипа, в
* ТЕКУЩЕМ банке (gfx_set_bank). Быстрый вариант gfx_blit для тайловых
* движков: вызывающий гарантирует, что картинка целиком на экране.
*
* Зачем отдельная функция: gfx_blit идёт через общее ядро _gfx_blit_full
* (16-битная арифметика, клип, пересчёт src, нарезка полос >256) замер
* в MAME (PoP roomtest, 2026-07-27): 13 288 тактов на блит спрайта 32×3,
* т.е. цена почти не зависит от размера, платим за проход по цепочке.
* Линейное спрайтовое ядро на том же спрайте 4 617 тактов (в 2.9 раза
* быстрее): 8-битные w/h, без split, без клип-кода.
*
* От putsprite отличается тем, что НЕ навязывает GFX_BANK_SPRITE: фон
* рисуется в GFX_BANK_TRANSPARENT (0xFF не пишется, но ОЗУ-копия
* обновляется иначе gfx_heal сотрёт нарисованное).
*
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): вызывать её из
* приложения, собранного с --w3, нельзя после _bgi_begin окно W3
* занято видеобанком и код вызывающего исчезает из адресного
* пространства (проверено: белый экран).
*
* Ограничения: w,h 255; картинка целиком в пределах экрана. Не
* подходит зовите gfx_blit/gfx_blit_part.
*/
#include "../_bgi.h"
void gfx_blit_noclip(int x, int y, const void *img)
{
const uint8_t *p = (const uint8_t *)img;
_bgi_begin();
_gfx_blit_sprite_noclip(x, y, p + 4, (uint16_t)(p[0] | (p[1] << 8)),
p[0], p[2]);
_bgi_end();
}
+30
View File
@@ -0,0 +1,30 @@
/*
* gfx_heal_noclip восстановить прямоугольник экрана из ОЗУ-копии БЕЗ
* клипа: вызывающий гарантирует, что прямоугольник целиком на экране,
* w,h 255. Быстрый вариант gfx_heal для тайловых движков (пара к
* gfx_blit_noclip).
*
* Зачем: gfx_heal идёт через общее ядро _gfx_heal_full (клип, нарезка
* полос >256, 16-битная арифметика) замер в MAME (PoP roomtest,
* 2026-07-27): heal области 22×22 стоил 11 658 тактов при кадре 430 080,
* т.е. цена почти вся в обвязке, а не в пикселях. Линейное спрайтовое
* ядро _gfx_heal_sprite_noclip делает ту же работу без этого груза.
*
* Банк как у gfx_heal: на время операции ВСЕГДА GFX_BANK_NORMAL
* (чтение отдаёт ОЗУ-копию, запись должна дойти и до ОЗУ), затем
* пользовательский банк восстанавливается. W3-скобка ставится ЗДЕСЬ:
* из модуля, собранного с --w3, её вызывать нельзя (после _bgi_begin
* окно W3 занято видеобанком и код вызывающего исчезает).
*/
#include "../_bgi.h"
void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h)
{
uint8_t saved = gfx_get_bank();
gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin();
_gfx_heal_sprite_noclip(x, y, w, h);
_bgi_end();
gfx_set_bank(saved);
}
+64
View File
@@ -0,0 +1,64 @@
/*
* gfx_scroll_h ГОРИЗОНТАЛЬНЫЙ скролл региона (mode-agnostic обёртка).
*
* Берёт чистый фон из НЕактивной (теневой) страницы и копирует его со
* сдвигом dx по X в активную (draw) страницу. Обе стороны банк 0x50
* (GFX_BANK_NORMAL): запись бьёт VRAM + ОЗУ-копию, т.е. копия
* ОДНОВРЕМЕННО скроллит И «лечит» целевую страницу (старые спрайты
* затираются сдвинутым фоном; scroll-impl-guide.md §2).
*
* dx > 0 сдвиг в сторону УВЕЛИЧЕНИЯ X (изображение едет вправо,
* открывается полоса слева); dx < 0 влево (открывается справа);
* dx == 0 чистая копия фона (полный heal целевой страницы). В обеих
* страницах остаёмся в пределах area. Открывшуюся |dx|-полосу НЕ
* заполняем возвращаем её в *dirty (её дозаполняет вызывающий).
*
* РЕАЛИЗАЦИЯ построчно, через _bgi_scroll_rows_raw (без страйдов, один
* OUT Port_Y на строку на ~5-7% быстрее колоночного _bgi_scroll_cols_raw,
* которому на колонку нужен OUT и на чтение, и на запись). Горизонтальный
* сдвиг чистая разница CPU-адресов src/dst (src_x vs dst_x), Port_Y для
* read и write один (одна строка). DI/EI бандами по 16 строк ВНУТРИ
* leaf'а; он же режет высоту на банды и принимает h 1..256 (0=256), так что
* здесь делим только ширину >256 (лимит accel-блока). Перекрытий src/dst
* нет: стороны в разных страницах.
*/
#include "../include/sprite.h"
#include "../_bgi.h"
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
uint8_t saved = gfx_get_bank();
uint16_t adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx;
/* src-строка стартует правее на |dx| при сдвиге влево, dst — правее
* на dx при сдвиге вправо; так оба края остаются внутри area. */
uint16_t src_x = area->x + ((dx < 0) ? adx : 0);
uint16_t dst_x = area->x + ((dx > 0) ? adx : 0);
uint16_t w = area->w - adx; /* байт (пикселей) в строке */
uint8_t h = (uint8_t)area->h; /* строк (256 => 0 = 256 у leaf) */
uint16_t src = _gfx_addr_shadow_base + src_x;
uint16_t dst = _gfx_addr_base + dst_x;
gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin();
/* Ширина >256 байт — банды ≤256 (размер accel-блока, 0=256). Высоту и
* DI/EI-банды по 16 строк leaf делает сам (h: 0=256). Между бандами по
* ширине CPU-адрес двигаем на 256 (Port_Y выбирает строку). */
while (w) {
uint8_t cw = (w > 256) ? 256 : (uint8_t)w; /* 0 => 256 */
_bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y);
src += 256; dst += 256;
w -= (w > 256) ? 256 : w;
}
_bgi_end();
gfx_set_bank(saved);
if (dirty) {
dirty->x = (dx >= 0) ? area->x : (int16_t)(area->x + area->w - adx);
dirty->y = area->y;
dirty->w = adx;
dirty->h = area->h;
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
* gfx_scroll_v ВЕРТИКАЛЬНЫЙ скролл региона (mode-agnostic обёртка).
*
* Симметрична gfx_scroll_h, но сдвиг по Y. Берёт чистый фон из НЕактивной
* (теневой) страницы и копирует его со сдвигом dy по Y в активную (draw)
* страницу. Банк 0x50: копия = скролл + heal целевой страницы (старые
* спрайты затираются сдвинутым фоном; см. gfx_scroll_h / guide §2).
*
* dy > 0 сдвиг в сторону УВЕЛИЧЕНИЯ Y (изображение едет вниз,
* открывается полоса сверху); dy < 0 вверх (открывается снизу);
* dy == 0 чистая копия (полный heal). Открывшуюся |dy|-полосу НЕ
* заполняем возвращаем в *dirty.
*
* РЕАЛИЗАЦИЯ колонками, через _bgi_scroll_cols_raw (вертикальный режим
* accel): колонку читаем с Port_Y=ys, пишем с Port_Y=yd (= ys+dy) сдвиг
* по Y без буфера-посредника (STOP между read и write делает промежуточный
* OUT Port_Y безопасным; см. шапку leaf'а). Один проход, вдвое быстрее
* прежнего grabblit-через-буфер. Ширину >255 режем на банды колонок
* (leaf сам делит их на DI-скобки по 16). Высота колонки h = area->h|dy|
* (256, 0=256 у accel). Перекрытия src/dst нет: стороны в разных страницах.
*/
#include "../include/sprite.h"
#include "../_bgi.h"
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) {
uint8_t saved = gfx_get_bank();
uint16_t ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy;
/* Строку чтения (ys) сдвигаем вниз на |dy| при сдвиге вверх, строку
* записи (yd) вниз на dy при сдвиге вниз; края в пределах area. */
uint8_t ys = (uint8_t)(area->y + ((dy < 0) ? ady : 0));
uint8_t yd = (uint8_t)(area->y + ((dy > 0) ? ady : 0));
uint16_t hb = area->h - ady; /* строк к переносу (≤256) */
if (hb) { /* 0 = переносить нечего */
uint8_t hblk = (uint8_t)hb; /* размер accel-блока: 256 => 0 */
uint16_t w = area->w;
uint16_t src = _gfx_addr_shadow_base + area->x;
uint16_t dst = _gfx_addr_base + area->x;
gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin();
/* >255 колонок — банды (leaf 8-битный по числу колонок; 0=выход,
* поэтому 255, а не 256). Высота колонки одна на весь проход. */
while (w) {
uint8_t cw = (w > 255) ? 255 : (uint8_t)w;
_bgi_scroll_cols_raw(src, dst, cw, hblk, ys, yd);
src += cw; dst += cw; w -= cw;
}
_bgi_end();
gfx_set_bank(saved);
}
if (dirty) {
dirty->x = area->x;
dirty->y = (dy >= 0) ? area->y : (int16_t)(area->y + area->h - ady);
dirty->w = area->w;
dirty->h = ady;
}
}
+7 -1
View File
@@ -10,5 +10,11 @@ void gfx_set_draw_page(uint8_t page)
_gfx_draw_page = page & 1;
/* Прямые константы короче (0xC000 + (cond ? 0x140 : 0)) на 3
* инструкции Z80 SDCC не сворачивает сложение констант. */
_gfx_addr_base = _gfx_draw_page ? 0xC140 : 0xC000;
if(_gfx_draw_page) {
_gfx_addr_base = 0xC140;
_gfx_addr_shadow_base = 0xC000;
} else {
_gfx_addr_base = 0xC000;
_gfx_addr_shadow_base = 0xC140;
}
}
+20
View File
@@ -87,6 +87,18 @@ inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; }
/* Блит целой картинки левым-верхним углом в (x,y). */
void gfx_blit(int x, int y, const void *img);
/* Быстрый блит БЕЗ клипа в ТЕКУЩЕМ банке: картинка обязана быть целиком на
* экране, w/h 255. Идёт линейным спрайтовым ядром ~2.9× быстрее
* gfx_blit (замер PoP roomtest 2026-07-27: 4617 против 13288 тактов на
* спрайт 32×3; общее ядро тратит время на клип/16-бит/split, а не на
* пиксели). Для тайловых движков; где нужен клип gfx_blit/gfx_blit_part. */
void gfx_blit_noclip(int x, int y, const void *img);
/* Быстрый heal БЕЗ клипа: прямоугольник обязан быть целиком на экране,
* w,h 255. Пара к gfx_blit_noclip (общее ядро тратит время на клип и
* 16-бит, а не на пиксели: замер 11 658 тактов на heal 22×22). */
void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h);
/* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров:
* кадр N ленты sx = N*FRAME_W). Под-прямоугольник обязан лежать
* внутри img; по экрану клиппится. */
@@ -100,6 +112,14 @@ void gfx_blit_part(int x, int y, const void *img,
* Для персонажей (флип); фон обычный gfx_blit (row-major). */
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);
/* То же с ОБРЕЗКОЙ СВЕРХУ: рисуются только строки [skip, skip+rows) кадра,
* (x,y) экранная позиция ПЕРВОЙ нарисованной строки (как gfx_blit_part).
* rows <= 0 = «до низа кадра». Для персонажа, ушедшего головой под пол
* (порт clip_char/obj_clip_top в PoP). Обрезка сверху бесплатна: колонка
* непрерывный кусок ОЗУ, сдвигается только старт. */
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows);
/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
* оверлея, нарисованного банком с битом 2: 0x54/0x5C). Всегда
* работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
+12
View File
@@ -308,4 +308,16 @@ void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx);
void gfx_w0_map(uint8_t page);
void gfx_w0_unmap(void);
typedef struct {
int16_t x, y;
uint16_t w, h;
} gfx_rect_t;
/* Скролл региона area из НЕактивной страницы в активную со сдвигом на
* dx/dy (0 = чистая копия/heal, >0 = в сторону увеличения координат,
* <0 = уменьшения). Открывшуюся полосу |d| НЕ заполняют возвращают
* в *dirty (может быть NULL). Банк 0x50: копия = скролл + heal цели. */
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty);
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty);
#endif
+11 -3
View File
@@ -48,9 +48,13 @@ uint8_t kbd_raw_down(uint16_t code);
/* Восстановление после Rx-overrun SIO: звать РАЗ В КАДР (до чтения
* kbd_raw_down). Аппаратный FIFO SIO 3 байта; при длинных DI-окнах пачка
* скан-кодов (напр. shift+стрелка release) переполняет его потерян break
* залипшая клавиша. Трамплин ловит overrun и взводит флаг; kbd_raw_sync
* по флагу сбрасывает ВСЁ held-состояние (реально зажатые перечитаются). */
* скан-кодов (напр. быстрый тап стрелки: make+break = 5 байт) переполняет
* его потерян break залипшая клавиша. Трамплин ловит overrun и
* взводит флаг; kbd_raw_sync по флагу сбрасывает held-состояние всех
* клавиш КРОМЕ модификаторов (Shift/Ctrl/Alt): PS/2 автоповторяет только
* последнюю нажатую клавишу, поэтому обычные зажатые перечитаются
* typematic'ом, а сброшенный модификатор восстановить нечем он бы
* «отваливался» при каждом overrun (см. kbd_raw_sync.c). */
void kbd_raw_sync(void);
#define KBD_EXT 0x0100 /* база кода была расширенной (0xE0-префикс) */
@@ -66,5 +70,9 @@ void kbd_raw_sync(void);
#define KBD_ESC 0x76
#define KBD_LSHIFT 0x12
#define KBD_RSHIFT 0x59
#define KBD_LCTRL 0x14
#define KBD_LALT 0x11
#define KBD_RCTRL (KBD_EXT | 0x14)
#define KBD_RALT (KBD_EXT | 0x11)
#endif
+26 -11
View File
@@ -3,25 +3,40 @@
*
* Аппаратный приёмный FIFO SIO 3 байта. Если прерывания запрещены
* достаточно долго (DI-окна графики/кадрового IRQ), а в этот момент
* приходит пачка скан-кодов (напр. одновременное отпускание shift+стрелка
* = F0 12 E0 F0 74, 5 байт) FIFO переполняется, байт(ы) теряются
* аппаратно. Потерянный break клавиша «залипает» в _kbdraw_down.
* приходит пачка скан-кодов (напр. быстрый тап стрелки = E0 74 + E0 F0 74,
* 5 байт) FIFO переполняется, байт(ы) теряются аппаратно. Потерянный
* break клавиша «залипает» в _kbdraw_down.
*
* Трамплин ловит overrun (RR1 бит5) и взводит _kbdraw_overrun. Приложение
* зовёт kbd_raw_sync() раз в кадр: при взведённом флаге сбрасывает ВСЁ
* held-состояние (какой именно break потерян неизвестно). Реально
* зажатые клавиши «перечитаются» их make-повтором/следующим нажатием.
*
* ВНИМАНИЕ: путь overrun-recovery НЕ проверен детерминированно (баг
* интермиттентный, зависит от тайминга DI) требует проверки на железе.
* зовёт kbd_raw_sync() раз в кадр: при взведённом флаге сбрасывается
* held-состояние ВСЕХ клавиш, КРОМЕ модификаторов (Shift/Ctrl/Alt
* таблица ниже). Обычные реально зажатые клавиши «перечитаются» их
* make-повтором (typematic) за ~0.1 с. Модификаторы НЕ сбрасываем
* принципиально: PS/2 автоповторяет только ПОСЛЕДНЮЮ нажатую клавишу,
* удерживаемый Shift при повторе стрелки не шлёт НИЧЕГО сброшенный
* модификатор невозможно перечитать, он «отваливался» до перенажатия
* (баг PoP: Shift+ = осторожный шаг превращался в бег, 2026-07-22).
* Цена: если overrun потерял именно break модификатора, тот залипнет до
* следующего нажатия редкий случай против регулярного отвала.
*/
#include <string.h>
#include "_kbdraw.h"
/* Скан-коды модификаторов (PS/2 Set 2): L/R Shift, LCtrl, LAlt,
* RCtrl (E0 14 +256), RAlt (E0 11 +256). */
static const uint16_t _kbd_mod_codes[6] = {
0x12, 0x59, 0x14, 0x11, 0x0114, 0x0111
};
void kbd_raw_sync(void)
{
if (_kbdraw_overrun) {
uint8_t save[6];
uint8_t i;
if (!_kbdraw_overrun) return;
_kbdraw_overrun = 0;
for (i = 0; i < 6; i++) save[i] = _kbdraw_down[_kbd_mod_codes[i]];
memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
}
for (i = 0; i < 6; i++) _kbdraw_down[_kbd_mod_codes[i]] = save[i];
}
+23
View File
@@ -121,6 +121,17 @@ w2_join:
ld (_estex_block_id), a
ld (_estex_startup_ix), ix
.ifdef W3_RESIDENT
;; --w3 в HUGE: резидентный код окна W3 (0xC000) — часть HOME-образа,
;; замаплен DSS. Загрузка банков ниже подменяет W3 своими страницами;
;; запомним физ-страницу резидента, чтобы вернуть её дефолтом после
;; цикла — тогда прямые вызовы в резидентный W3-код снова корректны.
;; (Трамплин bank.s сам сохраняет/восстанавливает W3 на каждый __banked
;; вызов, поэтому дефолтная страница обязана быть резидентной.)
in a, (#0xE2)
ld (_w3_resident_page), a
.endif
;; ---- Allocate _n_banks pages via ESTEX GETMEM ----
ld a, (_n_banks)
or a, a
@@ -182,6 +193,14 @@ load_bank_done:
rst #0x10
;; Ignore CF from close.
.ifdef W3_RESIDENT
;; Вернуть резидентную страницу W3 дефолтом (цикл выше оставил в W3
;; последний банк). После этого прямые call'ы в резидентный код 0xC0xx
;; корректны, а трамплины будут сохранять/возвращать именно её.
ld a, (_w3_resident_page)
out (#0xE2), a
.endif
skip_bank_load:
;; ---- Standard SDCC init path ----
call gsinit
@@ -411,6 +430,10 @@ _estex_block_id::
.ds 1
_bank_block_id::
.ds 1
.ifdef W3_RESIDENT
_w3_resident_page:
.ds 1
.endif
.area _DATA
_argc::
+42
View File
@@ -0,0 +1,42 @@
# w3probe — проверка --w3 (резидентный код окна W3, прямой вызов без
# трамплинов) во ВСЕХ режимах памяти. Все пробы переиспользуют w3res.c
# (резидентный W3-модуль: w3_show/w3_probe/w3_sig).
#
# small : код 0x4100 (W1/W2), W3 резидент. w3probe.exe
# tiny : код 0x8100 (W2), W1 не исп., W3 резидент. w3tiny.exe
# big : код 0x8100 (W2), банки в W1, W3 резидент. w3big.exe
# huge : код 0x4100, W3 ДЕЛЯТ резидент + трамплин-банки. w3huge.exe
#
# Проверено в MAME (2026-07-21): все режимы работают, включая huge —
# резидент↔W3-банк через трамплин в W1, crt0_banked возвращает резидентную
# страницу дефолтом. Раскладка страниц: tiny W1=FF (не исп.), big/huge W1=F1.
PROJ_ROOT := $(abspath $(CURDIR)/../..)
CC := $(PROJ_ROOT)/bin/sprinter-cc
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
HDD_IMG := $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd
EXES := w3probe.exe w3tiny.exe w3big.exe w3huge.exe
all: $(EXES)
w3probe.exe: w3probe.c w3res.c
$(CC) --memory small --w3 w3res.c -o $@ w3probe.c
w3tiny.exe: w3tiny.c w3res.c
$(CC) --memory tiny --w3 w3res.c -o $@ w3tiny.c
w3big.exe: w3big.c w3res.c bigbank.c
$(CC) --memory big --w3 w3res.c --bank 1=bigbank.c -o $@ w3big.c
w3huge.exe: w3huge.c w3res.c w3huge_res.c hugebank.c
$(CC) --memory huge --w3 w3res.c --w3 w3huge_res.c --bank 1=hugebank.c -o $@ w3huge.c
# Упаковать все четыре на HDD (D:) для прогона в MAME. После — рестарт MAME.
hdd: $(EXES)
$(MAKE_HDD) $(HDD_IMG) $(EXES)
clean:
rm -rf .sprinter-cc-* $(EXES)
.PHONY: all hdd clean
+12
View File
@@ -0,0 +1,12 @@
/* bigbank.c — свапаемый банк в W1 (big mode, трамплин через порт 0xA2).
* Отдельно от резидентного W3-кода (w3res.c) проверяем сосуществование. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
void bigbank_func(void) __banked
{
printf("W1-BANK: bigbank_func @ ~0x4xxx (trampoline), W1 phys=0x%02X\n",
_io_page_w1);
}
+13
View File
@@ -0,0 +1,13 @@
/* hugebank.c — СВАПАЕМЫЙ банк в окне W3 (huge mode, трамплин через 0xE2).
* Отдельная страница, подменяет резидентный код на время __banked-вызова;
* трамплин обязан вернуть резидентную страницу по выходу. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
void hugebank_func(void) __banked
{
printf("W3-BANK: hugebank_func (trampoline), W3 phys now=0x%02X\n",
_io_page_w3);
}
+26
View File
@@ -0,0 +1,26 @@
/* w3big.c — проба --w3 в режиме big.
* big: код 0x8100 (W2), свапаемые банки в W1 (трамплины), W3 свободно под
* резидент. Проверяем сосуществование: W1-банк (трамплин) + W3-резидент
* (прямой вызов) в одной программе. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
/* crt0_banked читает это ДО gsinit — только `const`. */
const uint8_t n_banks = 1;
void bigbank_func(void) __banked; /* W1-банк, трамплин */
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямо */
extern void w3_show(void);
int main(void)
{
printf("BIG : main @ 0x%04X | W1=%02X W2=%02X W3=%02X\n",
(unsigned)&main, _io_page_w1, _io_page_w2, _io_page_w3);
bigbank_func(); /* W1 через трамплин */
w3_show(); /* W3 напрямую */
printf("BIG : w3_probe(0x42)=0x%02X (expect 0xBD), exit to DSS.\n",
(unsigned)w3_probe(0x42));
return 0;
}
+33
View File
@@ -0,0 +1,33 @@
/* w3huge.c — проба --w3 в режиме huge (самый сложный случай).
* huge: код 0x4100 (W1/W2), окно W3 ДЕЛЯТ резидентный код (0xC000, прямой
* вызов) и свапаемые трамплин-банки. crt0_banked захватывает резидентную
* страницу и возвращает её дефолтом после загрузки банков; трамплин
* сохраняет/восстанавливает W3 на каждый __banked-вызов.
*
* Ключевая проверка: w3_show() (резидент) вызывается ДО и ПОСЛЕ
* hugebank_func() (банк). Второй вызов проходит только если трамплин
* вернул резидентную страницу в W3. */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
const uint8_t n_banks = 1; /* один свапаемый W3-банк */
void hugebank_func(void) __banked; /* W3-банк, трамплин */
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямой вызов */
extern void w3_show(void); /* W3-резидент, прямой вызов */
extern void w3_resident_calls_bank(void); /* W3-резидент, зовёт банк */
int main(void)
{
printf("HUGE: main @ 0x%04X | W1=%02X W2=%02X W3(resident)=%02X\n",
(unsigned)&main, _io_page_w1, _io_page_w2, _io_page_w3);
w3_show(); /* резидент напрямую (до банка) */
hugebank_func(); /* HOME → W3-банк (свап+возврат) */
w3_resident_calls_bank(); /* резидент → W3-банк (трамплин W1) */
w3_show(); /* резидент СНОВА — возврат стр. */
printf("HUGE: w3_probe(0x42)=0x%02X (expect 0xBD), exit to DSS.\n",
(unsigned)w3_probe(0x42));
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
/* w3huge_res.c — второй резидентный W3-модуль для huge-пробы.
*
* Содержит РЕЗИДЕНТНУЮ функцию (W3CODE @0xC000, прямой вызов), которая
* сама зовёт W3 __banked-функцию (свапаемый банк, трамплин). Этот случай
* ДОЛЖЕН работать: трамплин ___sdcc_bcall_ehl живёт в _CODE (W1, всегда
* замаплен) он сохраняет резидентную страницу W3, маппит банк, а по
* возврату восстанавливает резидента. Адрес возврата (0xC0xx) снова
* валиден, и управление корректно возвращается в резидентную функцию.
*
* (Контраст с запретом «из __banked-контекста нельзя дёрнуть резидент»:
* здесь наоборот резидент зовёт банк, и это ок.) */
#include <stdio.h>
#include <stdint.h>
#include <sprinter.h>
void hugebank_func(void) __banked; /* W3-банк, трамплин */
void w3_resident_calls_bank(void)
{
printf("W3-RES: w3_resident_calls_bank @ 0x%04X -> calling W3 __banked...\n",
(unsigned)&w3_resident_calls_bank);
hugebank_func(); /* резидент → банк через трамплин W1 */
printf("W3-RES: returned into resident OK (W3 phys now=0x%02X)\n",
_io_page_w3);
}
+27
View File
@@ -0,0 +1,27 @@
/* w3probe.c — явная проба подхода A для --w3.
*
* main (W1/W2) зовёт функцию w3_show() в W3, которая печатает свой
* собственный адрес (должен быть 0xC0xx) через printf. Затем main
* проверяет прямой вызов с возвратом и ЧИСТО выходит в DSS (return 0),
* чтобы вернулось приглашение шелла. */
#include <stdio.h>
#include <stdint.h>
extern const uint8_t w3_sig; /* rodata в W3 */
extern uint8_t w3_probe(uint8_t x); /* код в W3, прямой вызов */
extern void w3_show(void); /* код в W3, зовёт printf */
int main(void)
{
printf("HOME: main @ 0x%04X | w3_probe @ 0x%04X | w3_show @ 0x%04X\n",
(unsigned)&main, (unsigned)&w3_probe, (unsigned)&w3_show);
w3_show(); /* W3-код печатает свой адрес */
printf("HOME: w3_probe(0x42) = 0x%02X (expect 0xBD)\n",
(unsigned)w3_probe(0x42));
printf("HOME: done, exiting to DSS.\n");
return 0; /* чистый выход → приглашение DSS */
}
+25
View File
@@ -0,0 +1,25 @@
/* w3res.c — резидентный модуль окна W3 (проба подхода A, явная версия).
*
* Компилируется с --codeseg W3CODE --constseg W3CODE (БЕЗ --dataseg),
* линкуется по -Wl-b_W3CODE=0xC000. Здесь только код + rodata. */
#include <stdio.h>
#include <stdint.h>
/* rodata в W3 (для проверки, что область реально по 0xC000). */
const uint8_t w3_sig = 0x5A;
/* Функция, живущая в W3. Печатает СВОЙ адрес через printf — а printf
* лежит в libc (_CODE = W1/W2). Тем самым проверяем оба направления:
* W3-код исполняется по 0xC0xx И умеет звать код в W1/W2 обычным call'ом. */
void w3_show(void)
{
printf("W3 : w3_show @ 0x%04X | w3_sig @ 0x%04X = 0x%02X\n",
(unsigned)&w3_show, (unsigned)&w3_sig, (unsigned)w3_sig);
}
/* Прямой вызов с возвратом значения: x^0xFF. */
uint8_t w3_probe(uint8_t x)
{
return (uint8_t)(x ^ 0xFF);
}
+30
View File
@@ -0,0 +1,30 @@
/* w3tiny.c — проба --w3 в режиме tiny.
* tiny: код с 0x8100 (W2), W1 не используется, W3 свободно под резидент.
* Образ тянется 0x8100..0xC0xx (2 страницы) DSS маппит W2 и W3. */
#include <stdio.h>
#include <stdint.h>
extern uint8_t w3_probe(uint8_t x);
extern void w3_show(void);
uint8_t w1_p, w2_p, w3_p; /* физ-страницы окон (заполняются asm) */
int main(void)
{
__asm
in a,(#0xA2)
ld (_w1_p), a
in a,(#0xC2)
ld (_w2_p), a
in a,(#0xE2)
ld (_w3_p), a
__endasm;
printf("TINY: main @ 0x%04X | pages W1=%02X W2=%02X W3=%02X\n",
(unsigned)&main, w1_p, w2_p, w3_p);
w3_show();
printf("TINY: w3_probe(0x42)=0x%02X (expect 0xBD), exit to DSS.\n",
(unsigned)w3_probe(0x42));
return 0;
}
+359
View File
@@ -0,0 +1,359 @@
Creative Commons Legal Code
Attribution-ShareAlike 3.0 Unported
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
DAMAGES RESULTING FROM ITS USE.
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the Work and
other pre-existing works, such as a translation, adaptation,
derivative work, arrangement of music or other alterations of a
literary or artistic work, or phonogram or performance and includes
cinematographic adaptations or any other form in which the Work may be
recast, transformed, or adapted including in any form recognizably
derived from the original, except that a work that constitutes a
Collection will not be considered an Adaptation for the purpose of
this License. For the avoidance of doubt, where the Work is a musical
work, performance or phonogram, the synchronization of the Work in
timed-relation with a moving image ("synching") will be considered an
Adaptation for the purpose of this License.
b. "Collection" means a collection of literary or artistic works, such as
encyclopedias and anthologies, or performances, phonograms or
broadcasts, or other works or subject matter other than works listed
in Section 1(f) below, which, by reason of the selection and
arrangement of their contents, constitute intellectual creations, in
which the Work is included in its entirety in unmodified form along
with one or more other contributions, each constituting separate and
independent works in themselves, which together are assembled into a
collective whole. A work that constitutes a Collection will not be
considered an Adaptation (as defined below) for the purposes of this
License.
c. "Creative Commons Compatible License" means a license that is listed
at https://creativecommons.org/compatiblelicenses that has been
approved by Creative Commons as being essentially equivalent to this
License, including, at a minimum, because that license: (i) contains
terms that have the same purpose, meaning and effect as the License
Elements of this License; and, (ii) explicitly permits the relicensing
of adaptations of works made available under that license under this
License or a Creative Commons jurisdiction license with the same
License Elements as this License.
d. "Distribute" means to make available to the public the original and
copies of the Work or Adaptation, as appropriate, through sale or
other transfer of ownership.
e. "License Elements" means the following high-level license attributes
as selected by Licensor and indicated in the title of this License:
Attribution, ShareAlike.
f. "Licensor" means the individual, individuals, entity or entities that
offer(s) the Work under the terms of this License.
g. "Original Author" means, in the case of a literary or artistic work,
the individual, individuals, entity or entities who created the Work
or if no individual or entity can be identified, the publisher; and in
addition (i) in the case of a performance the actors, singers,
musicians, dancers, and other persons who act, sing, deliver, declaim,
play in, interpret or otherwise perform literary or artistic works or
expressions of folklore; (ii) in the case of a phonogram the producer
being the person or legal entity who first fixes the sounds of a
performance or other sounds; and, (iii) in the case of broadcasts, the
organization that transmits the broadcast.
h. "Work" means the literary and/or artistic work offered under the terms
of this License including without limitation any production in the
literary, scientific and artistic domain, whatever may be the mode or
form of its expression including digital form, such as a book,
pamphlet and other writing; a lecture, address, sermon or other work
of the same nature; a dramatic or dramatico-musical work; a
choreographic work or entertainment in dumb show; a musical
composition with or without words; a cinematographic work to which are
assimilated works expressed by a process analogous to cinematography;
a work of drawing, painting, architecture, sculpture, engraving or
lithography; a photographic work to which are assimilated works
expressed by a process analogous to photography; a work of applied
art; an illustration, map, plan, sketch or three-dimensional work
relative to geography, topography, architecture or science; a
performance; a broadcast; a phonogram; a compilation of data to the
extent it is protected as a copyrightable work; or a work performed by
a variety or circus performer to the extent it is not otherwise
considered a literary or artistic work.
i. "You" means an individual or entity exercising rights under this
License who has not previously violated the terms of this License with
respect to the Work, or who has received express permission from the
Licensor to exercise rights under this License despite a previous
violation.
j. "Publicly Perform" means to perform public recitations of the Work and
to communicate to the public those public recitations, by any means or
process, including by wire or wireless means or public digital
performances; to make available to the public Works in such a way that
members of the public may access these Works from a place and at a
place individually chosen by them; to perform the Work to the public
by any means or process and the communication to the public of the
performances of the Work, including by public digital performance; to
broadcast and rebroadcast the Work by any means including signs,
sounds or images.
k. "Reproduce" means to make copies of the Work by any means including
without limitation by sound or visual recordings and the right of
fixation and reproducing fixations of the Work, including storage of a
protected performance or phonogram in digital form or other electronic
medium.
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
limit, or restrict any uses free from copyright or rights arising from
limitations or exceptions that are provided for in connection with the
copyright protection under copyright law or other applicable laws.
3. License Grant. Subject to the terms and conditions of this License,
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
perpetual (for the duration of the applicable copyright) license to
exercise the rights in the Work as stated below:
a. to Reproduce the Work, to incorporate the Work into one or more
Collections, and to Reproduce the Work as incorporated in the
Collections;
b. to create and Reproduce Adaptations provided that any such Adaptation,
including any translation in any medium, takes reasonable steps to
clearly label, demarcate or otherwise identify that changes were made
to the original Work. For example, a translation could be marked "The
original work was translated from English to Spanish," or a
modification could indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as incorporated
in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
e. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme cannot be waived, the Licensor
reserves the exclusive right to collect such royalties for any
exercise by You of the rights granted under this License;
ii. Waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme can be waived, the Licensor waives the
exclusive right to collect such royalties for any exercise by You
of the rights granted under this License; and,
iii. Voluntary License Schemes. The Licensor waives the right to
collect royalties, whether individually or, in the event that the
Licensor is a member of a collecting society that administers
voluntary licensing schemes, via that society, from any exercise
by You of the rights granted under this License.
The above rights may be exercised in all media and formats whether now
known or hereafter devised. The above rights include the right to make
such modifications as are technically necessary to exercise the rights in
other media and formats. Subject to Section 8(f), all rights not expressly
granted by Licensor are hereby reserved.
4. Restrictions. The license granted in Section 3 above is expressly made
subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the terms
of this License. You must include a copy of, or the Uniform Resource
Identifier (URI) for, this License with every copy of the Work You
Distribute or Publicly Perform. You may not offer or impose any terms
on the Work that restrict the terms of this License or the ability of
the recipient of the Work to exercise the rights granted to that
recipient under the terms of the License. You may not sublicense the
Work. You must keep intact all notices that refer to this License and
to the disclaimer of warranties with every copy of the Work You
Distribute or Publicly Perform. When You Distribute or Publicly
Perform the Work, You may not impose any effective technological
measures on the Work that restrict the ability of a recipient of the
Work from You to exercise the rights granted to that recipient under
the terms of the License. This Section 4(a) applies to the Work as
incorporated in a Collection, but this does not require the Collection
apart from the Work itself to be made subject to the terms of this
License. If You create a Collection, upon notice from any Licensor You
must, to the extent practicable, remove from the Collection any credit
as required by Section 4(c), as requested. If You create an
Adaptation, upon notice from any Licensor You must, to the extent
practicable, remove from the Adaptation any credit as required by
Section 4(c), as requested.
b. You may Distribute or Publicly Perform an Adaptation only under the
terms of: (i) this License; (ii) a later version of this License with
the same License Elements as this License; (iii) a Creative Commons
jurisdiction license (either this or a later license version) that
contains the same License Elements as this License (e.g.,
Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible
License. If you license the Adaptation under one of the licenses
mentioned in (iv), you must comply with the terms of that license. If
you license the Adaptation under the terms of any of the licenses
mentioned in (i), (ii) or (iii) (the "Applicable License"), you must
comply with the terms of the Applicable License generally and the
following provisions: (I) You must include a copy of, or the URI for,
the Applicable License with every copy of each Adaptation You
Distribute or Publicly Perform; (II) You may not offer or impose any
terms on the Adaptation that restrict the terms of the Applicable
License or the ability of the recipient of the Adaptation to exercise
the rights granted to that recipient under the terms of the Applicable
License; (III) You must keep intact all notices that refer to the
Applicable License and to the disclaimer of warranties with every copy
of the Work as included in the Adaptation You Distribute or Publicly
Perform; (IV) when You Distribute or Publicly Perform the Adaptation,
You may not impose any effective technological measures on the
Adaptation that restrict the ability of a recipient of the Adaptation
from You to exercise the rights granted to that recipient under the
terms of the Applicable License. This Section 4(b) applies to the
Adaptation as incorporated in a Collection, but this does not require
the Collection apart from the Adaptation itself to be made subject to
the terms of the Applicable License.
c. If You Distribute, or Publicly Perform the Work or any Adaptations or
Collections, You must, unless a request has been made pursuant to
Section 4(a), keep intact all copyright notices for the Work and
provide, reasonable to the medium or means You are utilizing: (i) the
name of the Original Author (or pseudonym, if applicable) if supplied,
and/or if the Original Author and/or Licensor designate another party
or parties (e.g., a sponsor institute, publishing entity, journal) for
attribution ("Attribution Parties") in Licensor's copyright notice,
terms of service or by other reasonable means, the name of such party
or parties; (ii) the title of the Work if supplied; (iii) to the
extent reasonably practicable, the URI, if any, that Licensor
specifies to be associated with the Work, unless such URI does not
refer to the copyright notice or licensing information for the Work;
and (iv) , consistent with Ssection 3(b), in the case of an
Adaptation, a credit identifying the use of the Work in the Adaptation
(e.g., "French translation of the Work by Original Author," or
"Screenplay based on original Work by Original Author"). The credit
required by this Section 4(c) may be implemented in any reasonable
manner; provided, however, that in the case of a Adaptation or
Collection, at a minimum such credit will appear, if a credit for all
contributing authors of the Adaptation or Collection appears, then as
part of these credits and in a manner at least as prominent as the
credits for the other contributing authors. For the avoidance of
doubt, You may only use the credit required by this Section for the
purpose of attribution in the manner set out above and, by exercising
Your rights under this License, You may not implicitly or explicitly
assert or imply any connection with, sponsorship or endorsement by the
Original Author, Licensor and/or Attribution Parties, as appropriate,
of You or Your use of the Work, without the separate, express prior
written permission of the Original Author, Licensor and/or Attribution
Parties.
d. Except as otherwise agreed in writing by the Licensor or as may be
otherwise permitted by applicable law, if You Reproduce, Distribute or
Publicly Perform the Work either by itself or as part of any
Adaptations or Collections, You must not distort, mutilate, modify or
take other derogatory action in relation to the Work which would be
prejudicial to the Original Author's honor or reputation. Licensor
agrees that in those jurisdictions (e.g. Japan), in which any exercise
of the right granted in Section 3(b) of this License (the right to
make Adaptations) would be deemed to be a distortion, mutilation,
modification or other derogatory action prejudicial to the Original
Author's honor and reputation, the Licensor will waive or not assert,
as appropriate, this Section, to the fullest extent permitted by the
applicable national law, to enable You to reasonably exercise Your
right under Section 3(b) of this License (right to make Adaptations)
but not otherwise.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. Termination
a. This License and the rights granted hereunder will terminate
automatically upon any breach by You of the terms of this License.
Individuals or entities who have received Adaptations or Collections
from You under this License, however, will not have their licenses
terminated provided such individuals or entities remain in full
compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
survive any termination of this License.
b. Subject to the above terms and conditions, the license granted here is
perpetual (for the duration of the applicable copyright in the Work).
Notwithstanding the above, Licensor reserves the right to release the
Work under different license terms or to stop distributing the Work at
any time; provided, however that any such election will not serve to
withdraw this License (or any other license that has been, or is
required to be, granted under the terms of this License), and this
License will continue in full force and effect unless terminated as
stated above.
8. Miscellaneous
a. Each time You Distribute or Publicly Perform the Work or a Collection,
the Licensor offers to the recipient a license to the Work on the same
terms and conditions as the license granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
offers to the recipient a license to the original Work on the same
terms and conditions as the license granted to You under this License.
c. If any provision of this License is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this License, and without further action
by the parties to this agreement, such provision shall be reformed to
the minimum extent necessary to make such provision valid and
enforceable.
d. No term or provision of this License shall be deemed waived and no
breach consented to unless such waiver or consent shall be in writing
and signed by the party to be charged with such waiver or consent.
e. This License constitutes the entire agreement between the parties with
respect to the Work licensed here. There are no understandings,
agreements or representations with respect to the Work not specified
here. Licensor shall not be bound by any additional provisions that
may appear in any communication from You. This License may not be
modified without the mutual written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced, in this
License were drafted utilizing the terminology of the Berne Convention
for the Protection of Literary and Artistic Works (as amended on
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
and the Universal Copyright Convention (as revised on July 24, 1971).
These rights and subject matter take effect in the relevant
jurisdiction in which the License terms are sought to be enforced
according to the corresponding provisions of the implementation of
those treaty provisions in the applicable national law. If the
standard suite of rights granted under applicable copyright law
includes additional rights not granted under this License, such
additional rights are deemed to be included in the License; this
License is not intended to restrict the license of any rights under
applicable law.
Creative Commons Notice
Creative Commons is not a party to this License, and makes no warranty
whatsoever in connection with the Work. Creative Commons will not be
liable to You or any party on any legal theory for any damages
whatsoever, including without limitation any general, special,
incidental or consequential damages arising in connection to this
license. Notwithstanding the foregoing two (2) sentences, if Creative
Commons has expressly identified itself as the Licensor hereunder, it
shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of doubt,
this trademark restriction does not form part of the License.
Creative Commons may be contacted at https://creativecommons.org/.
+27
View File
@@ -0,0 +1,27 @@
# 16x16 RPG characters `v3.0`
16x16px RPG character sprite sheet for up to down games.
This asset pack has been downloaded from https://route1rodent.itch.io/16x16-rpg-character-sprite-sheet
Adapted from opengameart.org 's "[NES-Style RPG Characters](https://opengameart.org/content/nes-style-rpg-characters)" and "[More NES-style RPG Characters](https://opengameart.org/content/more-nes-style-rpg-characters)", with additional content made by [@route1rodent](https://route1rodent.itch.io).
## License
16x16 RPG character sprite sheet (c) by @route1rodent
"16x16 RPG character sprite sheet" is licensed under a
Creative Commons Attribution-ShareAlike 3.0 Unported License (CC BY-SA 3.0).
You should have received a copy of the license along with this
work. If not, see http://creativecommons.org/licenses/by-sa/3.0/.
## Credits
Maintained by @route1rodent:
- itch.io: https://route1rodent.itch.io
- Twitter: https://twitter.com/route1rodent
- Github: https://github.com/itsjavi
- Blog: https://blog.itsjavi.com
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

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