diff --git a/app.mk b/app.mk index f65e918..063beea 100644 --- a/app.mk +++ b/app.mk @@ -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 diff --git a/applications/PoP/CLAUDE.md b/applications/PoP/CLAUDE.md new file mode 100644 index 0000000..ba293e9 --- /dev/null +++ b/applications/PoP/CLAUDE.md @@ -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.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 клавиатуры (единственный принципиальный + пробел движка, закрыт ``): вычерпывать FIFO SIO циклом. +- `png_strip_padding_tradeoff`, `pop_tile_atlas_palette_merge` — квирки + упаковки ассетов. diff --git a/applications/PoP/README.md b/applications/PoP/README.md new file mode 100644 index 0000000..75fba6e --- /dev/null +++ b/applications/PoP/README.md @@ -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). diff --git a/applications/PoP/docs/MSDOS_RESOURCE_FORMAT.md b/applications/PoP/docs/MSDOS_RESOURCE_FORMAT.md index 5034e56..580e92b 100644 --- a/applications/PoP/docs/MSDOS_RESOURCE_FORMAT.md +++ b/applications/PoP/docs/MSDOS_RESOURCE_FORMAT.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`. Содержит diff --git a/applications/PoP/docs/POP-DAT-FormatSpecifications.txt b/applications/PoP/docs/POP-DAT-FormatSpecifications.txt new file mode 100644 index 0000000..c2aa09a --- /dev/null +++ b/applications/PoP/docs/POP-DAT-FormatSpecifications.txt @@ -0,0 +1,2345 @@ +Prince of Persia +Specifications of +File Formats +Princed Development Team +January 5, 2008 + +Contents +1 Preamble + +3 + +2 Introduction + +3 + +3 DAT v1.0 Format Specifications +3.1 General file specifications . . . . . . . . . . . . . . . . . . . . . . +3.1.1 Some definitions . . . . . . . . . . . . . . . . . . . . . . . +3.1.2 Index structures . . . . . . . . . . . . . . . . . . . . . . . +3.1.3 Checksums byte . . . . . . . . . . . . . . . . . . . . . . . +3.2 Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +3.2.1 Headers . . . . . . . . . . . . . . . . . . . . . . . . . . . . +3.2.2 Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . +3.2.3 Run length encoding (RLE) . . . . . . . . . . . . . . . . . +3.2.4 LZ variant (LZG) . . . . . . . . . . . . . . . . . . . . . . . +3.3 Palettes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +3.4 Levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +3.4.1 Unknown blocks . . . . . . . . . . . . . . . . . . . . . . . +3.4.2 Room mapping . . . . . . . . . . . . . . . . . . . . . . . . +3.4.3 Wall drawing algorithm . . . . . . . . . . . . . . . . . . . +3.4.4 Room linking . . . . . . . . . . . . . . . . . . . . . . . . +3.4.5 Guard handling . . . . . . . . . . . . . . . . . . . . . . . . +3.4.6 Start Position . . . . . . . . . . . . . . . . . . . . . . . . +3.4.7 Door events . . . . . . . . . . . . . . . . . . . . . . . . . . +3.5 Digital Waves . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +3.6 Midi music . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +3.7 Internal PC Speaker . . . . . . . . . . . . . . . . . . . . . . . . . +3.8 Binary files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +3.9 Levels in POP1 for Mac . . . . . . . . . . . . . . . . . . . . . . . + +3 +3 +3 +4 +5 +5 +6 +6 +7 +7 +8 +10 +11 +11 +14 +15 +16 +16 +17 +17 +18 +18 +18 +18 + +1 + + 4 DAT v2.0 Format Specifications +19 +4.1 General file specifications . . . . . . . . . . . . . . . . . . . . . . 19 +4.1.1 The master index . . . . . . . . . . . . . . . . . . . . . . . 20 +4.1.2 The slave indexes . . . . . . . . . . . . . . . . . . . . . . 21 +4.2 Levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 +4.2.1 Room mapping . . . . . . . . . . . . . . . . . . . . . . . 22 +4.2.2 Door events . . . . . . . . . . . . . . . . . . . . . . . . . 24 +4.2.3 Guard handling . . . . . . . . . . . . . . . . . . . . . . . . 24 +4.2.4 Static guards . . . . . . . . . . . . . . . . . . . . . . . . . 25 +4.2.5 Dynamic guards . . . . . . . . . . . . . . . . . . . . . . . 26 +5 PLV v1.0 Format Specifications +26 +5.1 User data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 +5.2 Allowed Date format . . . . . . . . . . . . . . . . . . . . . . . . . 27 +6 SAV v1.0 Format Specifications + +28 + +7 HOF v1.0 Format Specifications + +28 + +8 Credits + +29 + +9 License + +29 + +2 + + 1 + +Preamble + +This file was written thanks to the hard work on reverse engineering made by +several people, see the credits section. In case you find any mistake in the text +please report it. A copy of this document should be available in our official site +at http://www.princed.org. + +2 + +Introduction + +There are two versions of the DAT file format: DAT v1.0 used in POP 1.x and +DAT v2.0 used in POP 2. In this document we will specify DAT v1.0. +DAT files were made to store levels, images, palettes, wave, midi and internal +speaker sounds. Each type has its own format as described below in the following +sections. +As the format is very old and the original game was distributed in disks, it +is normal to think that the file format uses some kind of checksum validation +to detect file corruptions. +DAT files are indexed, this means that there is an index and you can access +each resource through an ID that is unique for the resource inside the file. +Images store their height and width but not their palette, so the palette is +another resource and must be shared by a group of images. +PLV files use the extension defined to support a format with only one level +inside. +Both versions of DAT are supported and may be manipulated by PR. This +program works like an archive manager (i.e. pkzip) and extracts the files in +known formats that may be handled by other programs. For more information +about PR check the Princed home page at http://www.princed.org. +In this document you will also find the SAV and HOF format specifications +and the algorithm used by POP1 to draw the dungeon walls. + +3 + +DAT v1.0 Format Specifications + +3.1 + +General file specifications + +All DAT files have an index, this index has one entry per item with information +on each one. +The index is stored at the very end of the file. But may be located reading +the first 6 bytes (as headers) of the file, that are reserved to locate the index +and know it’s size. The sum of the location and the index size should be the +size of the file. +3.1.1 + +Some definitions + +Let’s define the numbers as: + +3 + + SC Signed char: 8 bits, the first bit is for the sign and the 7 last for the number. +If the first bit is a 0, then the number is positive, if not the number is +negative, in that case invert all bits and add 1 to get the positive number. +i.e. -1 is FF (1111 1111), 1 is 01 (0000 0001) +Range: -128 to 127 +1 byte +UC Unsigned char: 8 bits that represent the number. +i.e. 32 is 20 (0010 0000) +Range: 0 to 255 +1 byte +US Unsigned Short: Little endian, 16 bits, storing two groups of 8 bits ordered +from the less representative to the most representative without sign. +i.e. 65534 is FFFE in hex and is stored FE FF (1111 1110 1111 1111) +Range: 0 to 65535 +2 bytes +SS Signed Short: Little endian, 16 bits, storing two groups of 8 bits ordered +from the less representative to the most representative with sign. If the +first byte is 0 then the number is positive, if not the number is negative, +in that case invert all bits and add 1 to get the positive number. +i.e. -2 is FFFE in hex and is stored FE FF (1111 1110 1111 1111) +Range: -32768 to 32767 +2 bytes +UL Unsigned long: Little endian, 32 bits, storing four groups of 8 bits each +ordered from the less representative to the most representative without +sign. +i.e. 65538 is 00010002 in hex and is stored 02 00 01 00 +(0000 0010 0000 0000 0000 0001 0000 0000) +Range: 0 to 232 − 1 +4 bytes +Sizes will always be expressed in bytes unless another unit is specified. + +3.1.2 + +Index structures + +Offset + +Size + +Type + +Name + +0 + +4 + +UL + +IndexOf f set + +4 + +2 + +US + +IndexSize1 + +1 IndexOf f set + IndexSize = f ilesize + +4 + +Description +The location where the index begins +The number of bytes the index has + + Offset + +Size + +Type + +Name + +IndexOf f set = α + +IndexSize + +↓ + +F ooter + +α + +2 + +US + +N umberOf Items + +α + 2 = β2 + +N umberOf Items ∗ 8 + +- + +Index + +β + 8i = γ + +8 + +↓ + +Entry + +γ+0 + +2 + +US + +Id(i)3 + +γ+2 + +4 + +UL + +Of f set(i)3 + +γ+6 + +2 + +US + +Size(i)3 + +Description +The DAT index +Resources count +A list of NumberOfItems +blocks of 8-bytes-length index record called Entry +The 8-bytes-length index +record (one per item) +Item ID +Absolute offset where the resource start +Size of the item not including the checksum byte + +Table 1: DAT file blocks +Note: POP1 doesn’t validate the DAT file checking IndexOf f set + +IndexSize = F ileSize, this means you can append data at the end of the +file. +PR validates that IndexOf f set + IndexSize ≤ F ileSize. It also compares +IndexSize with 8 ∗ numberOf Items + 2 to determine if a file is a valid POP1 +DAT file. +3.1.3 + +Checksums byte + +There is a checksum byte for each item (resource), this is the first byte of the +item, the rest of the bytes are the item data. The item type is not stored and may +only be determined by reading the data and applying some filters, unfortunately +this method may fail. When you extract an item you should know what kind +of item you are extracting. +If you add (sum) the whole item data including checksum and take the less +representative byte (modulus 256) you will get the sum of the file. This sum +must be FF in hex (255 in UC or -1 in SC). If the sum is not FF, then adjust +the checksum in order to set this value to the sum. The best way to do that +is adding all the bytes in the item data (excluding the checksum) and inverting +all the bits. The resulting byte will be the right checksum. +From now on the specification are special for each data type (that means we +won’t include the checksum byte anymore). + +3.2 + +Images + +Images are stored compressed and have a header and a compressed data area. +Each image only one header with 6 bytes in it as follows +2 so IndexSize = 8 ∗ numberOf Items + 2 +3 with 0 ≤ i < numberOf Items + +5 + + 3.2.1 + +Headers +Offset Size Type + +Name + +Description + +0 + +6 + +↓ + +ImageHeader + +0 + +2 + +US + +Height + +2 + +2 + +US + +W idth + +4 + +2 + +- + +ImageM ask + +The header of an image +The height of the image in +pixels +The width of the image in +pixels +Information on the compression algorithm and bitrate + +Table 2: Image headers + +ImageMask is a set of bits where: +⋄ the first 8 are zeros +⋄ the next 4 are the resolution +so if it is 1011 (B in hex) then the image has 16 colours; and if it is 0000 +(0 in hex) then the image has 2 colours. To calculate the bits per pixel +there are in the image, just take the last 2 bits and add 1. e. g. 11 is 4 +(24 = 16 colours) and 00 is 1 (21 = 2 colours). +⋄ the last 4 bits are the 5 compression types (from 0 to 4) as specified in +Table 3. +Dec +Bin +Algorithm +0 +0000 +RAW LR +1 +0001 +RLE LR +2 +0010 +RLE UD +3 +0011 +LZG LR +4 +0100 +LZG UD +Table 3: Algorithm codes + +The following data in the resource is the image compressed with the algorithm specified by those 4 bits. +3.2.2 + +Algorithms + +RAW LR means that the data has not been compressed in any way, it is used +for small images. The format is saved from left to right (LR) serialising a +line to the next integer byte if necessary. In case the image was 16 colours, +two pixels per byte (4bpp) will be used. In case the image was 2 colours, +8 pixels per byte (1bpp) will be used. + +6 + + RLE LR has a Run length encoding (RLE) algorithm, after uncompressed the +image can be read as a RAW LR. +RLE UD is the same as RLE LR except that after uncompressed the bytes in +the image must be drawn from up to down and then from left to right. +LZG LR has some kind of variant of the LZ77 algorithm (the sliding windows +algorithm), here we named it LZG in honour of Lance Groody, the original +coder. After decompressed it may be handled as RAW LR. +LZG UD Uses LZG compression but is drawn from top to bottom as RLE UD. +3.2.3 + +Run length encoding (RLE) + +The first byte is always a control byte, the format is SC. If the control byte is +negative, then the next byte must be repeated n times as the bit inverted control +byte says, after the next byte (the one that was repeated) another control byte +is stored. +If the control byte is positive or zero just copy textual the next n bytes where +n is the control byte plus one. After that, the next byte is the following control +byte. +If you reach a control byte but the image size is passed, then you have completed +the image. +3.2.4 + +LZ variant (LZG) + +This is a simplified algorithm explanation: +Definition: “print” means to commit a byte into the current location of the +output stream. +The output stream is a slide window initialised with zeros. +The first byte of the input stream is a maskbyte. +For each of the 8 bits in the maskbyte the next actions must be performed: +If the bit is 1 print the next unread byte to the slide window +If the bit is a zero read the next two bytes as control bytes with +the following format (RRRRRRSS SSSSSSSS): +· 6 bits for the copy size number (R). +Range: 2 to 26 + 2 = 66 + +Add 3 to this number. + +· 10 bits for the slide position (S). Add 66 to this number. +Range: 26 + 2 = 66 to 26 + 2 + 210 = 1090 +Then print in the slide window the next R bytes that are the same slide +window starting with the S th byte. +After all the maskbyte is read and processed, the following input byte is another +maskbyte. Use the same procedure to finish decompressing the file. Remaining +unused maskbits should be zeros to validate the file. +7 + + This is the modus operandi of the compression algorithm +For each input byte we take a window containing the 1023 previous bytes. +If the window goes out of bounds (ie, when the current input byte is before +position 210 = 1024), we consider it filled with zeros. + +Figure 1: Distribution of the sindow size. +The algorithm works as follows: +While there is unread input data: +Create a maskbyte. +For each bit in the maskbyte (and there is still unread input data): +Compare the following input bytes with the bytes in the window, +and search the longest pattern that is equal to the next bytes. +If we found a pattern of length n > 2: +Assign 0 to the current bit of the maskbyte. +In the next 2 bytes of the output, specify the relative +position and length of the pattern. +Advance output pointer by 2. +Advance input pointer by n. +Else: +Assign 1 to the current bit of the maskbyte. +Copy the current input byte in the next output byte. +Advance output pointer by 1. +Advance input pointer by 1. + +For a better understanding of the algorithm we strongly recommend to read +the PR source files lzg uncompress.c and lzg compress.c that may be located at +https://gforge.lug.fi.uba.ar/plugins/scmcvs/cvsweb.php/PR/src/lib/compression/?cvsroot=freeprince in the PR +repository module. + +3.3 + +Palettes + +Palette resources store a palette for the VGA and patterns for the CGA and +EGA. Each palette resource is sized 100 bytes distributed as explained in Table 25: +Length + +Offset + +Block Name + +8 + + 4 +0 +unknown (TGA?) +48 +4 +vga palette +16 +52 +cga patterns +32 +68 +ega patterns +Table 4: DAT 1.0 Palette blocks + +The vga palette block stores 16 records of three bytes each that is the palette +in the RGB-18-bits format (6 bits for each colour). Each colour is a number +from 0 to 63. Remember to shift the colour bytes by two to get the colour +number from 0 to 256. The format is 00rrrrrr 00gggggg 00bbbbbb where rrrrrr +is the 6 bit red, gggggg the 6 bits green and bbbbbb the 6 bits blue. +In the case of EGA and CGA, palettes are not stored. The palettes are +the standard ones defined by the adapter. In Table 5 the standard palettes are +shown. +EGA CGA1 CGA2 Color name +HTML +0 +0 +0 +black +#000000 +1 +blue +#0000aa +2 +1 +green +#00aa00 +3 +1 +cyan +#00aaaa +4 +2 +red +#aa0000 +5 +2 +magenta +#aa00aa +6 +3 +brown +#aa5500 +7 +3 +light gray +#aaaaaa +8 +dark gray +#555555 +9 +bright blue +#5555ff +10 +bright green +#55ff55 +11 +bright cyan +#55ffff +12 +bright red +#ff5555 +13 +bright magenta #ff55ff +14 +bright yellow +#ffff55 +15 +bright white +#ffffff +Table 5: EGA and CGA palettes + +rgbRGB +000000 +000001 +000010 +000011 +000100 +000101 +010100 +000111 +111000 +111001 +111010 +111011 +111100 +111101 +111110 +111111 + +Where EGA is the only one palette used in EGA mode of the game and +CGA1 and CGA2 are the two palettes used in the CGA mode. As the palettes +are always the same, but the graphics are in 16 colours, some patterns are used +instead of colours. Remember EGA has 16 colours, so is represented in 4 bits +and CGA has 4 simultaneous colours represented in 2 bits. +The cga patterns block stores 16 records of one byte each, separated in four +parts, so the format is a0 a1 b0 b1 c0 c1 d0 d1 where aa is a two bit colour in one of +the two CGA palettes (palette 1 is normally used in the dungeon environment +and 2 in the palace environment). +9 + + The pattern is arranged in a 2x2 box and each pixel is denoted: +a0 a1 +c0 c1 + +b0 b1 +d0 d1 + +So for example if the entry 1 is 00101000 (0x28) in mode CGA2, the pattern +will be a checkerboard of black and green like the following: +Bin +00 01 +01 00 + +Dec +Colour +0 1 black green +1 0 green black + +The ega patterns block stores 16 records of two bytes each, this time separated in two parts. So we have again, four parts per record in the format +aaaabbbb ccccdddd. +Now, using the EGA entries 0-15 (the four bits are represented) the same +patterns as the CGA may be used. +For example, with 00101111 11110010 (0x2ff2) you can create the following +pattern: +Bin +0010 1111 +1111 0010 + +3.4 + +Dec +2 15 +15 2 + +Hex +Colour +2 f brown white +f 2 white brown + +Levels + +This table has a summary of the blocks to be used in this section, you can refer +it from the text below. +Length +Offset +Block Name +720 +0 +pop1 foretable +720 +720 +pop1 backtable +256 +1440 +door I +256 +1696 +door II +96 +1952 +links +64 +2048 +unknown I +3 +2112 +start position +3 +2115 +unknown II +1 +2116 +unknown III +24 +2119 +guard location +24 +2143 +guard direction +24 +2167 +unknown IV (a) +24 +2191 +unknown IV (b) +24 +2215 +guard skill +24 +2239 +unknown IV (c) +24 +2263 +guard colour +16 +2287 +unknown IV (d) +2 +2303 +0F 09 (2319) +Table 6: DAT 1.0 Level blocks +10 + + All levels have a size of 2305, except in the original game, that the potion +level has a size of 2304 (may be it was wrong trimmed). +3.4.1 + +Unknown blocks + +Blocks described in this section are: Unknown from I to IV. +Unknown III and IV blocks does not affect the level if changed, if you find +out what they are used to we will welcome your specification text. +Unknown I may corrupt the level if edited. +We believe unknown II has something to do with the start position, but we +do not know about it. +As unknown II were all zeros for each level in the original set, it was a team +decision to use those bytes for format extension. If one of them is not the default +00 00 00 hex then the level was extended by the team. Those extensions are +only supported by RoomShaker at this moment. To see how those extensions +were defined read the appendix I will write some day. For the moment you may +contact us if you need to know that. +3.4.2 + +Room mapping + +This section explains how the main walls and objects are stored. The blocks +involved here are “pop1 foretable” and “pop1 backtable” +In a level you can store a maximum of 24 rooms (also called screens) of 30 +tiles each, having three stages of 10 tiles each. Screens are numbered from 1 to +24 (not 0 to 23) because the 0 was reserved for special cases. +The pop1 foretable and pop1 backtable blocks have 24 sub-blocks inside. +Those sub-blocks have a size of 30 bytes each and has a room associated. So, +for example, the sub-block staring in 0 corresponds to the room 1 and the +sub-block starting in 690 corresponds to the room 24. It is reserved 1 byte +from the pop1 foretable block and one from the pop1 backtable block for each +tile. To locate the appropriate tile you have to do the following calculation: +tile = (room − 1) ∗ 30 + tileOf f set where tileOffset is a number from 0 to 29 +that means a tile from 0 to 9 if in the upper stage, from 10 to 19 if in the middle +stage and 20 to 29 if in the bottom stage. We define this as the location format +and will be used also in the start position. Always looking from the left to the +right. So there is a pop1 foretable and pop1 backtable byte for each tile in the +level and this is stored this way. +The pop1 foretable part of the tile stores the main tile form according to the +table below. Note that those are just a limited number of tiles, each code has a +tile in the game. The tiles listed are all the ones needed to make a level so the +missing tiles have an equivalent in this list. +Each tile has a code id, as some codes are repeated this is how you have to +calculate the codes. A tile in the pop1 foretable part has 8 bits in this format +rrmccccc, where rr are random bits and can be ignored. m is a modifier of the +tile. For example modified loose floors do not fall down. The rest ccccc is the + +11 + + code of the tile tabled below. Tile names are the same as the ones used by +RoomShaker to keep compatibility. +Hex +0x00 +0x01 +0x02 +0x03 +0x04 +0x05 +0x06 +0x07 +0x08 +0x09 +0x0A +0x0B +0x0C +0x0D +0x0E +0x0F +0x10 +0x11 +0x12 +0x13 +0x14 +0x15 +0x16 +0x17 +0x18 +0x19 +0x1A +0x1B +0x1C +0x1D +0x1E +0x1F + +Binary +Group +Description +00000 +free +Empty +00001 +free +Floor +00010 +spike +Spikes +00011 +none +Pillar +00100 +gate +Gate +00101 +none +Stuck Button +00110 +event +Drop Button +00111 +tapest +Tapestry +01000 +none +Bottom Big-pillar +01001 +none +Top Big-pillar +01010 +potion +Potion +01011 +none +Loose Board +01100 +ttop +Tapestry Top +01101 +none +Mirror +01110 +none +Debris +01111 +event +Raise Button +10000 +none +Exit Left +10001 +none +Exit Right +10010 +chomp +Chopper +10011 +none +Torch +10100 +wall +Wall +10101 +none +Skeleton +10110 +none +Sword +10111 +none +Balcony Left +11000 +none +Balcony Right +11001 +none +Lattice Pillar +11010 +none +Lattice Support +11011 +none +Small Lattice +11100 +none +Lattice Left +11101 +none +Lattice Right +11110 +none +Torch with Debris +11111 +none +Null +Table 7: POP1 Foretable codes + +The pop1 backtable part of the tile stores a modifier or attribute of the +pop1 foretable part of the tile. This works independently of the modifier bit +in the code. The tile modifier depends on the group the tile belongs which are +wall, chomp, event, ttop, potion, tapp, gate, spike and free. The group event +allows the 256 modifiers and will be described in Section 4.2.2. +In the original game all elements are allowed in all levels, but it is possible +to set it up Hex-editing the uncompressed version. To do that, read the Hex +12 + + editing documentation. +Group +none +free +free +free +free +spike +spike +spike +spike +spike +spike +spike +spike +spike +spike +gate +gate +tapest +tapest +tapest +tapest +potion +potion +potion +potion +potion +potion +potion +ttop +ttop +ttop +ttop +ttop +ttop +ttop +ttop +chomp +chomp +chomp +chomp +chomp + +Code +0x00 +0x00 +0x01 +0x03 +0xFF +0x00 +0x01 +0x02 +0x03 +0x04 +0x05 +0x06 +0x07 +0x08 +0x09 +0x00 +0x01 +0x00 +0x01 +0x02 +0x03 +0x00 +0x01 +0x02 +0x03 +0x04 +0x05 +0x06 +0x00 +0x01 +0x02 +0x03 +0x04 +0x05 +0x06 +0x07 +0x00 +0x01 +0x02 +0x03 +0x04 + +Description +This value is used always for this group +Nothing1 , Blue line2 +Spot11 , No blue line2 +Window +Spot31 , Blue line?2 +Normal (allows animation) +Barely Out +Half Out +Fully Out +Fully Out +Out? +Out? +Half Out? +Barely Out? +Disabled +Closed +Open +With Lattice2 +Alternative Design2 +Normal2 +Black2 +Empty +Health point +Life +Feather Fall +Invert +Poison +Open +With lattice2 +Alternative design2 +Normal2 +Black2 +Black2 +With alternative design and bottom2 +With bottom2 +With window2 +Normal +Half Open +Closed +Partially Open +Extra Open + +1 Dungeon environment +2 Dungeon environment + +13 + + Group +chomp + +Code +Description +0x05 +Stuck Open +Table 8: Background modifiers by group + +Note: Some modifiers have not been tested, there +may be any other unknown tile type we have not +still discover. + +3.4.3 + +Wall drawing algorithm + +This section does not have a direct relation with the format because it describes +how the walls must be drawn on the room. However, as this information should +be useful to recreate a cloned room read from the format we decided to include +this section to the document. +Wall drawing depends on what is in the right panel. If the right panel is not +a wall (binary code ends in 10100) a base wall will be drawn and other random +seed will be used. If the right panel is a wall then the main base wall will be +drawn and the described seed will be used. +When the base wall is drawn, the modifiers should be blitted over it. There +are 53 different types of walls using the same base image. We will call the seed +array to the one having the modifier information of those 53 panels. This array +has indexes from 1 to 53 included. +To calculate what value take from the array this calculation must be performed: panelInf o = seedArray[roomN umber + wallP osition] where panelInfo is the result modifier information that will be applied to the base image; +seedArray is this array that will be described above as a table; roomNumber +is the number of the room the wall is (from 1 to 24) and wallPosition is the +position the wall is (from 0 to 29), using the location format specified in section +3.4.2. This means the first value is 1 (roomNumber=1 and wallPosition=0) and +the last is 53 (roomNumber=24 and wallPosition=29). +Modifiers affects the corners of a stone. There are three stone rows per wall. +If the modifier is activated this corner will appear different (seems to be darker). +Another modifier is the grey stone. +Modifier +Grey stone +Left, bottom +Left, top +Right, bottom +Right, up + +Seed Positions +(First row) +2, 5, 14, 17, 26, 32, 35, 50 +2, 11, 36, 45 +37 +27, 33 +4, 10, 31, 37 + +Grey stone +Left, bottom + +(Second row) +none +34, 47 +14 + + Modifier +Left, top +Right, bottom +Right, top + +Seed Positions +9, 10 +2, 8, 25, 35 +6, 12, 23, 29, 39 + +(Third row) +Grey stone +none +Left, bottom +none +Left, top +16 +Right, bottom +none +Right, top +none +Table 9: Stone modifiers on seed position + +Another modifiers are saved in the seed too. Those modifiers are not boolean +values, they are offsets and sizes. As each stone has a different size the stone +separation offset is saved in the seed. +For the first row, the stones are all the same size and the seed is not needed. +For the second row we have got the first 20 values, ordered from 1 to 20. +Field +Values +position +1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 +offsets +543315421 1 5 3 2 1 5 4 3 2 5 4 +separator size +011000110 0 1 1 1 0 0 1 1 1 0 0 +Table 10: First 20 seed values of the second row separator + +We will be adding the next values as soon as we count the pixels ;) This +information can be found in walls.conf file from FreePrince. +3.4.4 + +Room linking + +This section describes the links block. +Each room is linked to another by each of the four sides. Each link is stored. +There is no room mapping, just room linking. +The links block has 24 sub-blocks of 4 bytes each. All those sub-blocks has +its own correspondence with a room (the block starting at 0 is related to the +room 1 and the block starting at with 92 is related to room 24). Each block of +4 bytes stores the links this room links to reserving one byte per each side in +the order left (0), right (1), up (2), down (3). The number 0 is used when there +is no room there. Cross links should be made to allow the kid passing from a +room to another and then coming back to the same room but it is not a must. + +15 + + 3.4.5 + +Guard handling + +This section specifies the blocks: guard location, guard direction, guard skill +and guard colour. +Each guard section has 24 bytes, each byte of them corresponds to a room +so byte 0 is related to room 1 and byte 23 is related to room 24. This room is +where the guard is located. The format only allows one guard per room. Each +block describes a property or attribute of the guard. +The guard location part of a guard describes where in the room the guard +is located, this is a number from 0 to 29 if the guard is in the room or 30 if +there is no guard in this room. Other values are allowed but are equivalent to +30. The number from 0 to 29 is in the location format specified in Section 3.4.2. +The guard direction part describes where the guard looks at. If the value is +0, then the guard looks to the right, if the value is the hex FF (-1 or 255) then +he looks left. This is the direction format, and will be used in the start position +too. +The guard skill is how the guard fights, style and hit points. Note that the +hit points also depends on the level you are. Allowed numbers are from 0 to 9. +The guard colour is the palette the guard has (see Section 3.8). The default +colours are in this table: +Code +Pants +Cape +0x00 +Light +Blue Pink +0x01 +Red +Purple +0x02 +Orange +Yellow +0x03 +Green +Yellow +0x04 +Dark Blue +Beige +0x05 +Purple +Beige +0x06 +Yellow +Orange +Table 11: Default Guard colours + +Other codes may generate random colours because the game is reading the +palette from trashed memory. This may also cause a game crash. It is possible +to add new colours in the guard palette resource (see Section 3.8) avoiding the +crash. +3.4.6 + +Start Position + +This section describes the start position block. +This block stores where and how the kid starts in the level. Note that all +level doors that are on the starting room will be closed in the moment the level +starts. + +16 + + This block has 3 bytes. The first byte is the room, allowed values are from +1 to 24. The second byte is the location, see the section 3.4.2 for the location +format specifications. The third byte is the direction, see 3.4.5 for the direction +format specifications. +3.4.7 + +Door events + +This section explains how the doors are handled and specifies the blocks door I +and II. +First of all he have to define what an event line is in this file. An event line +is a link to a door that will be activated. If the event was triggered with the +action close, then the event will close the door, if the action was open then the +event will open the door. An event line has also a flag to trigger the next event +line or not. An event is defined as a list of event lines, from the first to the +last. The last must have the trigger-next-event-line flag off. This is like a list of +doors that performs an action. An event performs the action that it was called +to do: open those doors or close them. This action is defined by the type of tile +pressed. Each event line has an ID from 0 to 255. An event has the ID of the +first event line in it. +In section 3.4.2 it is explained how a door trigger is associated to an event +ID. Those are the tiles that starts the event depending on what are them: closers +or openers. +How events are stored: Each door block has 256 bytes, one per event line. +Each event line is located in an offset that is the event line ID, so event line 30 +is located in the byte 30 of each block. There is a door I part of an event line +and a door II part of it. We will define them as byte I and byte II. You can find +there: the door room, the door location, and the trigger-next flag. The format +is the following: +Let’s define Screen S = s1 s2 s3 s4 s5 as a 5-bit-number from 1 to 24 (5 bits) +where sn is the bit n of the binary representation and whose value represents +the screen number; Location L = l1 l2 l3 l4 l5 as another 5-bit-number from 0 to +29 where ln is the bit n of the binary representation whose value is according +to the location format specifications (See Section 3.4.5) ; Trigger-next T = t1 +as one bit having 1 for “off” or a 0 for “on”. +Byte I has the form: t1 s4 s5 l1 l2 l3 l4 l5 +Byte II has the form: s1 s2 s3 00000 + +3.5 + +Digital Waves + +Read them as raw digital wave sound using the following specifications: +Field +Size of Format +Format + +Value +16 +PCM + +17 + + Field Value +Attributes 8 bit, mono, unsigned +Channels 1 +Sample rate 11025 +Bytes/Second 11025 +Block Align 1 +Table 12: Wave Specifications + +GNU/Linux users can play the raw waves just dropping them inside /dev/dsp +As DAT headers are very small it is valid to type in a shell console with dsp +write access: cat digisnd?.dat>/dev/dsp to play the whole wave files. + +3.6 + +Midi music + +Standard MIDI files. There have been reports that some versions of MIDI did +not work, but we believe this can bi fixed saving in other MIDI format variant +(like type 0). + +3.7 + +Internal PC Speaker +Offset +Size +Type Name Description +0 +3 +↓ Header The file header +0 +2 +UC Junk 0x00 (or 0x80 sometimes) +2 +1 +US +bps +Beats per two seconds +0 +3numberOf N otes ↓ +Body The file body +3i +2 +US +F req frequency in hertz (0 if no +sound, 1 or 2 if marker) +3i + 2 +1 +US length length in beats +0 +2 +↓ +F ooter The file footer +0 +2 +Junk 0x12 0x00 +Table 13: DAT file blocks + +3.8 + +Binary files + +Some binary files contains relevant information The resource number 10 in +prince.dat has the VGA guard palettes in it saving n records of a 16-colourpalette of 3 bytes in the specified palette format. + +3.9 + +Levels in POP1 for Mac + +In the case of Mac, executable and resource data are embedded in the one runtime file. Level data is a part of resources, for examples graphics, icons and +sounds. Level blocks are very similar to PC but not exactly identical. Following +18 + + table has a summary of the blocks of DAT 1.0 for Mac. +Length +Offset +Block Name +720 +0 +pop1 foretable +720 +720 +pop1 backtable +256 +1440 +door I +256 +1696 +door II +96 +1952 +links +64 +2048 +unknown I +3 +2112 +start position +21 +2115 +unknown II+III +24 +2136 +guard location +24 +2160 +guard direction +24 +2184 +unknown IV (a) +24 +2208 +unknown IV (b) +24 +2232 +guard skill +24 +2256 +unknown IV (c) +24 +2280 +guard colour +4 +2304 +unknown IV (d) +Table 14: DAT 1.0 Level blocks for Mac + +All levels have a size of 2308. Also there are two different things in comparison with DAT 1.0 for PC. DAT 1.0 for Mac does not have any index and +checksums. 16 levels including demo and potion level are only chained in sequence. See 3.4 for reference on each block. + +4 + +DAT v2.0 Format Specifications + +4.1 + +General file specifications + +POP2 DAT files are not much different from their POP1 predecessors. The +format is similar in almost each way. The main difference is in the index. As +DAT v1.0 used an index in the high data, the DAT v2.0 indexes are two level +encapsulated inside a high data. So there is an index of indexes. +We will use the same conventions than in the prior chapter. The checksum +validations are still the same. +Offset Size Type +Name +Description +0 +6 +↓ +Header +The file header +0 +4 +UL HighDataOf f set The location where the +highData begins +4 +2 +US +HighDataSize the number of bytes the +highData has +Table 15: High data structures + +19 + + This is similar to DAT v1.0 format, except that the index area is now called +high data. +The high data part of the file contains multiple encapsulated indexes. Each +of those index is indexed in a high data index of indexes. We will call this index +the master index and the sub index the slave indexes. Slave indexes are the real +file contents index. +4.1.1 + +The master index + +Offset + +Size + +Type + +Name + +0 + +4 + +UL + +IndexOf f set + +4 + +2 + +US + +IndexSize1 + +IndexOf f set = α + +IndexSize + +↓ + +F ooter + +α + +2 + +US + +N umberOf Items + +α + 2 = β2 + +N umberOf Items ∗ 8 + +- + +Index + +β + 8i = γ + +8 + +↓ + +Entry + +γ+0 + +2 + +US + +Id(i)3 + +γ+2 + +4 + +UL + +Of f set(i)3 + +γ+6 + +2 + +US + +Size(i)3 + +Description +The location where the index begins +The number of bytes the index has +The DAT index +Resources count +A list of NumberOfItems +blocks of 8-bytes-length index record called Entry +The 8-bytes-length index +record (one per item) +Item ID +Absolute offset where the resource start +Size of the item not including the checksum byte + +Table 16: DAT 2.0 Master index +Note: POP1 doesn’t validate the DAT file checking IndexOf f set + +IndexSize = F ileSize, this means you can append data at the end of the +file. +The master index is made with: - Offset HighDataOf f set, size 2, type US: +NumberOfSlaveIndexes (the number of the high data sections) - Offset HighDataOf f set+ +2, size N umberOf SlaveIndexes ∗ 6: The master index record (a list of NumberOfSlaveIndexes blocks of 6-bytes-length index record each corresponding to +one slave index) +The 6-bytes-length index record (one per item): Size = 6 bytes - Relative offset 0, size 4, type sting: 4 ASCII bytes string denoting the Slave Index ID. The character order is inverted. - Relative offset 4, size 2, type US: +SlaveIndexOf f set (slave index offset relative to HighDataOffset) +1 IndexOf f set + IndexSize = f ilesize +2 so IndexSize = 8 ∗ numberOf Items + 2 +3 with 0 ≤ i < numberOf Items + +20 + + From the end of the DAT High Data Master Index to the end of the file +we will find the High Data section contents (where the HighDataOffset relative +offsets points to). This content has a set of second indexes each called Slave +Index. So, to find a Slave Index offset you have to add the file HighDataOffset +to the SlaveIndexOffset for the desired index. +There are different 4-byte-length ASCII strings called Slave Index IDs. When +the string is less than 4 bytes, a tailing byte 0x00 is used. We will denote it +with the null-space symbol “ ”. The character order is inverted, so for example +the text SLAP becomes PALS, MARF becomes FRAM, +becomes empty or +RCS becomes SCR. They must be in upper case. +ID +“cust” +“font” +“fram” +“palc” +“pals” +“palt” +“piec” +“psl” +“scr” +“shap” +“shpl” +“strl” +“snd” +“seqs” +“txt4” +“” + +4.1.2 + +Stored +TSUC +TNOF +MARF +CLAP +SLAP +TLAP +CEIP +LSP +RCS +PAHS +LPHS +LRTS +DNS +SQES +4TXT + +Description +Custom +Fonts +Frames +CGA Palette +SVGA Palette +TGA Palette +Pieces +? +Screens (images that have the full room) +Shapes (normal graphics) +Shape palettes +Text +Sound +Midi sequences +Text +Levels +Table 17: Slave Index ID strings + +The slave indexes + +All encapsulated sections pointed by the Master Index are Slave Indexes. The +slave index is specified with: - Offset SlaveIndexOffset, size 2, type US: NumberOfItems (the number of the records referring to the file data) - Offset SlaveIndexOffset+2, size NumberOfItems*11: The slave index record (a list of NumberOfItems blocks of 11-byte-length index record each corresponding to one +slave index) +The 11-byte-length slave index record (one per item): Size = 11 bytes Relative offset 0, size 2, type US: Item ID - Relative offset 2, size 4, type UL: +Resource start (absolute offset in file) - Relative offset 6, size 2, type US: Size +of the item (not including the checksum byte) - Relative offset 8, size 3, type +binary: A flags mask (in “shap” indexes it is always 0x40 0x00 0x00; in others +0x00 0x00 0x00) +Finally, we can locate whatever item we want if we have the - Slave Index +21 + + ID - Item ID this is not a unique key, unfortunately we have found repeated +pairs of IDs for different items, so we have to use the “order” as a third key. +So, the way to find an item is: first read the High Data Offset, go there, read +the number of slave items, iterate the master index to find the desired slave item +comparing it with the Slave Index ID. Then go to the found Slave Index offset +(remember to add the High Data Offset) and read the number of items for this +index. Finally iterate the slave index to find the desired item comparing the +Item ID and read the offset and size. Now you will have the offset of the desired +item’s checksum, increasing the offset by one will give you the beginning of the +item content. + +4.2 + +Levels + +This table has a summary of the blocks to be used in this section. +Length +960 +3840 +1280 +128 +32 +3 +4 +3712 +1088 +978 + +Offset +0 +960 +4800 +6080 +6208 +6240 +6243 +6247 +9959 +11047 + +Block Name +pop2 foretable +pop2 backtable +pop2 doors +links (as explained in Section 3.4.4 but having 32 rooms) +unknown I +start position (as explained in Section 3.4.6) +unknown II (00 01 00 02) (check pop1) +pop2 static guard +pop2 dynamic guard +unknown III +Table 18: DAT 2.0 Level blocks + +All levels have a size of 12025. +4.2.1 + +Room mapping + +You should read section 3.4.2 before reading this one. A POP2 level can store +a maximum of 32 rooms of 30 tiles each, having three stages of 10 tiles each. +Rooms are numbered from 1 to 32 (not 0 to 31) because the 0 is be reserved to +the null-room. +The pop2 foretable block has 32 sub-blocks inside. Each sub-block has a size +of 30 bytes and has a room associated. For each byte in this room there is a +tile in the game. Each byte has a code to represent a tile. There are additional +attributes to this tile also. +To locate the 7th tile in the bottom floor of the room 27 you have to do the +same calculation as in 3.4.2: tile = (room − 1) ∗ 30 + tileOf f set = (27 − 1) ∗ +30 + 2 ∗ 10 + 7 = 807 +Dec Hex Bin +Caverns +00 0x00 000000 Empty + +Ruins +Empty +22 + +Temple +Empty + + Dec Hex Bin +Caverns +Ruins +Temple +01 0x01 000001 Floor +Floor +Floor +02 0x02 000010 Spikes +(?) +Spikes +03 0x03 000011 Pillar +Pillar +Pillar +04 0x04 000100 Door +Gate +Gate +05 0x05 000101 (?) +Raised Button +Raised Button +06 0x06 000110 (?) +Drop Button +Drop Button +07 0x07 000111 (?) +Tunnel +(?) +08 0x08 001000 Bottom Big Pillar +Bottom Big Pillar Bottom Big Pillar +09 0x09 001001 Top Big Pillar +Top Big Pillar +Top Big Pillar +10 0x0A 001010 Potion +Potion +Potion +11 0x0B 001011 Loose Floor +Loose Floor +Loose Floor +12 0x0C 001100 (?) +Slicer Left Half +Slicer Left Half +13 0x0D 001101 (?) +Slicer Right Half Slicer Right Half +14 0x0E 001110 Debris +Debris +Debris +15 0x0F 001111 (?) +Drop Floor +(?) +16 0x10 010000 Exit Half Left +Exit Half Left +Exit Half Left +17 0x11 010001 Exit Half Right +Exit Half Right Exit Half Right +18 0x12 010010 Magic Carpet +(?) +(?) +19 0x13 010011 Torch +(?) +Torch +20 0x14 010100 Wall +Wall +Wall +21 0x15 010101 (?) +Skeleton +(?) +22 0x16 010110 (?) +Sword +(?) +23 0x17 010111 Lava Pit Left +(?) +(?) +24 0x18 011000 Lava Pit Right +(?) +(?) +25 0x19 011001 (?) +(?) +Squash Wall +26 0x1A 011010 (?) +(?) +Flip Tile +27 0x1B 011011 (?) +(?) +(?) +28 0x1C 011100 (?) +(?) +(?) +29 0x1D 011101 (?) +(?) +(?) +30 0x1E 011110 (?) +(?) +(?) +31 0x1F 011111 (?) +(?) +(?) +32 0x20 100000 Torch w/Debris +(?) +Torch w/Debris +33 0x21 100001 Exit Door Top Left (?) +(?) +34 0x22 100010 Pressure Plate +(?) +(?) +35 0x23 100011 Exit Door Top Right (?) +(?) +36 0x24 100100 Dart Gun +(?) +(?) +37 0x25 100101 (?) +(?) +(?) +38 0x26 100110 (?) +(?) +(?) +39 0x27 100111 (?) +(?) +(?) +40 0x28 101000 (?) +(?) +(?) +41 0x29 101001 (?) +(?) +(?) +42 0x2A 101010 (?) +(?) +(?) +43 0x2B 101011 (?) +(?) +Blue Flame +44 0x2C 101100 Rope Bridge +(?) +(?) +45 0x2D 101101 (?) +(?) +(?) +23 + + Dec Hex Bin +Caverns +Ruins +46 0x2E 101110 (?) +(?) +47 0x2F 101111 (?) +(?) +Table 19: POP2 Foretable Codes + +Temple +(?) +(?) + +The pop2 backtable is an expansion if the pop1 backtable and it is sized 4 +times bigger. For each tile there are 4 additional bytes in the pop2 backtable +tiles +block to specify further actions or attributes. This block is sized 4 bytes +tile ∗10 f loor ∗ +loors +∗ 32rooms that is 3840bytes. We call background mask to each block of +3 froom +4 bytes associated to a tile. To locate a background mask you have to do the +following operation: 960 + (room − 1) ∗ 30 ∗ 4 + tileOf f set ∗ 4 Background masks +are stored consecutively each after another until the 960 tiles are specified. +The first byte is an unsigned char (UC) association to one of the 256 door +event registers (see Section 4.2.2) if the tile is an activator. In any other case +this byte is an extra attribute information byte. For example in wall (0x14) +having this byte in 0x04 means the wall is curved. +The second byte in a background mask is the attribute byte. For example +0x18 modifies the tile 0x01 and adds two small stalactites. +We believe the special images uses the 3rd or 4th byte. + +4.2.2 + +Door events + +This section explains how doors are handled and specifies the block pop2 door. +The pop2 door block has 1280 bytes. It is divided in 256 registers of 5 bytes +called door events. Like POP1 events have associations to doors and activate +them. In POP2 events can also activate a floor shooter. +An event is triggered when an activator button (0x22) is pressed. As it is +specified in the Section 4.2.1, the first byte of the attribute mask belonging to a +button tile points it to a door event that is triggered when the button is pressed. +There is a maximum of 256 events because of the unsigned char of the first byte +if the attribute mask in the pop2 backtable block and the 256 registers in the +pop2 door block. +Each event register is of the form “LL SS TT FD FD” which activates the +normal door (0x04), right exit door (0x11) or shooter (0x24) located in the tile +LL of the screen SS. TT is 00 for normal activation and FF for exit doors. +4.2.3 + +Guard handling + +This section explains how guards are handled. In POP2 there are two different +types of guards. We will call them static and dynamic guards. Static guards are +the normal guards that are waiting in a room like in POP1. In the other hand, +dynamic guards are the ones who appear in the room running from one of the +sides. Each type of guard is attached to a room and is handled in a different +way, so a room can have both types of guards, one or none depending on the +specifications. There is a block for each type of guard, pop2 static guard is the +24 + + specification of the static guards and pop2 dynamic guard is the specification +of the dynamic ones. Each block has different specifications and sizes as it is +mentioned bellow. +4.2.4 + +Static guards + +In this item static guards are explained and the pop2 static guard is specified. +For each screen there is reserved memory space for a maximum of 5 guards. +The pop2 static guard block has a size of 3712 divided in 32 sub-blocks of +116 bytes each. As there is a correspondence between each sub-block and the +room with this number, we will call them “room guard blocks”. +Offset Size Type +Name +Description +0 +6 +↓ +Header +The file header +0 +4 +UL HighDataOf f set The location where the +highData begins +4 +2 +US +HighDataSize the number of bytes the +highData has +Table 20: High data structures +A room guard block has a size of 116 divided this way: +⋄ 1 byte for the number of guards present in this room. This byte may take +values from 0 to 5. +⋄ 5 block divisions of 23 bytes for each guard. The first divisions have the +guard information, if the number is less than 5, then the latest divisions +corresponding to the missing guards will be filled with zeros or garbage. +If there is a static guard corresponding to this division of 23 bytes, the +following bytes from 0 to 22 will specify the guard: +Byte 0 is a UC showing the location in this room (same as explained in +3.4.5) of the current guard. Bytes 1 and 2 are a SS with an offset in pixels +to reallocate the guard in the floor. Byte 3 is the facing direction as specified +in 3.4.5. Byte 4 is the skill Byte 5 is unknown Bytes 6,7 and 8 are always 0, +probably because 5 is a long from 0 to 255. Byte 9 is the guard colour in levels +where it is needed (0-3), ie. 1 white+blue, 2 white+black, 3 red. Byte 10 is the +guard number (0 for the first one, 1 for the second, etc). Bytes 11,12,13 and 14 +are unknown, mostly 0, but in 10 guards it is 0x52756e2d. Byte 15 is the type +(0-8 and 84), but does not apply in all levels, ie. 5 head, 8 snake. Byte 16 is +the hit points of the guard (0 to 8). Bytes 17 and 18 are the activate triggers +for skeletons, byte 17 is (0,1,-1) and 18 is (0,-1). Normal value is 0x0000 for a +sleeping skeleton. When set to -1 (0xffff) a trigger will be waiting to wake the +skeleton up, for example the exit door open. Other possible values are 0x0100 +that is the skeleton already awake and 0xff00 that seems to be similar than +0x0000. Bytes 19,20,21 are always 0. Byte 22 is unknown (mostly 0, but 1 and +3 where found for some guards). +25 + + 4.2.5 + +Dynamic guards + +The dynamic guards are the ones who appear running throw a room’s corner +and they are defined in the pop2 dynamic guard block. This block has 34 bytes +for each of the 32 rooms, so it is sized 1088 bytes. Each room has a specification +about those guards. There is only one different type of dynamic guard per room, +but it is possible to set the number of guards that will appear running. +Offset Size Type +Name +Description +0 +6 +↓ +Header +The file header +0 +4 +UL HighDataOf f set The location where the +highData begins +4 +2 +US +HighDataSize the number of bytes the +highData has +Table 21: High data structures + +A room guard block has a size of 116 divided this way: +The bytes are from 0 to 33: Bytes from 8 to 17 may take the value 0x5a and +0. Bytes 8,9,10,15,16 are always 0. Byte 18 activates dynamic guard. 1 is true +and 0 is false. Byte 19 is the skill (0-7, 8 to make it passive) Bytes 20, 21 and +22 are always 0. Byte 23 is the dynamic guard mode: 0 or 1 to make the guard +wait until all guards are dead to spawn and 2 to spawn even when the prior +guard is still alive. Byte 24 is the floor the guard will appear on. 0 is the upper +one and 2 is the lower. Another number will kill the guard playing the sound. +Byte 25 is the initial location of the guard. ie. 0 is the left side of the screen, 9 +is the right side, it is possible to locate them in the middle of the screen, they +will magically spawn in a position between 1 and 8. As they will run to the +center of the screen, this byte also sets the facing direction up (0 to 5 is right, 6 +to 9 is left). Byte 26 is the time in ticks the first guard will wait to spawn. Byte +27 is the time in ticks between guards spawn. Bytes 28 and 29 are unknown. +Byte 30 is the number of guards that will appear, there is a maximum of 5 per +room, including static guards. Byte 31 is the hit points the guards will have. + +5 + +PLV v1.0 Format Specifications + +PLV v1.0 files are defined in this table: +Size +7 +1 +1 +1 +4 +4 +B1 + +Offset +0 +7 +8 +9 +10 +14 +18 + +Description +Magic identifier +POP version +PLV version +Level Number +Number of fields +Block 1: Level size (B1 ) +Block 1: Level code +26 + +Type +Content +text “POP LVL” +UC +0x01 +UC +0x01 +UC +UL +UL 2306/2305 +- + + Size Offset Description +Type +4 18 + B1 Block 2: User data size (B2 ) UL +B2 22 + B1 Block 2: User data +Table 22: PLV blocks + +Content + +Level code is the exact level as described in Section 3.4 including the checksum byte. Note that Level size (B1 ) also includes the checksum byte in the +count. POP version is 1 for POP1 and 2 for POP2. PLV version is 1 for PLV +v1.0. Only one level may be saved in a PLV, the level number is saved inside. + +5.1 + +User data + +User data is a block of extensible information, Number of fields is the count of +each field/value information pair. A pair is saved in the following format: +field\_name\\0value\\0 +where \0 is the null byte (0x00) and field name and value are strings. +There are mandatory pairs that must be included in all PLV files. Those +are: +Field name +Description +Editor Name +The name of the editor used to save the file +Editor Version +The version of the editor used to save the file +Level Author +The author of the file +Level Title +A title for the level +Level Description +A description +Time Created +The time when the file was created +Time Last Modified +The time of the last modification to the file +Original Filename +The name of the original file name (levels.dat) +Original Level Number Optional. The level number it has when it was +first exported +Table 23: PLV Mandatory Fields + +The content values may be empty. There is no need to keep an order within +the fields. + +5.2 + +Allowed Date format + +To make easy time parsing the time format must be very strict. There are only +two allowed formats: with seconds and without. With seconds the format is +“YYYY-MM-DD HH:II:SS” Without seconds the format is “YYYY-MM-DD +HH:II” Where YYYY is the year in 4 digits, MM is the month in numbers, MM +the months, DD the days, HH the hour, II the minute and SS the second in the +military time: HH is a number from 00 to 23. +27 + + If the month, day, hour or second have only one digit, the other digit must +be completed with 0. +i.e. 2002-11-26 22:16:39 + +6 + +SAV v1.0 Format Specifications + +SAV v1.0 saves kid level, hit points and remaining time information in order to +restart the game from this position. +SAV files are 8 bytes length in the following format: +Offset Size Type Name Description +0 +2 +US +i +Remaining minutes +2 +2 +US +ii +Remaining ticks +4 +2 +US +iii Current level +6 +2 +US +iv Current hit points +Table 24: SAV blocks + +Remaining minutes (i) Range values: 0 to 32766 for minutes 32767 to 65534 +for NO TIME (but the time is stored) 65535 for game over +1 +Remaining ticks (ii) Seconds are stored in ticks, a tick is 12 +seconds. To get +the time in seconds you have to divide the integer “Remaining ticks” by 12. +Range values: 0.000 to 59.916 seconds (rounded by units of ≈ 83ms or +1 +seconds) +0 to 719 ticks +12 +Level (iii) Range values: 1 to 12 for normal levels 13 for 12bis 14 for princess +level 15 for potion level +Hit points (iv) Range values: 0 for an immediate death 1 to 65535 hit points + +7 + +HOF v1.0 Format Specifications + +HOF files are used to save the Hall of Fame information. +All HOF v1.0 files have a size of 176 bytes. The first 2 bytes belongs to the +record count. The format is US. The maximum number of records allowed is +6, so the second byte is always 0x00. Following those bytes there is an array +of records. This array has a full size of 29 bytes distributed according to the +following table. +Offset Size Type Name Description +0 +25 text +Player name +25 +2 US 1 +Remaining minutes +27 +2 US 1 +Remaining ticks +Table 25: HOF blocks +1 similar to SAV format + +28 + + In case there is no record, the 29 bytes spaces must be filled with zeros in +order to complete the whole file and give it the size of 2 + 29 ∗ 6 = 176. + +8 + +Credits + +This document +Writing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Enrique Calot +Corrections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Patrik Jakobsson +Hubai Tamas +Reverse Engineering +Indexes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Enrique Calot +Levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Enrique Calot +Brendon James +MAC Levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dongsoo Lim +Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tammo Jan Dijkema +RLE Compression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tammo Jan Dijkema +LZG Compression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anke Balderer +Diego Essaya +Sounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Christian Lundheim +Palettes and Speaker Sounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . David +PLV v1.0 +Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Brendon James +Enrique Calot + +9 + +License + +c 2004 – 2008 The Princed Project Team Permission is granted to +Copyright ° +copy, distribute and/or modify this document under the terms of the GNU Free +Documentation License, Version 1.2 or any later version published by the Free +Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no +Back-Cover Texts. + +Indexes +List of Tables +1 +2 +3 +4 + +DAT file blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . +Image headers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +Algorithm codes . . . . . . . . . . . . . . . . . . . . . . . . . . . +DAT 1.0 Palette blocks . . . . . . . . . . . . . . . . . . . . . . . + +29 + +5 +6 +6 +9 + + 5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 + +EGA and CGA palettes . . . . . . . . . . . . . . . . . . . . . . . +DAT 1.0 Level blocks . . . . . . . . . . . . . . . . . . . . . . . . . +POP1 Foretable codes . . . . . . . . . . . . . . . . . . . . . . . . +Background modifiers by group . . . . . . . . . . . . . . . . . . . +Stone modifiers on seed position . . . . . . . . . . . . . . . . . . +First 20 seed values of the second row separator . . . . . . . . . . +Default Guard colours . . . . . . . . . . . . . . . . . . . . . . . . +Wave Specifications . . . . . . . . . . . . . . . . . . . . . . . . . . +DAT file blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . +DAT 1.0 Level blocks for Mac . . . . . . . . . . . . . . . . . . . . +High data structures . . . . . . . . . . . . . . . . . . . . . . . . . +DAT 2.0 Master index . . . . . . . . . . . . . . . . . . . . . . . . +Slave Index ID strings . . . . . . . . . . . . . . . . . . . . . . . . +DAT 2.0 Level blocks . . . . . . . . . . . . . . . . . . . . . . . . . +POP2 Foretable Codes . . . . . . . . . . . . . . . . . . . . . . . . +High data structures . . . . . . . . . . . . . . . . . . . . . . . . . +High data structures . . . . . . . . . . . . . . . . . . . . . . . . . +PLV blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +PLV Mandatory Fields . . . . . . . . . . . . . . . . . . . . . . . . +SAV blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +HOF blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + +9 +10 +12 +14 +15 +15 +16 +18 +18 +19 +19 +20 +21 +22 +24 +25 +26 +27 +27 +28 +28 + +List of Figures +1 + +Distribution of the sindow size. . . . . . . . . . . . . . . . . . . . + +30 + +8 + + \ No newline at end of file diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index c645b33..f5d8381 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -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, разобрав, как устроены ресурсы игры в двух доступных нам версиях: diff --git a/applications/PoP/poc/res/bg/pop_env2.atl b/applications/PoP/poc/res/bg/pop_env2.atl index c204ff5..45d4520 100644 Binary files a/applications/PoP/poc/res/bg/pop_env2.atl and b/applications/PoP/poc/res/bg/pop_env2.atl differ diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md new file mode 100644 index 0000000..f8d3cbb --- /dev/null +++ b/applications/PoP/roomtest/CLAUDE.md @@ -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`) поверх ``. | +| `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`. diff --git a/applications/PoP/roomtest/README.md b/applications/PoP/roomtest/README.md new file mode 100644 index 0000000..2ed0588 --- /dev/null +++ b/applications/PoP/roomtest/README.md @@ -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`. diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index ee57edd..8034f5d 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -57,10 +57,13 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); if (w && h) - gfx_blit(x, ybottom - (int)h + 1, img); + gfx_blit(x, ybottom - (int)h + 1 + POP_YOFF, img); /* +YOFF: центрирование */ gfx_w0_unmap(); } +/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ +static void heal_off(int x, int y, int w, int h) { gfx_heal(x, y + POP_YOFF, w, h); } + static void env_b(uint8_t id, int x, int yb) { blit_b(&env[id >> 5], (uint8_t)(id & 31), x, yb); } static void wall_b(uint8_t id, int x, int yb) { blit_b(&wall_at, id, x, yb); } static void fore_b(uint8_t id, int x, int yb) { blit_b(&fore_at, id, x, yb); } @@ -129,8 +132,15 @@ static const uint8_t RPOS[4] = {52, 42, 31, 21}; /* ---- Текущая комната (globals для tile_code/mod) ------------------- */ static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg; +static const uint8_t *g_below; /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */ static uint8_t g_room; +/* Задать ряд 0 комнаты снизу (fg-коды) — нужен для draw_tile_topright/ + * floorright, когда тайл нижнего ряда стал видимым (empty после падения + * loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты + * снизу (кромка уровня = стена). */ +void pop_room_set_below(const uint8_t *below_row0_fg) { g_below = below_row0_fg; } + static uint8_t tile_code(int row, int col) { if (col < 0) { @@ -287,6 +297,24 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) env_b(DOOR_FRAM_SLICE[gate_frame], x, yb); } +/* ---- Loose floors: кадры дрожащей плиты (seg008 loose_fram_*) ------- * + * Состояние pop_loose_modif[] живёт в pop_map. draw_tile рисует грани + * loose АНИМИРОВАННО по modif (как SDLPoP: сам тайл — лево/низ, ПРАВЫЙ + * СОСЕД — правую грань через draw_tile_anim_right). При modif==0 (покой/ + * статич. запекание) get_loose_frame=0 → базовые кадры 41/42/43. */ +extern uint8_t pop_loose_modif[30]; + +static const uint8_t LOOSE_FRAM_LEFT[12] = {41,69,41,70,70,41,41,41,70,70,70,0}; +static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0}; +static const uint8_t LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0}; + +/* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору. */ +static uint8_t get_loose_frame(uint8_t m) +{ + if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; } + return m; +} + /* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */ static void draw_tile(int row, int col) { @@ -302,9 +330,14 @@ static void draw_tile(int row, int col) uint8_t rbl_code; uint8_t wmod; - /* тайл снизу-слева (для topright) */ - if (row + 1 > 2) rbl_code = 1; /* ниже нижнего ряда = пол */ - else rbl_code = tile_code(row + 1, col - 1); + /* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0 + * комнаты снизу (load_rowbelow, seg008:368): row_below_left_[col] = + * room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */ + if (row + 1 > 2) { + int lc = col - 1; + if (lc < 0) rbl_code = 20; /* кромка уровня = стена */ + else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1; + } else rbl_code = tile_code(row + 1, col - 1); /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */ if (code == 0 || code == 9 || code == 12 || code == 26) { @@ -337,8 +370,8 @@ static void draw_tile(int row, int col) /* --- draw_tile_anim_right (seg008.c:530) — статичные части --- */ if (lcode == 4) { /* gate: решётка */ draw_gate_back(lmod, xh, dby, dmy); - } else if (lcode == 11) { /* loose: правый край */ - env_b(42, x, dby - 1); + } else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ + env_b(LOOSE_FRAM_RIGHT[get_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); } else if (lcode == 2) { /* spike: статич. кадр */ uint8_t sf = (lmod & 0x80) ? 5 : lmod; if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) @@ -354,14 +387,15 @@ static void draw_tile(int row, int col) env_b(t->bottom_id, x, dby); } - /* --- draw_loose (seg008.c:600) — статич. кадр --- */ + /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif) --- */ if (code == 11) - env_b(43, x, dby); /* loose_fram_bottom[0] */ + env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_loose_modif[row * 10 + col])], x, dby); /* --- draw_tile_base (seg008.c:611) --- */ { uint8_t base_id = t->base_id; - if (code == 11) base_id = 41; /* loose_fram_left[0] */ + if (code == 11) /* loose: левая грань (анимир.) */ + base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])]; if (base_id) env_b(base_id, x, t->base_y + dmy); } @@ -376,6 +410,149 @@ static void draw_tile(int row, int col) fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); } +/* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- * + * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать + * ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif + * внутри draw_tile). */ +void pop_loose_shake_draw(int row, int col) +{ + int x = COL_XH[col] * 8; + /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх — + * перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из + * pop_loose_modif). Порт redraw_tile_height (loose + правый сосед). + * NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое + * решение (bake тайла как empty + рисовать loose всегда динамически) + * отложено; wipe-в-чёрный делал плиту невидимой — не годится. */ + heal_off(x, 63 * row + 46, 64, 24); + gfx_set_bank(GFX_BANK_SPRITE); + draw_tile(row, col); /* loose: лево+низ (анимир.) */ + if (col + 1 < 10) + draw_tile(row, col + 1); /* сосед: правая грань loose (анимир.) + его пол */ + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт + * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И + * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — + * рисовал loose-правый-край env_b(42); теперь пусто → чистая грань). + * g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые + * пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */ +void pop_loose_bake_empty(int row, int col) +{ + int x = COL_XH[col] * 8; + int xr = COL_XH[col + 1] * 8; + int yb = 63 * row + 46; + /* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора): + * стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе + * heal вернул бы старый пол из ОЗУ-копии. Ширина 40 — кусок пола + * заходит на x+32..+40. */ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(x, yb + POP_YOFF, x + 39, yb + 28 + POP_YOFF); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */ + if (col + 1 < 10) + bar(xr, yb + POP_YOFF, xr + 31, yb + 28 + POP_YOFF); /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */ + /* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты + * снизу) + правый сосед (его левая грань: был loose-край env_b(42), + * теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в + * ОЗУ-копию → heal сохранит. */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + draw_tile(row, col); + if (col + 1 < 10) + draw_tile(row, col + 1); + gfx_set_bank(GFX_BANK_NORMAL); +} + +/* ---- Падающий кусок loose (mob, seg007 move_loose) ------------------ * + * Упрощ. одиночный кусок (в комнате 1 — один loose). Модель fixed- + * corridor: коридор падения (колонка тайла) heal'ится каждый кадр (стирает + * прошлый кусок, возвращает печёный фон — empty+«стена вниз»), поверх — + * кадр 0 loose (41/42/43) на текущей y. Гравитация: speed 0→+3/кадр + * (cap 29), y+=speed. Двоебуфер: коридор фиксирован → heal чистит любой + * прошлый кадр ЭТОЙ страницы. Ниже поля кусок прячет clip_borders. */ +static uint8_t mob_active; /* 0 нет; 1 падает; 2 = очистка второй страницы */ +static int mob_x; /* левый край (COL_XH*8) */ +static int mob_y; /* main-y (dmy покоя), растёт вниз */ +static int8_t mob_speed; +static int8_t mob_row, mob_col; /* тайл-позиция куска (для окклюзии соседа) */ + +void pop_loose_mob_spawn(int row, int col) +{ + mob_x = COL_XH[col] * 8; + mob_y = 63 * row + 62; /* dmy покоя — откуда отрывается кусок */ + mob_row = (int8_t)row; + mob_col = (int8_t)col; + mob_speed = 0; + mob_active = 1; +} + +/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26) + * с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46: + * ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину + * [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной + * странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */ +#define MOB_X0(mx) ((mx) - 4) +#define MOB_W 64 /* mob_x-4 .. mob_x+59 (крышка +57 с запасом 2px) */ + +void pop_loose_mob_tick(void) +{ + if (!mob_active) return; + /* стереть прошлый кусок этой страницы + вернуть печёный фон коридора + * (фикс rect [160,192] по ВСЕЙ ширине куска; overflow прошлого кадра + * стёрт его же clip'ом той же ширины). */ + heal_off(MOB_X0(mob_x), 160, MOB_W, 32); + if (mob_active == 2) { mob_active = 0; return; } /* очистили вторую стр. — всё */ + /* гравитация (move_loose): ускорение до 29 */ + if (mob_speed < 29) mob_speed += 3; + mob_y += mob_speed; + if (mob_y - 24 >= POP_PLAYFIELD_H) { mob_active = 2; return; } /* весь ниже поля → +1 кадр очистки */ + /* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска — + * left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1, + * заходит в тайл СПРАВА 2,7) на obj_y-1. У нас bottom=dby(mob_y+3) → + * left=mob_y, right=mob_y+2 в mob_x+32 (col+1). */ + gfx_set_bank(GFX_BANK_SPRITE); + env_b(43, mob_x, mob_y + 3); + env_b(41, mob_x, mob_y); + env_b(42, mob_x + 32, mob_y + 2); + /* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob: + * set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob). + * Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола, + * её правый край ПРЯЧЕТСЯ за передней гранью пола 2,7. Перерисовываем + * сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу + * до heal'а следующего кадра — виден как хвост во время падения). */ + if (mob_col + 1 < 10) + draw_tile(mob_row, mob_col + 1); + gfx_set_bank(GFX_BANK_NORMAL); + /* клип СВОЕГО выхода за низ поля (в статус-полосу) — по ВСЕЙ ширине куска */ + pop_clip_sprite(MOB_X0(mob_x), MOB_W, mob_y - 23 + POP_YOFF, 30); +} + +/* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл- + * буфера). Узкий per-спрайт heal НЕ используем: accel-выравнивание блока + * оставляет 1–2px слайверы правой грани → на одной странице мерцает. + * Вместо этого борта чистит pop_room_clip_borders (полоса, но ТОЛЬКО в + * кадрах падения — гейт по флагу). */ +static uint8_t border_dirty; + +void pop_clip_sprite(int x, int w, int sy_top, int h) +{ + int sy_bot = sy_top + h; + (void)x; (void)w; + if (sy_bot > POP_CLIP_BOTTOM || sy_top < POP_YOFF) + border_dirty = 2; /* 2 кадра = обе страницы */ +} + +/* Клип бортов (см. pop_bg.h): работает ТОЛЬКО пока border_dirty (кто-то + * вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает + * чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу + * + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */ +void pop_room_clip_borders(void) +{ + if (!border_dirty) return; + border_dirty--; + gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */ + gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */ +} + 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) diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 4ec6ac5..f393322 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -13,6 +13,18 @@ #include +/* ---- Вертикальный 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,11 @@ 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); + /* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него — * звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ * спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики @@ -36,4 +53,29 @@ 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); +/* 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 (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); + +/* Детектор выхода спрайта за поле: если спрайт [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 diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 3c3aff7..a092b57 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -6,10 +6,15 @@ #include #include #include "pop_kid.h" +#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ #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 +113,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++; @@ -186,7 +195,7 @@ 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; } @@ -229,8 +238,11 @@ void kid_draw(void) top = obj_y - (int)h + 1 + KID_YOFF; uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */ gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ - gfx_blit_cols(bx, top, img, flip); + gfx_blit_cols(bx, top + POP_YOFF, img, flip); /* +YOFF: центрирование */ gfx_set_bank(GFX_BANK_NORMAL); + /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) — + * узко, только по прямоугольнику Кида и только при выходе. */ + pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); kid_lx[dp] = bx; kid_ly[dp] = top; kid_lw[dp] = w; kid_lh[dp] = h; kid_lvalid[dp] = 1; kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */ diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index ed0b262..a7615c8 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -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); diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index f64f950..d66030b 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -19,6 +19,7 @@ #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 — перерисовка */ /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ #define TILE_EMPTY 0 @@ -28,6 +29,8 @@ #define TILE_MIRROR 13 #define TILE_CHOMPER 18 #define TILE_WALL 20 +#define TILE_LOOSE 11 /* проваливающийся пол */ +#define TILE_DEBRIS 14 /* щебень (упавший loose) */ #define ACT_STAND 0 #define ACT_RUNJUMP 1 #define ACT_HANGCLIMB 2 @@ -82,12 +85,12 @@ static int8_t g_infrontx; /* колонка «перед персонаже /* ---- Карта комнаты ------------------------------------------------- */ uint8_t pop_fell_out; /* 1 = Kid упал за низ комнаты (сброс приложением) */ -static const uint8_t *g_fg; /* fg[30], row*10+col */ +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 */ -void pop_map_set(const uint8_t *fg) { g_fg = fg; } +void pop_map_set(uint8_t *fg) { g_fg = fg; } static uint8_t get_tile(int8_t col, int8_t row) { @@ -274,18 +277,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(); + } } } @@ -616,6 +627,116 @@ 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 = обе стр.) */ + +/* 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; + if (frame == 79) { /* прыжок вверх — тайл сверху */ + if (get_tile_above_char() == TILE_LOOSE) + make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1); + return; + } + /* стоя́ние: action turn/bumped или < hang_climb (0/1), с FRAME_NEEDS_FLOOR */ + if (action != ACT_BUMPED && action >= ACT_HANGCLIMB) return; + if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return; + if (get_tile_at_char() == TILE_LOOSE) + make_loose_fall(Kid.curr_row * 10 + Kid.curr_col, 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 < 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; + } +} + +/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ + * (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */ +void pop_loose_tick(void) +{ + int pos; + for (pos = 0; pos < 30; pos++) { + if (loose_bake[pos]) { /* упал: запечь пусто (2 кадра=2 стр.) */ + pop_loose_bake_empty(pos / 10, pos % 10); + loose_bake[pos]--; + continue; + } + if (loose_rest[pos]) { /* тряска кончилась: перерисовать ПОКОЙ */ + /* modif уже 0 → shake_draw рисует базовый кадр (покой) обоих тайлов + * (col и col+1: правая грань loose живёт в 2,7). 2 кадра = обе + * страницы дабл-буфера — иначе на одной остаётся застывший + * дрожащий кадр правой грани → мерцание («бесконечная тряска»). */ + pop_loose_shake_draw(pos / 10, pos % 10); + loose_rest[pos]--; + continue; + } + if (pop_loose_modif[pos] == 0) continue; + { + uint8_t m = ++pop_loose_modif[pos]; + if (m & 0x80) { /* тряска (do_knock) */ + if (m >= 0x84) { /* конец тряски: сброс + перерисовать покой */ + pop_loose_modif[pos] = 0; + loose_rest[pos] = 2; /* покой на ОБЕИХ страницах */ + continue; + } + } 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(pos / 10, pos % 10); /* запечь пусто на ТЕКУЩЕЙ странице сразу */ + loose_bake[pos] = 1; /* + вторая страница следующим кадром */ + pop_loose_mob_spawn(pos / 10, pos % 10); /* отрыв падающего куска (L4) */ + continue; + } + pop_loose_shake_draw(pos / 10, pos % 10); /* дрожащий кадр */ + } + } + pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */ +} + void pop_phys_tick(void) { if (grab_timer) grab_timer--; @@ -624,4 +745,6 @@ void pop_phys_tick(void) determine_col(); check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_action(); + check_press(); /* seg006: стойка/пробой loose → make_loose_fall */ + pop_check_knock(); /* seg003: сотрясение приземления → тряска loose в ряду */ } diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 7cc239a..4c4f290 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -14,8 +14,22 @@ #include /* Задать карту текущей комнаты: 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); + +/* 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); + +/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда + * приземления/удара. Зовётся из pop_phys_tick. */ +void pop_check_knock(void); /* Установлен в 1, когда Kid упал ниже комнаты (комнаты снизу нет в этом * тесте). Приложение должно сбросить Kid (kid_init) и снять флаг. */ diff --git a/applications/PoP/roomtest/room1_data.h b/applications/PoP/roomtest/room1_data.h index d0cc8de..2f48c4d 100644 --- a/applications/PoP/roomtest/room1_data.h +++ b/applications/PoP/roomtest/room1_data.h @@ -7,4 +7,7 @@ static const unsigned char room1_fg[30] = {0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x 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}; +/* Ряд 0 комнаты СНИЗУ (room 2, link Dn) — для topright «стены вниз» под + * упавшим loose (2,6): room2(0,5)=0x14 стена. Из res2002.bin row0. */ +static const unsigned char belowrow_fg[10] = {0x14,0x14,0x00,0x00,0x00,0x14,0x01,0x03,0x01,0x03}; #endif diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index a0292b4..831f024 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -24,6 +24,11 @@ int main(void) * по умолчанию (без мерцания); SPACE переключает в однобуфер для * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ uint8_t pg, front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; + /* МУТАБЕЛЬНАЯ копия комнаты — одна на отрисовку (pop_room_draw) И + * коллизию (pop_map): loose-пол при падении меняет тайл на empty, и + * оба модуля должны это видеть. */ + uint8_t room_fg[30]; + for (pg = 0; pg < 30; pg++) room_fg[pg] = room1_fg[pg]; if (pop_bg_load() != 0) { /* атласы — ДО initgraph */ puts("pop_bg_load failed"); @@ -44,17 +49,18 @@ int main(void) * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ * страницы, в которую рисуем. Дальше каждый тик восстанавливаем * ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */ + pop_room_set_below(belowrow_fg); /* ряд снизу (room 2) для «стены вниз» */ 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); + pop_room_draw(1, room_fg, room1_bg, leftcol_fg, leftcol_bg); } gfx_set_draw_page(0); /* K2b: Kid стоит на уступе (row1), управляется с клавиатуры. * ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. */ - pop_map_set(room1_fg); /* карта для коллизии/падения (K3) */ + pop_map_set(room_fg); /* карта для коллизии/падения (K3) */ kid_init(SEQ_STAND, 1, 1, 0); kid_draw(); pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), @@ -102,10 +108,13 @@ int main(void) pop_fell_out = 0; kid_init(SEQ_STAND, 1, 1, 0); } - kid_draw(); /* нарисовать новый кадр */ + pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */ + /* — ДО Kid, чтобы Kid всегда был поверх плиты) */ + kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ 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 ловит начало кадра, diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index f36da90..15cd5ca 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -50,6 +50,12 @@ 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} + ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31 ENV_MASK = (1 << ENV_SHIFT) - 1 @@ -241,7 +247,8 @@ 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 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]