applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн
Порт PoP на Sprinter. Текущий PoC — applications/PoP/roomtest/: комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре и коллизией с картой. - roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип, seqtbl-анимация), pop_ctrl (порт control() PoP на held-state kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены, падение/приземление, отскок seq_47, вертикальный прыжок K4.1). MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge). - toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract — распаковка res-графики MSDOS в атласы + композиция комнат. - toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD), png_strip.py / room_compose.py (ассет-пайплайн). - docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT). - bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип). .gitignore: build-артефакты applications/*/*/*; исключены внешние референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
# make_hdd.sh OUT.chd FILE1 [FILE2 ...]
|
||||
#
|
||||
# Собирает 32 МБ RAW-образ жёсткого диска (MBR + один раздел FAT16) с
|
||||
# указанными файлами в корне и конвертит его в CHD для MAME (монтируется
|
||||
# как -hard2). Быстрая альтернатива floppy (mc.img): MAME эмулирует FDD
|
||||
# с реальной черепашьей скоростью, HDD — в разы быстрее.
|
||||
#
|
||||
# Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME).
|
||||
# Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено).
|
||||
#
|
||||
# Использование:
|
||||
# toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ...
|
||||
set -e
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "usage: $0 OUT.chd FILE1 [FILE2 ...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUT_CHD="$1"; shift
|
||||
|
||||
# Геометрия: 128*16*32*512 = 32 МБ. BPS=512.
|
||||
CYLS=128; HEADS=16; SECS=32
|
||||
SECTORS=$((CYLS * HEADS * SECS)) # 65536
|
||||
|
||||
# macOS mktemp требует X'ы в КОНЦЕ шаблона.
|
||||
RAW=$(mktemp /tmp/sphdd.raw.XXXXXX)
|
||||
MTC=$(mktemp /tmp/sphdd.mtc.XXXXXX)
|
||||
trap 'rm -f "$RAW" "$MTC"' EXIT
|
||||
|
||||
dd if=/dev/zero of="$RAW" bs=512 count=$SECTORS status=none
|
||||
|
||||
# mtools-диск z: = раздел 1 нашего RAW-образа.
|
||||
printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC"
|
||||
export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1
|
||||
|
||||
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16.
|
||||
mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
|
||||
mformat z:
|
||||
|
||||
for f in "$@"; do
|
||||
mcopy -o "$f" z:
|
||||
done
|
||||
|
||||
echo "--- содержимое образа ---"
|
||||
mdir z:
|
||||
|
||||
# RAW -> CHD (без сжатия = быстро; та же геометрия).
|
||||
chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \
|
||||
-chs $CYLS,$HEADS,$SECS -ss 512 -f
|
||||
echo "готово: $OUT_CHD"
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
png_strip.py — генерик-конвертер произвольного набора PNG-кадров (РАЗНОГО
|
||||
размера каждый) в одну raw-ленту + палитру для sprite-движка Sprinter
|
||||
(формат getimage/atlas — см. libbgi/include/sprite.h, toolchain/mkatlas.py).
|
||||
|
||||
Не завязан ни на какой конкретный проект/игру — просто берёт список
|
||||
картинок, которые вы сами укажете, и паковает их в ленту с общим
|
||||
канвасом (кадр движка обязан быть фиксированного размера).
|
||||
|
||||
Использование:
|
||||
png_strip.py OUT.raw OUT.pal img1.png img2.png ... [опции]
|
||||
|
||||
Опции:
|
||||
--canvas WxH размер канваса кадра (по умолчанию — максимум
|
||||
ширины/высоты среди входных картинок)
|
||||
--anchor MODE bottom-center (по умолчанию) | top-left | center
|
||||
--base-index N с какого индекса палитры начинать свои цвета
|
||||
(по умолчанию 16 — под 16 EGA-слотов initgraph)
|
||||
--ega добавить в палитру 16 EGA-цветов BGI перед своими
|
||||
(по умолчанию включено; --no-ega выключает)
|
||||
--seed-pal FILE переиспользовать цвета уже существующей палитры
|
||||
(EGA16 + N своих) — свои индексы этой ленты
|
||||
продолжаются ПОСЛЕ них (не с --base-index), а
|
||||
OUT.pal получится ПОЛНОЙ merged-палитрой
|
||||
(ega+seed+свои), готовой как --seed-pal для
|
||||
следующего вызова. Так несколько независимо
|
||||
собранных лент (напр. разные типы тайлов) можно
|
||||
свести в ОДНУ палитру для одновременного показа
|
||||
на экране (см. applications/PoP/poc/tools/
|
||||
build_room_palette.py).
|
||||
|
||||
Каждая картинка — источник ОДНОГО кадра ленты (не атлас-сетка;
|
||||
для сетки — см. mkatlas.py raw:FWxFH:NXxNY после этого шага, если
|
||||
нужно). Прозрачность — альфа < 128 → индекс 0xFF (сентинел движка).
|
||||
|
||||
Дальше: toolchain/mkatlas.py OUT.atl OUT.raw:CW x CH:1xN
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
EGA = [
|
||||
(0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0),
|
||||
(0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168),
|
||||
(84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84),
|
||||
(84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255),
|
||||
]
|
||||
|
||||
|
||||
def die(msg):
|
||||
sys.exit(f"png_strip: {msg}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(add_help=False)
|
||||
ap.add_argument("out_raw")
|
||||
ap.add_argument("out_pal")
|
||||
ap.add_argument("images", nargs="+")
|
||||
ap.add_argument("--canvas", default=None)
|
||||
ap.add_argument("--anchor", default="bottom-center",
|
||||
choices=["bottom-center", "top-left", "center"])
|
||||
ap.add_argument("--base-index", type=int, default=16)
|
||||
ap.add_argument("--ega", dest="ega", action="store_true", default=True)
|
||||
ap.add_argument("--no-ega", dest="ega", action="store_false")
|
||||
ap.add_argument("--seed-pal", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
imgs = [Image.open(p).convert("RGBA") for p in args.images]
|
||||
|
||||
seed_records = []
|
||||
if args.seed_pal:
|
||||
raw = open(args.seed_pal, "rb").read()
|
||||
if len(raw) % 4:
|
||||
die(f"--seed-pal {args.seed_pal}: длина не кратна 4")
|
||||
n_ega = 16 if args.ega else 0
|
||||
for i in range(n_ega, len(raw) // 4):
|
||||
b, g, r, _ = raw[i * 4:i * 4 + 4]
|
||||
seed_records.append((r, g, b))
|
||||
args.base_index = 16 + len(seed_records)
|
||||
|
||||
if args.canvas:
|
||||
cw, ch = (int(v) for v in args.canvas.lower().split("x"))
|
||||
else:
|
||||
cw = max(im.width for im in imgs)
|
||||
ch = max(im.height for im in imgs)
|
||||
if cw > 64 or ch > 64:
|
||||
die(f"канвас {cw}x{ch} превышает лимит движка 64x64")
|
||||
for p, im in zip(args.images, imgs):
|
||||
if im.width > cw or im.height > ch:
|
||||
die(f"{p}: {im.width}x{im.height} не влезает в канвас {cw}x{ch}")
|
||||
|
||||
colors = {}
|
||||
for i, rgb in enumerate(seed_records):
|
||||
colors[rgb] = 16 + i
|
||||
|
||||
def color_index(r, g, b):
|
||||
key = (r, g, b)
|
||||
if key not in colors:
|
||||
idx = args.base_index + (len(colors) - len(seed_records))
|
||||
if idx >= 0xFF:
|
||||
die("палитра переполнена (>= 0xFF своих цветов)")
|
||||
colors[key] = idx
|
||||
return colors[key]
|
||||
|
||||
data = bytearray()
|
||||
for im in imgs:
|
||||
if args.anchor == "top-left":
|
||||
ox, oy = 0, 0
|
||||
elif args.anchor == "center":
|
||||
ox, oy = (cw - im.width) // 2, (ch - im.height) // 2
|
||||
else: # bottom-center
|
||||
ox, oy = (cw - im.width) // 2, ch - im.height
|
||||
canvas = [0xFF] * (cw * ch)
|
||||
for y in range(im.height):
|
||||
for x in range(im.width):
|
||||
r, g, b, a = im.getpixel((x, y))
|
||||
if a < 128:
|
||||
continue
|
||||
canvas[(oy + y) * cw + (ox + x)] = color_index(r, g, b)
|
||||
data.extend(canvas)
|
||||
open(args.out_raw, "wb").write(data)
|
||||
|
||||
pal = bytearray()
|
||||
if args.ega:
|
||||
for b, g, r in EGA:
|
||||
pal += bytes((b, g, r, 0))
|
||||
new_colors = [rgb for rgb in colors if rgb not in seed_records]
|
||||
for (r, g, b) in (*seed_records, *new_colors):
|
||||
pal += bytes((b, g, r, 0))
|
||||
open(args.out_pal, "wb").write(pal)
|
||||
|
||||
print(f"png_strip: {len(imgs)} кадров, канвас {cw}x{ch}, "
|
||||
f"{len(data)} Б -> {args.out_raw}")
|
||||
print(f" палитра {len(seed_records)} seed + {len(new_colors)} новых "
|
||||
f"своих цветов ({len(pal)} Б) -> {args.out_pal}")
|
||||
print(f"дальше: mkatlas.py OUT.atl {args.out_raw}:{cw}x{ch}:1x{len(imgs)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
room_compose.py — генерик-компоновщик полноэкранного фона комнаты из
|
||||
тайлов ПРОИЗВОЛЬНОГО размера (см. applications/PoP/docs/PORT_PLAN.md §4:
|
||||
один полноэкранный фон на комнату готовится офлайн, поверх — спрайты
|
||||
через sprite.h). Не завязан ни на какой конкретный проект/игру — просто
|
||||
берёт раскладку тайлов, которую вы сами опишете, и картинки, которые
|
||||
сами укажете.
|
||||
|
||||
Раскладка — JSON:
|
||||
{
|
||||
"rows": [
|
||||
{"height": 20, "tiles": ["a.png", "b.png", ...]},
|
||||
{"height": 60, "tiles": [...]},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Тайлы в ряду кладутся слева направо; высота ряда — из "height" (тайл
|
||||
меньшей высоты — сверху, остаток — прозрачность 0xFF; выше — ошибка).
|
||||
Ширина итоговой картинки = сумма ширин тайлов первого ряда — остальные
|
||||
ряды обязаны давать ТУ ЖЕ сумму (иначе ошибка, во избежание кривого
|
||||
экрана по краю).
|
||||
|
||||
Использование:
|
||||
room_compose.py OUT.raw OUT.pal --layout layout.json [--base-index N]
|
||||
|
||||
Выход:
|
||||
OUT.raw — ГОТОВЫЙ формат getimage (uint16 w, uint16 h LE + пиксели
|
||||
построчно) — грузится fread'ом целиком и передаётся
|
||||
напрямую в gfx_blit(x, y, buf).
|
||||
OUT.pal — палитра (EGA 16 + свои цвета), формат gfx_pal_fload.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
EGA = [
|
||||
(0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0),
|
||||
(0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168),
|
||||
(84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84),
|
||||
(84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255),
|
||||
]
|
||||
|
||||
|
||||
def die(msg):
|
||||
sys.exit(f"room_compose: {msg}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(add_help=False)
|
||||
ap.add_argument("out_raw")
|
||||
ap.add_argument("out_pal")
|
||||
ap.add_argument("--layout", required=True)
|
||||
ap.add_argument("--base-index", type=int, default=16)
|
||||
ap.add_argument("--ega", dest="ega", action="store_true", default=True)
|
||||
ap.add_argument("--no-ega", dest="ega", action="store_false")
|
||||
args = ap.parse_args()
|
||||
|
||||
spec = json.loads(Path(args.layout).read_text())
|
||||
rows_spec = spec["rows"]
|
||||
if not rows_spec:
|
||||
die("layout: пустой список rows")
|
||||
|
||||
rows_imgs = []
|
||||
width = None
|
||||
for ri, row in enumerate(rows_spec):
|
||||
rh = int(row["height"])
|
||||
tiles = row["tiles"]
|
||||
if not tiles:
|
||||
die(f"row {ri}: пустой список tiles")
|
||||
imgs = [Image.open(t).convert("RGBA") for t in tiles]
|
||||
for t, im in zip(tiles, imgs):
|
||||
if im.height > rh:
|
||||
die(f"{t}: высота {im.height} > высоты ряда {rh}")
|
||||
row_w = sum(im.width for im in imgs)
|
||||
if width is None:
|
||||
width = row_w
|
||||
elif row_w != width:
|
||||
die(f"row {ri}: ширина {row_w} != ширины первого ряда {width} "
|
||||
f"(все ряды должны давать одинаковую суммарную ширину)")
|
||||
rows_imgs.append((rh, imgs))
|
||||
|
||||
height = sum(rh for rh, _ in rows_imgs)
|
||||
if width > 65535 or height > 65535:
|
||||
die(f"{width}x{height}: превышает предел формата (u16)")
|
||||
|
||||
colors = {}
|
||||
|
||||
def color_index(r, g, b):
|
||||
key = (r, g, b)
|
||||
if key not in colors:
|
||||
idx = args.base_index + len(colors)
|
||||
if idx >= 0xFF:
|
||||
die("палитра переполнена (>= 0xFF своих цветов)")
|
||||
colors[key] = idx
|
||||
return colors[key]
|
||||
|
||||
canvas = [0xFF] * (width * height)
|
||||
y0 = 0
|
||||
for rh, imgs in rows_imgs:
|
||||
x0 = 0
|
||||
for im in imgs:
|
||||
for y in range(im.height):
|
||||
for x in range(im.width):
|
||||
r, g, b, a = im.getpixel((x, y))
|
||||
if a < 128:
|
||||
continue
|
||||
canvas[(y0 + y) * width + (x0 + x)] = color_index(r, g, b)
|
||||
x0 += im.width
|
||||
y0 += rh
|
||||
|
||||
with open(args.out_raw, "wb") as f:
|
||||
f.write(struct.pack("<HH", width, height))
|
||||
f.write(bytes(canvas))
|
||||
|
||||
pal = bytearray()
|
||||
if args.ega:
|
||||
for b, g, r in EGA:
|
||||
pal += bytes((b, g, r, 0))
|
||||
for (r, g, b) in colors:
|
||||
pal += bytes((b, g, r, 0))
|
||||
open(args.out_pal, "wb").write(pal)
|
||||
|
||||
print(f"room_compose: {width}x{height} ({len(rows_imgs)} рядов) "
|
||||
f"-> {args.out_raw} ({4 + width * height} Б, вкл. getimage-заголовок)")
|
||||
print(f" палитра {len(colors)} своих цветов "
|
||||
f"({len(pal)} Б) -> {args.out_pal}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user