Files
Sprinter-SDCC/applications/SprPoP/src/pop_tile_cold.c
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00

241 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_tile_cold.c — ХОЛОДНАЯ половина общих листьев слоя фона (банк 5).
*
* ЗАЧЕМ РАЗРЕЗ. pop_tile.c был самым крупным жильцом резидента (5 972 Б
* кода), а резидент huge-режима — это всё окно 0x4100..0xBB00, и место в нём
* нужно под музыку, заставки и UI. Но целиком pop_tile не уедет: его
* const-таблицы (POP_TILE_DIV/MOD, pop_tile_table, таблицы кадров) читают
* банки 2, 3, 7 и 8, а таблица в чужом банке не видна.
*
* КРИТЕРИЙ ОТБОРА — ЗАМЕР, а не догадка. Каждая функция pop_tile была
* посчитана брейкпоинтом-счётчиком в MAME: сколько вызовов в кадре покоя и
* сколько в кадре ПОЛНОЙ перерисовки комнаты (её форсирует чит +/-). Сюда
* переехало только то, у чего пик <= 3 вызовов на кадр — и проверено это
* на ДВУХ тайлсетах: подземелье (уровень 1) и дворец (уровень 4), потому
* что pop_mem_b рисует композитный кусок и мог оказаться дворцовым.
* Замер: pop_mem_b 0, pop_cd_hit 0, pop_t_win_set 0..1, pop_heal_off 0..1,
* pop_room_set_above 1, pop_bar_black 3, pop_potion_flask 0..1,
* pop_cd_hit_slot 2..3 — при канарейке pop_blit_b 184..214 в том же кадре.
*
* ЦЕНА. Трамплин замерен: 252 такта пролог + 84 эпилог + ~50 на стороне
* вызывающего = ~410 тактов на вызов. На кадр покоя выходит ~1 000 тактов
* (0,2 % работы кадра), на кадр редрава ~3 700 (0,009 растра).
*
* ЧТО ОСТАЛОСЬ В РЕЗИДЕНТЕ и почему: pop_blit_b (184 вызова на редрав) со
* своими статиками blit_b_clip/blit_b_oversize, pop_cd_touch (198),
* pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73),
* pop_tile_mod (70), pop_cd_batch_end (40) — и все const-таблицы.
* pop_fore_set_clip оставлен намеренно: 88 Б не стоят отказа от прямого
* вызова из банка 4, ради которого он и заводился.
*
* ВЫЗОВЫ ОТСЮДА В РЕЗИДЕНТ — прямые, без трамплина: W1/W2 замаплены всегда.
* Поэтому blit_b_clip (статик горячей половины) просто перестал быть static
* и объявлен в _pop_tile.h, а не переехал следом.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF, POP_PLAYFIELD_H */
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
#include "pop_cdraw.h" /* pop_cd, pop_cd_dirty, pop_cd_ymin/ymax */
#include "pop_tile.h"
#include "_pop_draw.h" /* pop_heal_fast */
#include "_pop_tile.h" /* blit_b_clip — из горячей половины */
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
/* Смещение склянки внутри тайла (seg008 draw_tile_fore): те же числа, что в
* tile_table горячей половины — дублируем, потому что там они локальные
* define'ы, а тащить их в публичный заголовок ради одной функции незачем. */
#define POT_FORE_X 2
#define POT_FORE_Y (-3)
/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов.
*
* pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и
* функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)`
* (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть
* до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и
* аргументов остаётся два.
*
* Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит
* касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */
static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */
static uint16_t hit_w, hit_h;
static uint8_t hit_p;
static uint8_t hit_rect(void)
{
int8_t c0, c1, c;
uint8_t ya, yb;
int x1 = hit_x + (int)hit_w - 1;
int y1 = hit_y + (int)hit_h - 1;
if (!hit_w || !hit_h) return 0;
if (x1 < 0 || hit_x > 319) return 0;
if (y1 < 0 || hit_y > 255) return 0;
ya = (uint8_t)(hit_y < 0 ? 0 : hit_y);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5);
c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5);
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1;
return 0;
}
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked
{
const pop_cdraw_t *s = &pop_cd[who];
if (!(pop_cd_dirty & (1 << p))) return 0;
hit_p = p;
hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF;
hit_w = s->w[p]; hit_h = s->h[p];
if (hit_rect()) return 1;
if (!s->ovalid[p]) return 0;
hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF;
hit_w = s->ow[p]; hit_h = s->oh[p];
return hit_rect();
}
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked
{
int8_t c0, c1, c;
uint8_t ya, yb;
if (!(pop_cd_dirty & (1 << p))) return 0;
if (x1 < 0 || x0 > 319) return 0;
if (x0 < 0) x0 = 0;
if (x1 > 319) x1 = 319;
if (y1 < 0 || y0 > 255) return 0;
ya = (uint8_t)(y0 < 0 ? 0 : y0);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)(x0 >> 5);
c1 = (int8_t)(x1 >> 5);
/* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке.
* Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет
* само, отдельной проверки «пусто» не нужно. */
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1;
return 0;
}
/* Страница приведена в порядок — снять с неё метку целиком. */
/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не
* расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255,
* ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */
void pop_cd_init(void) __banked
{
pop_cd_clear(0);
pop_cd_clear(1);
}
void pop_cd_clear(uint8_t p) __banked
{
uint8_t c;
for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; }
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
}
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
* экранная — для клипа самого блита; при перевороте они разные. */
void pop_t_win_set(int x, int ytop, int w, int h) __banked
{
pop_t_fclip_x0 = x;
pop_t_fclip_x1 = x + w;
pop_t_fclip_ly0 = ytop;
pop_t_fclip_ly1 = ytop + h;
if (pop_upside) {
int t = FLIP_TOP(ytop, h);
pop_t_fclip_y0 = t;
pop_t_fclip_y1 = t + h;
} else {
pop_t_fclip_y0 = ytop;
pop_t_fclip_y1 = ytop + h;
}
pop_t_fclip_on = 1;
}
void pop_t_win_clear(void) __banked
{ pop_t_fclip_on = 0; }
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа:
* те же клип полосы у потолка и окно перерисовки, что у pop_blit_b, но без
* atlas_image и без gfx_w0_map/unmap (это ещё ~1 350 такта). Заведено под
* композитный спрайт падающего куска (pop_room.c: три части сложены в один
* блоб, один вызов блита вместо трёх). */
void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked
{
uint8_t w = img[0], h = img[2];
int top;
if (pop_t_fclip_on &&
(x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 ||
ybottom + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
ybottom + POP_YOFF - 64 >= pop_t_fclip_ly1))
return;
if (!w || !h) return;
top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
if (!pop_t_fclip_on && !pop_t_clip_top &&
!(pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) &&
x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) {
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
else gfx_blit_noclip(x, top, img);
pop_cd_touch(x, top, (int)w, (int)h);
} else {
blit_b_clip(img, x, top, w, h);
}
}
/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер,
* пики, loose, ворота, полоса потолка). При перевороте прямоугольник
* обязан отразиться ровно так же, как сам тайл, — иначе heal чистит одно
* место, а рисует в другое: именно так «ломалась анимация чомперов»
* (найдено пользователем на первом же прогоне зеркального фона). */
void pop_bar_black(int x, int top, int w, int h) __banked
{
if (pop_upside) top = FLIP_TOP(top, h);
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, top, x + w - 1, top + h - 1);
pop_cd_touch(x, top, w, h); /* bar идёт мимо pop_blit_b (DRAW-COST) */
}
void pop_heal_off(int x, int y, int w, int h) __banked
{
int top = y + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
pop_heal_fast(x, top, w, h);
}
/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- *
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
* склянка (id 13), остальные — маленькая (id 12). Спрайты из chtab_1 (атлас
* pop_pot), поэтому НЕ из env: раньше рисовали chtab_6 id 12 (res212),
* которого в VDUNGEON нет — оттуда и «мусорный объект». Позиция оригинала:
* xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + draw_main_y.
*
* Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная
* половина), и fore-проход поверх персонажа (горячая). */
void pop_potion_flask(int row, int col, uint8_t modif) __banked
{
uint8_t type = (uint8_t)((modif & 0xF8) >> 3);
uint8_t id = 12;
if (type == 0) return;
if (type >= 2 && type < 5) id = 13; /* большая склянка */
if (pop_palace) id += 2; /* seg008:747: во дворце склянки другие */
pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6,
POT_FORE_Y + 63 * row + 62);
}
/* Ряд 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) __banked
{ pop_t_below = below_row0_fg; }
/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у
* потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }