Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50aa652dbe | |||
| 08c6a8504f | |||
| 723da3c5c1 | |||
| e143ee010a | |||
| b106aba59b | |||
| 5e09bd9d5a | |||
| 6582154381 | |||
| c127a4b0d2 | |||
| 63a3b60524 | |||
| 86d7615841 | |||
| 3d586af031 | |||
| 95c22be9bd | |||
| d1bc97c589 | |||
| bf7ddfd4da | |||
| 7f3e32ddd6 | |||
| 21f978d441 | |||
| 8b30dc20c8 |
@@ -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
|
||||
|
||||
@@ -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` — квирки
|
||||
упаковки ассетов.
|
||||
@@ -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).
|
||||
@@ -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`. Содержит
|
||||
|
||||
@@ -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, разобрав, как устроены ресурсы игры в двух доступных нам
|
||||
версиях:
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# План: порт `clip_char()` — обрезка спрайта персонажа
|
||||
|
||||
Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8»
|
||||
(Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке).
|
||||
|
||||
## Симптом
|
||||
|
||||
room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт
|
||||
Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии
|
||||
пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке.
|
||||
|
||||
## Корень
|
||||
|
||||
Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из
|
||||
`add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ
|
||||
`set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД
|
||||
`add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и
|
||||
прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только
|
||||
внутри него. Мы рисуем без клипа вообще.
|
||||
|
||||
## Что делает оригинал (дословно)
|
||||
|
||||
`reset_obj_clip()` → `left=0, top=0, right=320, bottom=192`.
|
||||
|
||||
Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый
|
||||
`get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена
|
||||
ИЛИ пол»):
|
||||
|
||||
```
|
||||
T_L = get_tile(room, char_col_left, char_top_row)
|
||||
T_R = get_tile(room, char_col_right, char_top_row)
|
||||
if (T_L — стена или пол) &&
|
||||
( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */
|
||||
|| (T_R — стена или пол) )
|
||||
{
|
||||
clip_row = Char.curr_row + 1;
|
||||
clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */
|
||||
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y))
|
||||
obj_clip_top = char_top_y = clip_y;
|
||||
}
|
||||
```
|
||||
|
||||
Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой
|
||||
пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»).
|
||||
Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО.
|
||||
|
||||
Метрики из `set_char_collision` (seg006:0723):
|
||||
- `char_x_left = obj_x/2 + 58` (и `-= char_width_half`, если смотрит вправо)
|
||||
- `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2`
|
||||
- `char_top_y = obj_y - h + 1`; если `>= 192` → `0`
|
||||
- `char_top_row = y_to_row_mod4(char_top_y)`
|
||||
- `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`,
|
||||
`char_col_right = MIN(get_tile_div_mod(char_x_right), 9)`
|
||||
|
||||
Второй блок `clip_char` — `obj_clip_right` (doortop / стена / зеркало для
|
||||
виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228
|
||||
(выход в дверь уровня). **Фаза 2**, не в этом заходе.
|
||||
|
||||
## Наши точки касания
|
||||
|
||||
- `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит
|
||||
через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает
|
||||
прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`.
|
||||
- `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`,
|
||||
`get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже
|
||||
есть в `check_spike_below()` (строка ~1198) — брать за образец.
|
||||
- `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0`
|
||||
пропускает `sy` верхних строк колонки), но произвольной верхней границы нет.
|
||||
|
||||
## Шаги
|
||||
|
||||
1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
||||
int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`».
|
||||
Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник
|
||||
`+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`.
|
||||
Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро).
|
||||
2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w,
|
||||
uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y
|
||||
(0 = клипа нет). Экспорт в `pop_map.h`.
|
||||
3. **pop_kid.c**: в `kid_draw()` после расчёта `top` —
|
||||
`ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`,
|
||||
рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`;
|
||||
в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее
|
||||
и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает
|
||||
`reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять).
|
||||
4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`,
|
||||
`make size-check`; вывести свободное место в W2/W3 (порог 512 Б).
|
||||
5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после
|
||||
пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск.
|
||||
Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`).
|
||||
|
||||
## Риски / что проверить отдельно
|
||||
|
||||
- `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт −1 для
|
||||
полосы у потолка; `get_tile(row −1)` уже умеет ряд 2 верхнего соседа.
|
||||
- Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип
|
||||
прыгнет.
|
||||
- `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в
|
||||
обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой
|
||||
должно отсекать).
|
||||
- Клип меняет прямоугольник heal → возможен «хвост» на второй странице
|
||||
дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер).
|
||||
|
||||
## Дальше (Фаза 2, отдельно)
|
||||
|
||||
`obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при
|
||||
взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по
|
||||
колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но
|
||||
без тестовой сцены проверять нечем.
|
||||
|
||||
См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`,
|
||||
`pop_check_sdlpop_first`, `gfx_blit_noclip_fast`.
|
||||
@@ -0,0 +1,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 в комнату сверху) — ещё не сделан; можно закрыть до
|
||||
объектов или параллельно.
|
||||
@@ -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) | 0x4100–0xAB6F |
|
||||
| `_HOME` | 227 Б | 0xAB6F |
|
||||
| `_DATA` | 3 449 Б (0x0D79) | 0xAC78–0xB9F1 |
|
||||
| `_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.5–0.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 (лишние блиты, которых нет в оригинале).
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
@@ -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=61−57=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 = aligned−4 = 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+(184−140)+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 | | |
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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); }
|
||||
@@ -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
|
||||
@@ -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,18 +442,26 @@ static void start_fall(void)
|
||||
kid_set_seq(seq);
|
||||
play_seq();
|
||||
determine_col();
|
||||
if (get_tile_at_char() == TILE_WALL) {
|
||||
in_wall();
|
||||
return;
|
||||
}
|
||||
/* Хвост seg006:1099: тайл ПЕРЕД персонажем — стена → отодвинуть на 1
|
||||
* назад (иначе передний край зайдёт в грань). (seq_104 для frame 44 с
|
||||
* малым зазором не портирован — в этом тесте не встречается.) */
|
||||
tile = get_tile_infrontof_char();
|
||||
if (tile == TILE_WALL) {
|
||||
if (frame != 44 || distance_to_edge_weight() >= 6)
|
||||
Kid.x = (uint8_t)char_dx_forward(-1);
|
||||
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;
|
||||
}
|
||||
/* Хвост seg006:1099: тайл ПЕРЕД персонажем — стена → отодвинуть на 1
|
||||
* назад (иначе передний край зайдёт в грань). (seq_104 для frame 44 с
|
||||
* малым зазором не портирован — в этом тесте не встречается.) */
|
||||
tile = get_tile_infrontof_char();
|
||||
if (tile == TILE_WALL) {
|
||||
if (frame != 44 || distance_to_edge_weight() >= 6)
|
||||
Kid.x = (uint8_t)char_dx_forward(-1);
|
||||
determine_col();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: уход вбок → переход в соседнюю комнату */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 /* край уступа над ямой (дистанция = до кромки) */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
kid_draw(); /* нарисовать новый кадр */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,7 +187,32 @@ def load_indexed(chtab, id):
|
||||
if im.mode != "P":
|
||||
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
|
||||
src = im.tobytes()
|
||||
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
|
||||
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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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 (был неверный вывод, ниже — то, что реально
|
||||
подтвердилось):** изначально скриншот-тестирование (снимок экрана
|
||||
|
||||
@@ -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 (Адрес запуска);
|
||||
@@ -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`.
|
||||
```
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,58 @@
|
||||
������������������������������������������������������� ���������������
|
||||
|
||||
|
||||
|
||||
|
||||
���� ���������
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
�� ������
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
�� ���
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
���
|
||||
|
||||
|
||||
��������
|
||||
|
||||
|
||||
�� ���������
|
||||
|
||||
|
||||
�������������������
|
||||
|
||||
|
||||
������ ������������������������
|
||||
|
||||
|
||||
���������� ���������������
|
||||
@@ -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.
|
||||
|
||||
за тормоза в математике сильно не пинать!!!
|
||||
|
After Width: | Height: | Size: 80 KiB |
@@ -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
|
||||
;------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := scroll
|
||||
EXTRA_FLAGS ?= --gfx 256
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
make clean
|
||||
make run
|
||||
@@ -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 ≈ **14–15 мс** (<кадр); скролл
|
||||
копирует `(320−dx)` колонок → чуть меньше. Итого копия ~кадр + докод полосы +
|
||||
спрайты; целевой бюджет ~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`.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = y1−y0; 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 и соседи. */
|
||||
|
||||
@@ -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'а: y0→y0+h), write
|
||||
* выгружает их в колонку dst с Port_Y=y1 (y1→y1+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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = выключен
|
||||
* (приложение само гарантирует, что спрайты не выходят за края — даёт
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'а). Один проход, вдвое быстрее
|
||||
* прежнего grab→blit-через-буфер. Ширину >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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
_kbdraw_overrun = 0;
|
||||
memset(_kbdraw_down, 0, sizeof(_kbdraw_down));
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/.
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |