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:
2026-07-17 17:51:08 +03:00
parent 484b18d10c
commit cd8d566d82
196 changed files with 6630 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
/*
* room.c — вся логика комнаты, ОБЫЧНЫЙ HOME-файл (без --bank).
*
* 2026-07-15/16: изначально была банковская (--bank 1), сначала в W3
* (huge-дефолт), потом пробовали W1 (--memory big) — оба варианта
* упирались в проблемы (W3 конфликтует с gfx_blit_part/atlas_load,
* которые сами трогают W3; W1-банк несовместим с CODE=W1 — трамплин
* сам бы уехал из W1; --memory big тесно — CODE+DATA+BSS ~20КБ, не
* лезет в один 16КБ). После замены sprintf() на ручное hex-форматирование
* пути (экономия ~2.9КБ — sprintf/vsprintf тянули за собой заметный
* кусок форматирующего кода, единственный потребитель в HOME) всё
* снова влезает в W1 обычным HOME-кодом — банк ОКАЗАЛСЯ НЕ НУЖЕН.
* Так проще: gfx_blit_part()/atlas_load() (трогают W3 транзитивно)
* теперь просто обычные HOME-функции, без всяких W1/W3-танцев.
*
* gfx_blit_part пишет ПРЯМО в текущий банк (видео-ОЗУ + теневая копия
* при GFX_BANK_NORMAL) — перманентно, не через sprite_t. Вызывающий
* код обязан позвать room_draw() на КАЖДУЮ из двух страниц дабл-буфера
* при входе в комнату, иначе heal спрайтов увидит фон только на одной
* странице.
*/
#include <stdio.h>
#include <gfx.h>
#include "room.h"
/* 32x64 на тайл: 3*64+8=200 — сходится с разрешением DOS 320x200
* (8px внизу — под полоску жизней/строку текста, не часть игрового
* поля). Уточнить точнее, когда будут готовы настоящие ассеты. */
uint8_t room_row_h[ROOM_ROWS] = {64, 64, 64};
tile_atlas_t tile_atlas[32];
int tile_atlas_load(const char *dir)
{
static const char HEXD[] = "0123456789abcdef";
char path[64];
int i, count = 0;
for (i = 0; i < 32; i++) {
/* Вручную, не sprintf("%stile%02x.atl",...) — единственный
* потребитель printf-семейства в этом приложении, тянул за
* собой ~2.9КБ форматирующего кода; формат тут фиксированный
* (2 hex-цифры). */
char *p = path;
const char *s = dir;
while (*s) *p++ = *s++;
s = "tile";
while (*s) *p++ = *s++;
*p++ = HEXD[(i >> 4) & 0xF];
*p++ = HEXD[i & 0xF];
s = ".atl";
while (*s) *p++ = *s++;
*p = '\0';
if (atlas_load(&tile_atlas[i].at, path) == 0) {
tile_atlas[i].loaded = 1;
count++;
} else {
tile_atlas[i].loaded = 0;
}
}
return count;
}
void tile_atlas_free(void)
{
int i;
for (i = 0; i < 32; i++) {
if (tile_atlas[i].loaded) {
atlas_free(&tile_atlas[i].at);
tile_atlas[i].loaded = 0;
}
}
}
int level_load_room(level_t *lv, uint8_t room, const char *path)
{
FILE *f = fopen(path, "rb");
uint8_t buf[ROOM_TILES * 2 + 3]; /* fore[30]+back[30]+start_room/pos/dir */
int i;
if (!f)
return -1;
if (fread(buf, 1, sizeof(buf), f) != sizeof(buf)) {
fclose(f);
return -1;
}
fclose(f);
for (i = 0; i < ROOM_TILES; i++) {
tile_t *t = level_tile(lv, room, (uint8_t)(i % ROOM_COLS),
(uint8_t)(i / ROOM_COLS));
t->type = buf[i];
t->state = buf[ROOM_TILES + i];
}
lv->start_room = buf[ROOM_TILES * 2];
lv->start_location = buf[ROOM_TILES * 2 + 1];
lv->start_direction = (int8_t)buf[ROOM_TILES * 2 + 2];
return 0;
}
/* _bgi_blit_rows_raw держит ОДИН DI на весь блит (см. её шапку) —
* вызывающий обязан резать блиты выше 16 строк на чанки, иначе окно
* DI слишком длинное (риск сорвать кадровую цепочку IRQ/CBL). Наши
* тайлы 64px высотой — режем на чанки по CHUNK_H строк. */
#define CHUNK_H 16
void room_draw(level_t *lv, uint8_t room)
{
int row, col, y = 0;
for (row = 0; row < ROOM_ROWS; row++) {
int x = 0;
uint8_t fh = room_row_h[row];
for (col = 0; col < ROOM_COLS; col++) {
tile_t *t = level_tile(lv, room, (uint8_t)col, (uint8_t)row);
uint8_t type = TILE_TYPE(t->type);
if (tile_atlas[type].loaded) {
uint8_t count = tile_atlas[type].at.count;
uint8_t variant = 0;
uint8_t is_var;
if (count != 0) {
switch (type) {
case TILE_SPIKES:
case TILE_GATE:
case TILE_TAPESTRY:
case TILE_TAPESTRY_TOP:
case TILE_CHOPPER:
case TILE_POTION:
is_var = 1;
break;
default:
is_var = 0;
}
variant = is_var
? (uint8_t)(t->state % count)
: (uint8_t)((room * 31u + col * 7u + row * 13u) % count);
}
const void *img = atlas_image(&tile_atlas[type].at, 0);
uint8_t sub_y;
gfx_w0_map(tile_atlas[type].at.page);
for (sub_y = 0; sub_y < fh; sub_y = (uint8_t)(sub_y + CHUNK_H)) {
uint8_t chunk = (uint8_t)(fh - sub_y);
if (chunk > CHUNK_H)
chunk = CHUNK_H;
gfx_blit_part(x, y + sub_y, img, variant * ROOM_TILE_W,
sub_y, ROOM_TILE_W, chunk);
}
gfx_w0_unmap();
}
x += ROOM_TILE_W;
}
y += fh;
}
}