Files
Sprinter-SDCC/applications/PoP/roomtest/pop_intro.c
T
snark13 16d3262340 Звук: насос CBL качает через W3 — вход в BIOS ломал W0
СИМПТОМ: затемнение (fade) хрипело — одинаково с играющей музыкой и в
тишине.  Ключевое наблюдение пользователя: повтор тишины обязан звучать
тишиной, значит дело не в недоливе буфера.

ПОИСК: отладочные клавиши, каждая делала ровно один кусок fade.  Ожидание
кадров — чисто; чтение палитры, запись палитры и 512 вызовов
bios_get_place() (видео вообще не трогает) — скрежет во всех трёх.
Последнее и решило: виновата не палитра, а ЛЮБОЙ вызов BIOS.

ПРИЧИНА: `rst 8` раскрывается в `out ($7C),a`, который включает системное
ПЗУ и перестраивает окно 0 (MAME sprinter.cpp, update_memory: m_pages[0] +
m_bank_view0.select).  ПЗУ ложится ПОВЕРХ страничного регистра, поэтому
запись в порт 0x82 из прерывания бесполезна — OTIR вычитывает ПЗУ и
отдаёт его в звук.  Отсюда же старое правило «глушить CBL на время
загрузки файлов»: причина была не в том, что ESTEX долго занимает CPU.

РЕШЕНИЕ (идея пользователя): качать через W3.  Он управляется только
портом 0xE2, подмену из прерывания никто не перекрывает, а BIOS во время
нашего ISR не исполняется — окно возвращается до выхода, и для него
подмена невидима.  После этого BIOS безопасен везде.

* pop_sfx.c — насос берёт взаймы W3 вместо W0, чтение по 0xC000 + смещение.
* pop_ui.c — буфер палитры по фиксированному 0x4000 (эти 256 байт DSS
  занимает только при загрузке программы): 256 байт со стека долой.
* libbgi/common/gfx_pal_write.c — запись палитры прямо в видеопамять,
  минуя BIOS.  Писалась как обход скрежета, после переноса насоса не
  нужна; оставлена как более быстрый примитив (2,5 тыс. тактов на 64
  цвета против 10,8 тыс. у BIOS) с честной шапкой.  Адресация разобрана
  по исходникам BIOS (FUNC_SCREEN.ASM): Port_Y = индекс цвета, адрес
  0xC3E0 + pal*4, порядок R/G/B/Y.
* libc/video/pal_get.c, pal_load.c — в шапках зафиксировано, что BIOS
  выбирает окно ПО АДРЕСУ БУФЕРА (`BIT 7,H`).
* pop_intro.c — при пропуске интро клавишей не глушился CBL, и следующая
  загрузка уровня шла с открытым буфером; добавлен pop_sfx_pause.
* pop_ctrl.c — убраны отладочные «осторожные шаги» на J/L (эмуляция
  Shift+стрелка для MAME), у них и стоял TODO.

Разбор целиком — docs/sound_plan.md §5.  Бюджет: _CODE 23966, куча 245 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 19:36:47 +03:00

1414 lines
60 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_intro.c — вторая половина show_title(): story + Princess/Jaffar (FG8).
*
* Готовые сцены создаёт pop_pack_intro.py из TITLE/PV SDLPoP. Каждая сцена
* хранится пятью полосами <=49 строк, поэтому EMM удерживает лишь текущую
* полосу. pop_cutscene_t остаётся независимым от графики: этот модуль только
* исполняет его события и позднее будет образцом для pre-level сцен FG9.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h"
#include "pop_shadow.h"
#include "pop_sfx.h"
#include "pop_music.h"
#include "pop_cutscene.h"
#include "pop_intro.h"
#include "pop_pal.h"
#include "pop_timer.h"
#include "pop_ui.h"
#include "pop_kid.h" /* Char/окна, play_seq — движок персонажей катсцен */
#include "pop_guard.h" /* Guard, CHARID_*, pop_gframe */
#define INTRO_PARTS 5
#define INTRO_FADE 4
#define PV_ANIM_TICKS 1959
/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422). Музыка сюжета
* (50/52/53) в наборе пустая — её у нас нет; эти два ЕСТЬ и в оригинале
* звучат именно здесь: створка ворот закрывается, затем открывается дверь
* покоев, из которой входит Джафар. */
#define PV_SND_GATE_CLOSING 4
#define PV_SND_DOOR_OPENING 51
/* Шкала ниже повторяет порядок pv_scene() SDLPoP. Обычный кадр cutscene
* длится 8 тиков, после начала заклинания — 7. Длинные речевые паузы здесь
* заданы явно: в DOS они заканчиваются по окончании CBL-сэмпла. */
enum {
PV_WAIT_END = 360,
PV_GATE_END = 408,
PV_DOOR_END = 432,
PV_TURN_START = PV_DOOR_END,
PV_WALK1_START = 472,
PV_DIALOG1_START = 520,
PV_WALK2_START = 696,
PV_DIALOG2_START = 936,
PV_RAISE_START = 1216,
PV_STEPBACK_START = 1223,
PV_MAGIC_START = 1342,
PV_EXIT_START = 1490,
PV_GLASS_DONE = 1700,
PV_SLUMP_START = 1763
};
enum {
INTRO_STORY_ABSENCE = 0,
INTRO_STORY_MARRY,
INTRO_STORY_CREDITS,
INTRO_STORY_HAIL,
INTRO_STORY_FRAME,
INTRO_PV_WAITING,
INTRO_PV_JAFFAR,
INTRO_PV_MAGIC,
INTRO_PV_ALONE,
INTRO_PV_STAND,
INTRO_PV_LYING,
INTRO_PV_MOUSE,
INTRO_PV_SHORT
};
enum {
INTRO_PAL_STORY = 0,
INTRO_PAL_PV = 1
};
/* 0..4 — TITLE/res41 + тексты 42/43/45/44 и чистая рамка; 5..12 —
* PV/res951 + персонажи.
* 8.3-имена намеренно короткие: DSS не получает длинные path-компоненты. */
static const char * const intro_part[13][INTRO_PARTS] = {
{ "PV\\s0_0.atl", "PV\\s0_1.atl", "PV\\s0_2.atl", "PV\\s0_3.atl", "PV\\s0_4.atl" },
{ "PV\\s1_0.atl", "PV\\s1_1.atl", "PV\\s1_2.atl", "PV\\s1_3.atl", "PV\\s1_4.atl" },
{ "PV\\s2_0.atl", "PV\\s2_1.atl", "PV\\s2_2.atl", "PV\\s2_3.atl", "PV\\s2_4.atl" },
{ "PV\\s3_0.atl", "PV\\s3_1.atl", "PV\\s3_2.atl", "PV\\s3_3.atl", "PV\\s3_4.atl" },
{ "PV\\s4_0.atl", "PV\\s4_1.atl", "PV\\s4_2.atl", "PV\\s4_3.atl", "PV\\s4_4.atl" },
{ "PV\\p0_0.atl", "PV\\p0_1.atl", "PV\\p0_2.atl", "PV\\p0_3.atl", "PV\\p0_4.atl" },
{ "PV\\p1_0.atl", "PV\\p1_1.atl", "PV\\p1_2.atl", "PV\\p1_3.atl", "PV\\p1_4.atl" },
{ "PV\\p2_0.atl", "PV\\p2_1.atl", "PV\\p2_2.atl", "PV\\p2_3.atl", "PV\\p2_4.atl" },
{ "PV\\p3_0.atl", "PV\\p3_1.atl", "PV\\p3_2.atl", "PV\\p3_3.atl", "PV\\p3_4.atl" },
{ "PV\\p4_0.atl", "PV\\p4_1.atl", "PV\\p4_2.atl", "PV\\p4_3.atl", "PV\\p4_4.atl" },
{ "PV\\p5_0.atl", "PV\\p5_1.atl", "PV\\p5_2.atl", "PV\\p5_3.atl", "PV\\p5_4.atl" },
{ "PV\\p6_0.atl", "PV\\p6_1.atl", "PV\\p6_2.atl", "PV\\p6_3.atl", "PV\\p6_4.atl" },
{ "PV\\p7_0.atl", "PV\\p7_1.atl", "PV\\p7_2.atl", "PV\\p7_3.atl", "PV\\p7_4.atl" }
};
/* Чистая Princess-room без персонажей. Shadow-копии обеих страниц содержат
* именно этот фон: каждый анимационный кадр получает его accelerator-копией,
* а полупрозрачные actor-спрайты не оставляют следа. */
static const char * const pv_base_part[INTRO_PARTS] = {
"PV\\b0_0.atl", "PV\\b0_1.atl", "PV\\b0_2.atl", "PV\\b0_3.atl", "PV\\b0_4.atl"
};
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */
static const pop_cs_cmd_t intro_before_pv[] = {
/* absence собирается прямо поверх последнего title-кадра полосовым
* transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */
{ POP_CS_WAIT, 0x258 }, /* story 1: In the absence */
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* Файловый fallback сохраняет возможность пройти title на неполном HDD.
* Нормальная ветка его не вызывает: p0..p3 оставлены также для диагностики
* точек сюжета и не являются анимационным ресурсом. */
static const pop_cs_cmd_t intro_pv_fallback[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV },
{ POP_CS_SHOW, INTRO_PV_WAITING },
{ POP_CS_FADE_IN, 0 },
{ POP_CS_WAIT, 0x1A }, /* princess, gate and door */
{ POP_CS_SHOW, INTRO_PV_JAFFAR },
{ POP_CS_WAIT, 0x6D }, /* Jaffar comes/walks */
{ POP_CS_SHOW, INTRO_PV_MAGIC },
{ POP_CS_FLASH, 5 }, /* hourglass appears */
{ POP_CS_WAIT, 0x2F },
{ POP_CS_SHOW, INTRO_PV_ALONE },
{ POP_CS_WAIT, 0x3D }, /* Jaffar leaves, princess looks */
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t intro_after_pv[] = {
{ POP_CS_PALETTE, INTRO_PAL_STORY },
{ POP_CS_SHOW, INTRO_STORY_MARRY },
{ POP_CS_FADE_IN, 0 },
{ POP_CS_WAIT, 0x78 },
{ POP_CS_SHOW, INTRO_STORY_CREDITS },
{ POP_CS_WAIT, 0x168 },
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* Статические fallback-сценарии хранят уже физические кадры Sprinter.
* Один логический кадр cutscene SDLPoP длится 6/60 с; на 50 Гц это ровно
* 5/50 с. Поэтому суммы proc_cutscene_frame() здесь умножены на пять. */
static const pop_cs_cmd_t pre_2_6[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_STAND },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_4[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_LYING },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_8[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
/* 20 + 20 + 20 логических кадров = 6,0 с. */
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 300 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_9[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
/* 5 + 9 + 58 логических кадров = 7,2 с. */
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 360 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_12_short[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_SHORT },
/* 2 + 24 логических кадра = те же 2,6 с, что и длинная ветка. */
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* time_expired() SDLPoP: hourglass_state=7, fade in, 2+100 кадров, fade
* out. P7 — готовая стадия комнаты с часами; звук намеренно не стартует,
* так как CBL на lifecycle screen погашен до дисковых чтений. */
static const pop_cs_cmd_t time_expired_script[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_SHORT },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 102 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* end_sequence_anim() — объятие/мышь — здесь сведён к отдельной PV-стадии;
* затем end_sequence() SDLPoP показывает HAIL ровно 900 тиков. */
/* Хвост финала после живой сцены встречи: только исходный story-экран. */
static const pop_cs_cmd_t ending_hail_script[] = {
{ POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t ending_script[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 54 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static uint8_t intro_scene;
static uint8_t intro_pending_pal;
static uint8_t intro_pal_pending;
/* Таблицы intro_part/pv_base_part расположены в этом же банке 11. Общий
* pop_screen_blit_atlas_strips находится в банке 9, и передавать ему
* указатель на W3-данные нельзя: после bcall тот же адрес уже видит другой
* физический банк. Держим короткий полосовой loader рядом с его таблицами.
* atlas_load сам временно занимает W3 только ПОСЛЕ open(path). */
static int intro_blit_atlas_strips(const char * const *parts,
uint8_t count, int x, int y)
{
uint8_t part;
gfx_set_bank(GFX_BANK_NORMAL);
for (part = 0; part < count; part++) {
atlas_t at;
const uint8_t *img;
if (atlas_load(&at, parts[part]) != 0) return -1;
gfx_w0_map(at.page);
img = (const uint8_t *)atlas_image(&at, 0);
gfx_blit(x, y, img);
y += img[2];
gfx_w0_unmap();
atlas_free(&at);
}
return 0;
}
/* ---- «Пропустить сцену» — по НОВОМУ нажатию, а не по удержанию -------- *
*
* Клавиша, которой закончили уровень (движение Кида в дверь, чит Shift+L,
* skip предыдущего экрана), в момент старта сцены ЕЩЁ ЗАЖАТА. Считать это
* за skip нельзя: сцена схлопывается в один кадр, и её никто не видит.
* Ждать отпускания безусловно тоже нельзя — потерянный break-код вешает
* игру намертво (BUGS_OPEN.md#kbd-stuck-wait).
*
* Поэтому детектор ФРОНТА: пока хоть что-то зажато со старта, skip не
* взводится; как только всё отпущено, детектор «взводится» и следующее
* нажатие сцену прерывает. Залипший бит в худшем случае делает сцену
* непропускаемой — она доиграет сама, а игра не встанет. */
typedef struct { uint8_t armed; } intro_skip_t;
static void intro_skip_begin(intro_skip_t *sk)
{
kbd_raw_sync();
sk->armed = (uint8_t)!kbd_raw_any_down();
}
static uint8_t intro_skip_requested(intro_skip_t *sk)
{
kbd_raw_sync();
if (!kbd_raw_any_down()) { sk->armed = 1; return 0; }
return sk->armed;
}
static int intro_draw_page(uint8_t scene, uint8_t page)
{
if (scene >= sizeof(intro_part) / sizeof(intro_part[0])) return -1;
gfx_set_draw_page(page);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
if (intro_blit_atlas_strips(intro_part[scene], INTRO_PARTS,
0, POP_YOFF) != 0)
return -1;
return 0;
}
static int intro_draw(uint8_t scene)
{
uint8_t page = pop_screen_begin();
if (intro_draw_page(scene, page) != 0) return -1;
pop_screen_present(page);
return 0;
}
/* У gfx_pal_fload/sync палитра меняется по 64 цвета за вызов BIOS. Если в
* этот момент хоть одна страница содержит следующую сцену, один VSync может
* показать её уже яркой, ещё до штатного fade-in. Поэтому перед сменой
* источника палитры уничтожаем растр на ОБЕИХ страницах, а не только гасим
* их палитры. */
static void intro_black_pages(void)
{
gfx_set_draw_page(0);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
gfx_set_draw_page(1);
pop_screen_fill(BLACK);
pop_screen_present(1);
}
/* Для смены полного источника палитры сначала гасим старую сцену и
* оставляем на видимой странице настоящий чёрный растр. */
static void intro_prepare_palette(uint8_t pal)
{
intro_pending_pal = pal;
intro_pal_pending = 1;
pop_pal_apply(4);
intro_black_pages();
}
static void intro_commit_palette(void)
{
if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load();
else pop_pal_story_load();
pop_pal_apply(4);
intro_pal_pending = 0;
}
static int intro_draw_new_palette(uint8_t scene)
{
uint8_t page;
/* Палитра загружается, пока обе страницы содержат только чёрный растр.
* После повторного затемнения можно собрать новую сцену в скрытую
* страницу и показать её исключительно чёрной перед fade-in. */
intro_commit_palette();
page = pop_screen_begin();
if (intro_draw_page(scene, page) != 0) return -1;
pop_screen_present(page);
return 0;
}
/* В отличие от intro_draw_page() здесь base собирается в shadow ОБЕИХ
* страниц. После show page 1 palette 0 не видна и в неё безопасно загрузить
* PV\pv.pal; первый animated frame соберётся в page 0. */
static int intro_pv_draw_base(void)
{
gfx_rect_t full;
gfx_set_draw_page(0);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
if (intro_blit_atlas_strips(pv_base_part, INTRO_PARTS,
0, POP_YOFF) != 0)
return -1;
gfx_set_draw_page(1);
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
gfx_copy_page(&full, GFX_COPY_DIRECT);
pop_screen_present(1);
return 0;
}
static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor)
{
const uint8_t *img;
gfx_w0_map(at->page);
img = (const uint8_t *)atlas_image(at, frame);
/* add_*table() оригинала задаёт нижнюю строку спрайта включительно. */
gfx_blit(x, POP_YOFF + floor - img[2] + 1, img);
gfx_w0_unmap();
}
static void pv_blit_actor(const atlas_t *at, uint8_t frame, int anchor_x,
int floor, uint8_t flip)
{
const uint8_t *img;
int x = anchor_x;
gfx_w0_map(at->page);
img = (const uint8_t *)atlas_image(at, frame);
/* draw_mid SDLPoP зеркалит правое направление относительно hot-point:
* экранный x сначала масштабирован, затем из него вычитается width. */
if (flip) x -= img[0];
gfx_blit_cols(x, POP_YOFF + floor - img[2] + 1, img, flip);
gfx_w0_unmap();
}
static void pv_draw_stars(const uint8_t *phase)
{
static const uint8_t star_x[] = { 20, 16, 23, 17, 24, 18 };
static const uint8_t star_y[] = { 97, 104, 110, 116, 120, 128 };
static const uint8_t star_color[] = { 8, 7, 15, 15, 7 };
uint8_t i;
for (i = 0; i < sizeof(star_x); i++) {
/* draw_rect SDLPoP получает полуинтервал [x,x+1)×[y,y+1).
* BGI bar() включает правую/нижнюю границу и давал звезду 2x2. */
putpixel(star_x[i], POP_YOFF + star_y[i], star_color[phase[i]]);
}
}
/* У Princess/Jaffar номер frame — это индекс frame_tbl_cuts, а картинка
* лежит в отдельном chtab. Эти таблицы — компактная нужная часть таблицы
* SDLPoP: не приходится тащить в банк весь interpreter SEQTBL. */
typedef struct {
int16_t x; /* Char.x оригинала: полу-пиксели */
uint8_t frame;
uint8_t left;
uint8_t visible;
uint8_t step;
} pv_actor_t;
/* frame_tbl_cuts[1..18].image; 10 — служебная пустая запись. */
static const uint8_t pv_princess_image[19] = {
0, 15, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 9, 10, 11, 12, 13, 14, 16
};
static const int8_t pv_princess_dx[19] = {
0, 0, 0, 0, 0, -1, 2, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
static const uint8_t pv_princess_parity[19] = {
0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0
};
/* frame_tbl_cuts[48..85].dx. image равен frame - 48. */
static const int8_t pv_jaffar_dx[38] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3, 3, 3, 2, 3, 5, 5, 1, 2, 2, 1, 1, 2, 3, 3, 0, 2, 2, 1
};
static const uint8_t pv_jaffar_parity[38] = {
1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0,
0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0
};
static const uint8_t pv_walk_frame[6] = { 48, 49, 50, 51, 52, 53 };
static const int8_t pv_walk_dx[6] = { 1, 2, 6, 1, -1, 1 };
static const uint8_t pv_raise_frame[] = {
85, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67,
68, 69, 70, 71, 72, 73, 74, 75, 83, 84, 76
};
static int pv_actor_x(const pv_actor_t *actor, int frame_dx, uint8_t parity)
{
int x = actor->x;
int logical;
if (actor->left) x -= frame_dx;
else x += frame_dx;
logical = x * 2 - 116;
/* load_frame_to_obj: нечётная фаза зависит от flags кадра и direction.
* chtab Princess/Jaffar затем проходит calc_screen_x_coord(320/280). */
if (actor->left == parity) ++logical;
return logical * 8 / 7;
}
static void pv_walk_step(pv_actor_t *jaffar)
{
uint8_t step = (uint8_t)(jaffar->step % 6);
int dx = pv_walk_dx[step];
if (jaffar->left) jaffar->x -= dx;
else jaffar->x += dx;
jaffar->frame = pv_walk_frame[step];
++jaffar->step;
}
static void pv_princess_step(pv_actor_t *princess, uint16_t tick)
{
uint16_t pos;
/* Palert: 2..9, flip, dx(8), 11. Она продолжается и во время входа
* Jaffar, как последовательность Palert оригинала. */
if (tick >= PV_TURN_START && tick < PV_TURN_START + 72 &&
((tick - PV_TURN_START) & 7) == 0) {
pos = (uint16_t)((tick - PV_TURN_START) >> 3);
if (pos < 8) princess->frame = (uint8_t)(2 + pos);
else {
princess->left = 0;
princess->x += 8;
princess->frame = 11;
}
}
/* Pstepback: flip, dx(11), 12; затем 13..16 и вечный кадр 17. */
if (tick >= PV_STEPBACK_START && tick < PV_STEPBACK_START + 42 &&
((tick - PV_STEPBACK_START) % 7) == 0) {
pos = (uint16_t)((tick - PV_STEPBACK_START) / 7);
if (pos == 0) {
princess->left = 1;
princess->x -= 11;
princess->frame = 12;
} else if (pos == 1) {
princess->x -= 1;
princess->frame = 13;
} else if (pos == 2) {
princess->x -= 1;
princess->frame = 14;
} else if (pos == 3) {
princess->x -= 3;
princess->frame = 15;
} else if (pos == 4) {
princess->x -= 1;
princess->frame = 16;
} else {
princess->frame = 17;
}
}
/* Pslump: один переходный frame 1, затем frame 18 до fade-out. */
if (tick == PV_SLUMP_START) princess->frame = 1;
else if (tick == PV_SLUMP_START + 7) princess->frame = 18;
}
static void pv_jaffar_exit_step(pv_actor_t *jaffar)
{
uint8_t pos = jaffar->step++;
if (pos < 6) {
jaffar->frame = (uint8_t)(77 + pos);
} else if (pos < 12) {
if (pos == 6) --jaffar->x;
jaffar->frame = 54;
} else if (pos < 22) {
uint8_t frame = (uint8_t)(57 + pos - 12);
if (frame == 62) jaffar->x -= 2;
else if (frame == 63) ++jaffar->x;
else if (frame == 64) jaffar->x += 3;
else if (frame == 66) ++jaffar->x;
jaffar->frame = frame;
} else if (pos == 22) {
/* Vexit заканчивается flip, dx(16), dx(3), затем Vwalk2. */
jaffar->left = 0;
jaffar->x += 19;
jaffar->frame = 49;
} else {
/* Vexit прыгает на метку Vwalk2: кадр 49 уже выведен в ветке
* flip, следующий шаг обязан начинаться с dx(6), frame 50. */
uint8_t walk = (uint8_t)(pos - 23 + 2);
int dx = pv_walk_dx[walk % 6];
jaffar->x += dx;
jaffar->frame = pv_walk_frame[walk % 6];
}
}
static void pv_jaffar_step(pv_actor_t *jaffar, uint16_t tick)
{
uint16_t pos;
if (tick == PV_WALK1_START || tick == PV_WALK2_START) jaffar->step = 0;
if (((tick >= PV_WALK1_START && tick < PV_DIALOG1_START) ||
(tick >= PV_WALK2_START && tick < PV_DIALOG2_START)) &&
((tick - ((tick < PV_DIALOG1_START) ? PV_WALK1_START :
PV_WALK2_START)) & 7) == 0) {
jaffar->visible = 1;
pv_walk_step(jaffar);
}
/* Vstop: dx(1), 55, 56, Vstand. */
if (tick == PV_DIALOG1_START || tick == PV_DIALOG2_START) {
--jaffar->x;
jaffar->frame = 55;
} else if (tick == PV_DIALOG1_START + 8 || tick == PV_DIALOG2_START + 8) {
jaffar->frame = 56;
} else if ((tick >= PV_DIALOG1_START + 16 && tick < PV_WALK2_START) ||
(tick >= PV_DIALOG2_START + 16 && tick < PV_RAISE_START)) {
jaffar->frame = 54;
}
if (tick >= PV_RAISE_START && tick < PV_EXIT_START &&
((tick - PV_RAISE_START) % 7) == 0) {
pos = (uint16_t)((tick - PV_RAISE_START) / 7);
if (pos < sizeof(pv_raise_frame)) jaffar->frame = pv_raise_frame[pos];
else jaffar->frame = 76;
}
if (tick == PV_EXIT_START) jaffar->step = 0;
if (tick >= PV_EXIT_START && ((tick - PV_EXIT_START) % 7) == 0) {
pv_jaffar_exit_step(jaffar);
if (!jaffar->left && jaffar->x > 205) jaffar->visible = 0;
}
}
static void pv_blit_jaffar(const atlas_t *j0, const atlas_t *j1,
const atlas_t *j2, const pv_actor_t *jaffar)
{
uint8_t image;
int x;
if (!jaffar->visible || jaffar->frame < 48 || jaffar->frame > 85) return;
image = (uint8_t)(jaffar->frame - 48);
x = pv_actor_x(jaffar, pv_jaffar_dx[image], pv_jaffar_parity[image]);
if (image < 13)
pv_blit_actor(j0, image, x, 166, (uint8_t)!jaffar->left);
else if (image < 24)
pv_blit_actor(j1, (uint8_t)(image - 13), x, 166,
(uint8_t)!jaffar->left);
else
pv_blit_actor(j2, (uint8_t)(image - 24), x, 166,
(uint8_t)!jaffar->left);
}
static void pv_blit_princess(const atlas_t *princess, const pv_actor_t *actor)
{
uint8_t frame = actor->frame;
if (frame == 0 || frame > 18) return;
pv_blit_actor(princess, pv_princess_image[frame],
pv_actor_x(actor, pv_princess_dx[frame],
pv_princess_parity[frame]),
166, (uint8_t)!actor->left);
}
/* Внутренняя шкала сценария пока остаётся в исходных тиках 60 Гц: так
* сохраняются точки начала реплик и последовательностей. Внешне каждый
* такой логический кадр показывается четыре физических кадра Sprinter. */
static uint8_t pv_seq_period(uint16_t tick)
{
if (tick < PV_WAIT_END) return 6;
if (tick < PV_RAISE_START) return 8;
return 7;
}
static void intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
const atlas_t *j1, const atlas_t *j2,
const atlas_t *hourglass, const atlas_t *torch,
const pv_actor_t *p, const pv_actor_t *j,
const uint8_t *star_phase,
uint8_t torch_left, uint8_t torch_right,
uint8_t sand_frame, uint8_t flash,
uint16_t tick)
{
gfx_rect_t full;
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE);
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
pv_blit_floor(torch, torch_left, 93, 116);
pv_blit_floor(torch, torch_right, 211, 116);
pv_draw_stars(star_phase);
pv_blit_princess(princess, p);
pv_blit_jaffar(j0, j1, j2, j);
if (tick >= PV_MAGIC_START) {
/* В SDLPoP hourglass_sandflow=0 означает первый кадр потока,
* а -1 — отсутствие песка. Корпус появляется ещё при молнии. */
pv_blit_floor(hourglass,
(uint8_t)(tick < PV_GLASS_DONE ? 1 : 2), 152, 168);
}
if (tick >= PV_EXIT_START) {
/* hourglass_sandflow=0 оригинал ставит лишь после окончания
* conjuring — когда Jaffar опускает руки и начинает уходить. */
pv_blit_floor(hourglass, (uint8_t)(8 + sand_frame), 160, 164);
}
/* draw_princess_room_bg() SDLPoP помещает image 2 chtab PV в
* foretable с x=30*8, ybottom=167. В нашем h0 это кадр 0 (res952):
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
* правой передней колонной. */
pv_blit_floor(hourglass, 0, 240, 167);
gfx_wait_vsync();
gfx_set_visible_page(back);
if (flash) {
uint8_t pulse;
/* Молния намеренно живёт на физической, а не cutscene-шкале:
* пять быстрых white/black импульсов, актёры на это время стоят. */
for (pulse = 0; pulse < 5; pulse++) {
pop_pal_flash_white(1);
gfx_wait_vsync();
pop_pal_flash_white(0);
gfx_wait_vsync();
}
} else {
uint8_t hold;
/* Номинальные 50/4 = 12,5 FPS. Если сама сборка кадра пересекла
* лишний фронт, фактическая частота естественно станет ниже; точный
* anchor-based pacing оставлен отдельным TODO. */
for (hold = 1; hold < 4; hold++) gfx_wait_vsync();
}
}
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
* вход и речь Jaffar, заклинание+молния, часы с песком, уход. Тайминг
* воспроизводится на кадровой шкале; пропуск проверяется каждый кадр. */
static int intro_pv_animated(void)
{
atlas_t princess, jaffar0, jaffar1, jaffar2, hourglass, torch;
pv_actor_t princess_actor;
pv_actor_t jaffar_actor;
uint16_t tick;
uint16_t frame_end;
uint16_t render_tick;
uint8_t anim_step = 0;
uint8_t torch_left = 1;
uint8_t torch_right = 6;
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
uint8_t sand_frame = 0;
uint8_t flash_done = 0;
uint8_t flash;
intro_skip_t skip;
int result = -1;
intro_prepare_palette(INTRO_PAL_PV);
/* commit обязан быть до сборки base: gfx_pal_sync обновляет и видимую
* страницу, поэтому там в этот момент должен оставаться только чёрный
* растр из intro_prepare_palette(). */
intro_commit_palette();
if (intro_pv_draw_base() != 0) return -1;
/* b0_* собирается полосами: на время очередной полосы ей тоже нужна
* временная EMM-страница. Поэтому фон готовится до постоянных
* actor-атласов: порядок сохраняет независимость загрузчика полос от
* числа одновременно живущих анимационных ресурсов. */
if (atlas_load(&princess, "PV\\a0.atl") != 0) return -1;
if (atlas_load(&jaffar0, "PV\\j0.atl") != 0) {
goto free_princess;
}
if (atlas_load(&jaffar1, "PV\\j1.atl") != 0) {
goto free_jaffar0;
}
if (atlas_load(&jaffar2, "PV\\j2.atl") != 0) {
goto free_jaffar1;
}
if (atlas_load(&hourglass, "PV\\h0.atl") != 0) {
goto free_jaffar2;
}
if (atlas_load(&torch, "PV\\t0.atl") != 0) {
goto free_hourglass;
}
pop_pal_fade_in(INTRO_FADE);
/* Все атласы сцены уже в EMM: дальше файлового ввода-вывода нет, и
* насос CBL можно открывать (иначе ESTEX не даёт долить блок — см.
* контракт pop_sfx_start). Полноэкранная accel-копия кадра насосу не
* мешает: leaf режет DI бандами по 16 строк. */
(void)pop_sfx_start();
result = 0;
princess_actor.x = 120;
princess_actor.frame = 11; /* Pstand, facing left */
princess_actor.left = 1;
princess_actor.visible = 1;
princess_actor.step = 0;
jaffar_actor.x = 198;
jaffar_actor.frame = 54; /* Vstand, behind the closed door */
jaffar_actor.left = 1;
jaffar_actor.visible = 0;
jaffar_actor.step = 0;
tick = 0;
intro_skip_begin(&skip);
while (tick < PV_ANIM_TICKS) {
if (intro_skip_requested(&skip)) {
result = 1;
break;
}
/* Один проход = один логический кадр cutscene. Внутри проходим все
* его исходные 6/8/7 тиков, снаружи draw_frame держит результат
* четыре физических кадра — единые номинальные 12,5 FPS. */
flash = 0;
frame_end = (uint16_t)(tick + pv_seq_period(tick));
if (frame_end > PV_ANIM_TICKS) frame_end = PV_ANIM_TICKS;
do {
pv_princess_step(&princess_actor, tick);
pv_jaffar_step(&jaffar_actor, tick);
if (tick == PV_MAGIC_START && !flash_done) {
flash = 1;
flash_done = 1;
}
/* Границы шкалы уже стоят там, где оригинал ЖДЁТ окончания
* очередного сэмпла, — заявляем эффект ровно на них. */
if (tick == PV_WAIT_END) pop_sfx_play(PV_SND_GATE_CLOSING);
else if (tick == PV_GATE_END) pop_sfx_play(PV_SND_DOOR_OPENING);
++tick;
} while (tick < frame_end);
{
uint8_t star = (uint8_t)(anim_step % 6);
torch_left = (uint8_t)((torch_left + 1) % 9);
torch_right = (uint8_t)((torch_right + 1) % 9);
star_phase[star] = (uint8_t)((star_phase[star] + 1) % 5);
if (tick > PV_EXIT_START)
sand_frame = (uint8_t)((sand_frame + 1) % 3);
++anim_step;
}
render_tick = (uint16_t)(tick - 1);
intro_pv_draw_frame(&princess, &jaffar0, &jaffar1, &jaffar2,
&hourglass, &torch, &princess_actor,
&jaffar_actor, star_phase, torch_left,
torch_right, sand_frame, flash, render_tick);
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
}
pop_sfx_pause(); /* дальше снова только загрузка файлов */
if (!result) pop_pal_fade_out(INTRO_FADE);
free_torch:
atlas_free(&torch);
free_hourglass:
atlas_free(&hourglass);
free_jaffar2:
atlas_free(&jaffar2);
free_jaffar1:
atlas_free(&jaffar1);
free_jaffar0:
atlas_free(&jaffar0);
free_princess:
atlas_free(&princess);
return result;
}
/* hourglass_frame() SDLPoP: четыре границы оставшегося времени выбирают
* один из кадров 6..2. В h0.atl номер кадра совпадает с hourglass_state. */
static uint8_t pre_hourglass_state(void)
{
static const uint8_t bound[] = { 6, 17, 33, 65 };
uint8_t i;
for (i = 0; i < sizeof(bound); i++)
if (bound[i] > pop_timer_minutes) break;
return (uint8_t)(6 - i);
}
/* cutscene_2_6()/cutscene_4(): 26 логических кадров по 100 мс. У SDLPoP
* это 6/60 секунды, у Sprinter — 5/50, то есть 130 физических кадров.
* Статична только поза Princess; draw_princess_room_bg оригинала
* каждый логический кадр заново обновляет факелы, звёзды, корпус часов и
* одну из трёх фаз сыплющегося песка. */
static int pre_princess_animated(pop_pre_cutscene_t scene)
{
atlas_t hourglass, torch;
gfx_rect_t full;
uint8_t picture = (uint8_t)(scene == POP_PRE_4 ?
INTRO_PV_LYING : INTRO_PV_STAND);
uint8_t state = pre_hourglass_state();
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
intro_skip_t skip;
int result = -1;
intro_prepare_palette(INTRO_PAL_PV);
intro_commit_palette();
/* Обе shadow-страницы содержат чистую статическую сцену. Динамика ниже
* рисуется GFX_BANK_SPRITE и потому не оседает в этом источнике. */
if (intro_draw_page(picture, 0) != 0 ||
intro_draw_page(picture, 1) != 0) return -1;
if (atlas_load(&hourglass, "PV\\h0.atl") != 0) return -1;
if (atlas_load(&torch, "PV\\t0.atl") != 0) goto free_hourglass_pre;
pop_screen_present(1);
pop_pal_fade_in(INTRO_FADE);
result = 0;
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
intro_skip_begin(&skip);
for (frame = 0; frame < 26; frame++) {
uint8_t back, hold, star;
if (intro_skip_requested(&skip)) { result = 1; break; }
back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE);
pv_blit_floor(&torch, torch_left, 93, 116);
pv_blit_floor(&torch, torch_right, 211, 116);
pv_draw_stars(star_phase);
pv_blit_floor(&hourglass, state, 152, 168);
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
gfx_wait_vsync();
gfx_set_visible_page(back);
/* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */
for (hold = 1; hold < 5; hold++) gfx_wait_vsync();
torch_left = (uint8_t)((torch_left + 1) % 9);
torch_right = (uint8_t)((torch_right + 1) % 9);
sand = (uint8_t)((sand + 1) % 3);
star = (uint8_t)(frame % 6);
star_phase[star] = (uint8_t)((star_phase[star] + 1) % 5);
}
if (!result) pop_pal_fade_out(INTRO_FADE);
atlas_free(&torch);
free_hourglass_pre:
atlas_free(&hourglass);
return result;
}
/* SDLPoP transition_ltr(): подготовленная story-страница копируется на
* видимый title узкими вертикальными полосами слева направо. У оригинала
* 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1.3 с), не
* удваивая число полных копий 320x256 в ограниченном bank 9. */
static int intro_story_transition(void)
{
gfx_rect_t stripe;
uint8_t visible = gfx_get_visible_page();
uint8_t target = (uint8_t)(visible ^ 1);
int x;
if (intro_draw_page(INTRO_STORY_ABSENCE, target) != 0) return -1;
gfx_set_draw_page(visible); /* source gfx_copy_page = target */
stripe.y = 0;
stripe.h = GFX_HEIGHT;
for (x = 0; x < GFX_WIDTH; x += 4) {
if (kbd_raw_any_down()) {
stripe.x = 0; stripe.w = GFX_WIDTH;
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
break;
}
stripe.x = x;
stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
gfx_wait_vsync();
}
return 0;
}
static void intro_flash(uint16_t ticks)
{
uint8_t page;
page = pop_screen_begin();
pop_screen_fill(WHITE);
pop_screen_present(page);
while (ticks--) gfx_wait_vsync();
/* Вспышка ничего не меняет в сценарии: вернуть ту же собранную стадию. */
intro_draw(intro_scene);
}
static void intro_restore_game_palette(void)
{
/* Обе страницы — именно чёрный растр до gfx_pal_fload/sync. Иначе
* синхронизация palette 1 способна на один кадр высветить credits перед
* переходом в demo. */
intro_black_pages();
pop_pal_game_load();
pop_pal_apply(4);
}
/* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= *
*
* Порт cutscene_8 / cutscene_9 (seg001:034D/03B7) и end_sequence_anim
* (seg001:041C). Все три идут через load_intro(1, …): фон — комната
* принцессы (та же b0_*, что у intro), а поверх играют ДВА персонажа.
*
* Движок общий с игрой, и это главное: последовательности крутит наш
* play_seq (pop_kid_play/pop_guard_play), кадры принцессы берутся из
* таблицы КАТСЦЕН (kid_data.bin, KID_BIN_CFRAMES_OFF), мышь и Кид — из
* таблицы Кида, как в оригинале (frame_table_kid общая для kid и mouse).
* Отличается только ОТРИСОВКА: спрайты принцессы лежат в PV-атласах, а
* Кид/мышь — в игровых kid-атласах, поэтому в pv.pal под них отведена
* своя палитровая строка 0x70.
*
* Слоты — как в оригинале: Kid-окно занимает мышь (или Кид в финале),
* Guard-окно — принцесса.
*/
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
#define CUT_PV2_SPLIT 17
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен
* (cutscene_frame_time SDLPoP; см. pre_princess_animated). */
#define CUT_FRAME_VSYNC 5
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
static uint8_t cut_atlases; /* сколько успели загрузить — столько и вернём */
/* Детектор фронта живёт на ВСЮ сцену: она склеена из нескольких cut_run, и
* взводить его заново на каждом отрезке значило бы ловить одно удержание
* несколько раз. */
static intro_skip_t cut_skip;
/* Фазы «живого» фона: пламя двух факелов, песок и мерцание звёзд. Счётчики
* общие на всю сцену — она склеена из нескольких cut_run. */
static uint8_t cut_torch_l, cut_torch_r, cut_sand, cut_star_phase[6];
static uint8_t cut_hg_state;
static void cut_free(void)
{
if (cut_atlases > 4) atlas_free(&cut_torch);
if (cut_atlases > 3) atlas_free(&cut_hg);
if (cut_atlases > 2) atlas_free(&cut_pv2b);
if (cut_atlases > 1) atlas_free(&cut_pv2a);
if (cut_atlases > 0) atlas_free(&cut_pv1);
cut_atlases = 0;
}
static int cut_load(void)
{
cut_atlases = 0;
if (atlas_load(&cut_pv1, "PV\\a0.atl") != 0) return -1;
cut_atlases = 1;
if (atlas_load(&cut_pv2a, "PV\\a1.atl") != 0) { cut_free(); return -1; }
cut_atlases = 2;
if (atlas_load(&cut_pv2b, "PV\\a2.atl") != 0) { cut_free(); return -1; }
cut_atlases = 3;
/* Комната принцессы живёт и в катсценах: пламя факелов, звёзды в окне,
* песок в часах — и ПЕРЕДНЯЯ КОЛОННА, которая по оригиналу закрывает
* персонажей (draw_princess_room_bg + foretable). */
if (atlas_load(&cut_hg, "PV\\h0.atl") != 0) { cut_free(); return -1; }
cut_atlases = 4;
if (atlas_load(&cut_torch, "PV\\t0.atl") != 0) { cut_free(); return -1; }
cut_atlases = 5;
return 0;
}
/* Экранная X кадра: та же математика, что у актёров intro — logical
* координата (x*2 − 116) с нечётной фазой по flags кадра, затем
* calc_screen_x_coord (×8/7). */
static int cut_screen_x(const pop_char_t *ch, const kframe *fr)
{
int x = ch->x;
int logical;
uint8_t left = (uint8_t)(ch->direction < 0);
if (left) x -= fr->dx;
else x += fr->dx;
logical = x * 2 - 116;
if (left == (uint8_t)((fr->flags & 0x80) != 0)) ++logical;
return logical * 8 / 7;
}
/* Принцесса: набор выбирает поле sword кадра (>>6: 1 = PV1 res800,
* 2 = PV2 res900) — ровно как chtab_base + (cur_frame.sword >> 6)
* в seg008:1759. */
static void cut_blit_princess(const pop_char_t *ch, const kframe *fr)
{
const atlas_t *at;
uint8_t idx;
if (fr->image == 255 || cut_atlases < 3) return;
if ((uint8_t)(fr->sword >> 6) >= 2) {
if (fr->image < CUT_PV2_SPLIT) { at = &cut_pv2a; idx = fr->image; }
else { at = &cut_pv2b; idx = (uint8_t)(fr->image - CUT_PV2_SPLIT); }
} else {
at = &cut_pv1; idx = fr->image;
}
pv_blit_actor(at, idx, cut_screen_x(ch, fr),
(int)ch->y + fr->dy, (uint8_t)(ch->direction >= 0));
}
/* Кид и мышь — спрайты chtab_2 из игровых атласов (kidp[]). Атласы Кида
* хранятся column-major (ради бесплатного флипа), поэтому блит колоночный. */
static void cut_blit_kid(const pop_char_t *ch, const kframe *fr)
{
const uint8_t *img;
uint8_t pg, idx, flip;
int x;
if (fr->image == 255) return;
pg = (uint8_t)(fr->image >> 3);
idx = (uint8_t)(fr->image & 7);
if (pg >= kid_npages) return;
flip = (uint8_t)(ch->direction >= 0);
x = cut_screen_x(ch, fr);
gfx_w0_map(kidp[pg].page);
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
if (flip) x -= img[0];
gfx_blit_cols(x, POP_YOFF + (int)ch->y + fr->dy - img[2] + 1, img, flip);
gfx_w0_unmap();
}
/* Сменить последовательность персонажа в его окне (seqtbl_offset_kid_char /
* seqtbl_offset_shad_char оригинала). */
static void cut_kid_seq(uint8_t seq)
{
pop_loadkid(); pop_char_set_seq(seq); pop_savekid();
}
static void cut_shad_seq(uint8_t seq)
{
pop_loadshad(); pop_char_set_seq(seq); pop_saveshad();
}
/* Поставить персонажа так, как это делают init_* оригинала: позиция,
* направление и стартовая последовательность, затем первый play_seq. */
static void cut_kid_init(uint8_t charid, uint8_t x, uint8_t y, uint8_t seq)
{
Kid.charid = charid;
Kid.x = x;
Kid.y = y;
Kid.direction = -1; /* dir_FF_left */
Kid.alive = -1;
Kid.frame = 0;
Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0;
cut_kid_seq(seq);
pop_kid_play();
}
static void cut_shad_init(uint8_t x, uint8_t y, uint8_t seq)
{
Guard.charid = CHARID_5_PRINCESS;
Guard.x = x;
Guard.y = y;
Guard.direction = -1;
Guard.alive = -1;
Guard.frame = 0;
Guard.fall_x = Guard.fall_y = 0;
Guard.repeat = 0;
cut_shad_seq(seq);
pop_guard_play();
}
/* proc_cutscene_frame(n): прокрутить n логических кадров, каждый — шаг
* последовательности обоих персонажей плюс отрисовка. Возврат 1 — игрок
* прервал сцену (любое НОВОЕ нажатие). */
static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
{
gfx_rect_t full;
uint16_t i;
uint8_t hold;
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
for (i = 0; i < frames; i++) {
uint8_t back;
kframe kid_fr, pr_fr;
if (intro_skip_requested(&cut_skip)) return 1;
/* Кадр забираем СРАЗУ после play каждого слота. load_frame кладёт
* его в кэш ПО charid активного персонажа: у Кида это kid_frame, у
* всех прочих — общий pop_gframe. В Kid-окне здесь сидит МЫШЬ
* (charid 24), то есть её кадр уходит в pop_gframe и затирается
* следом кадром принцессы, а kid_frame держит позу игрового Кида.
* Читать кэши в конце кадра значило рисовать мышь спрайтом Кида и
* терять принцессу — ровно это и было видно в сценах 8/9. */
pop_kid_play(); /* мышь либо Кид */
kid_fr = (Kid.charid == CHARID_0_KID) ? kid_frame : pop_gframe;
pop_guard_play(); /* принцесса */
pr_fr = pop_gframe;
back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
* банком спрайтов и потому в неё не оседают. */
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE);
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
pv_draw_stars(cut_star_phase);
pv_blit_floor(&cut_hg, cut_hg_state, 152, 168);
pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164);
cut_blit_princess(&Guard, &pr_fr);
if (kid_visible) cut_blit_kid(&Kid, &kid_fr);
/* ПОСЛЕ персонажей: передняя колонна — передний слой комнаты, она
* обязана закрывать и принцессу, и мышь (иначе мышь «просвечивает»
* сквозь неё — поймано пользователем 2026-08-25). */
pv_blit_floor(&cut_hg, 0, 240, 167);
gfx_set_bank(GFX_BANK_NORMAL);
gfx_wait_vsync();
gfx_set_visible_page(back);
for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9);
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
cut_sand = (uint8_t)((cut_sand + 1) % 3);
{
uint8_t star = (uint8_t)(i % 6);
cut_star_phase[star] = (uint8_t)((cut_star_phase[star] + 1) % 5);
}
}
return 0;
}
/* Общая обвязка всех трёх сцен: чистый фон комнаты принцессы в обе
* страницы, PV-палитра, актёрские атласы. Возврат -1 — ресурсов нет
* (неполный HDD), и caller откатывается на статический сценарий. */
static int cut_begin(void)
{
uint8_t i;
intro_skip_begin(&cut_skip);
cut_torch_l = 1; cut_torch_r = 6; cut_sand = 0;
for (i = 0; i < 6; i++) cut_star_phase[i] = 1;
cut_hg_state = pre_hourglass_state();
intro_prepare_palette(INTRO_PAL_PV);
intro_commit_palette();
if (intro_pv_draw_base() != 0) return -1;
if (cut_load() != 0) return -1;
return 0;
}
static void cut_end(uint8_t aborted)
{
if (!aborted) pop_pal_fade_out(INTRO_FADE);
cut_free();
}
/* cutscene_8 (seg001:034D): мышь сидит рядом с принцессой, встаёт и
* убегает; принцесса поднимается ей вслед. */
static int cut_scene_8(void)
{
uint8_t aborted;
if (cut_begin() != 0) return -1;
/* init_mouse_cu8 (seg001:0212) = init_mouse_go + x=144 + seq_106. */
cut_kid_init(CHARID_24_MOUSE, 144, 167, 106);
/* princess_crouching (seg001:024D): сидит и гладит мышь. */
cut_shad_init(131, 169, 110);
pop_pal_fade_in(INTRO_FADE);
aborted = cut_run(20, 1);
if (!aborted) {
cut_kid_seq(107); /* мышь встаёт и уходит */
aborted = cut_run(20, 1);
}
if (!aborted) {
cut_shad_seq(111); /* принцесса встаёт */
aborted = cut_run(20, 1);
}
cut_end(aborted);
return 0;
}
/* cutscene_9 (seg001:03B7): принцесса стоит, мышь прибегает, принцесса
* приседает к ней. */
static int cut_scene_9(void)
{
uint8_t aborted;
if (cut_begin() != 0) return -1;
/* princess_stand (seg001:026A) — поза PV1, лицом вправо. */
cut_shad_init(144, 169, 94);
Guard.direction = 0;
pop_pal_fade_in(INTRO_FADE);
/* init_mouse_go (seg001:022A): мышь вбегает справа. */
cut_kid_init(CHARID_24_MOUSE, 199, 167, 105);
aborted = cut_run(5, 1);
if (!aborted) {
cut_shad_seq(112); /* принцесса приседает */
aborted = cut_run(9, 1);
}
if (!aborted) {
cut_kid_seq(114); /* мышь останавливается */
aborted = cut_run(58, 1);
}
cut_end(aborted);
return 0;
}
/* end_sequence_anim (seg001:041C): Кид вбегает к принцессе, она
* поворачивается и обнимает его; следом появляется мышь. */
static int cut_ending(void)
{
uint8_t aborted;
if (cut_begin() != 0) return -1;
/* init_ending_princess (seg001:0251) + init_ending_kid (seg001:0287). */
cut_shad_init(136, 164, 109);
cut_kid_init(CHARID_0_KID, 198, 164, 1);
pop_pal_fade_in(INTRO_FADE);
pop_sfx_play(26); /* «arrived to princess» */
aborted = cut_run(8, 1);
if (!aborted) {
cut_shad_seq(108); /* поворот и объятие */
aborted = cut_run(5, 1);
}
if (!aborted) {
cut_kid_seq(13); /* Кид останавливается */
aborted = cut_run(2, 1);
}
/* Kid.frame = 0 (seg001:0348): дальше Кида не видно — он в объятиях,
* и рисуется только принцесса. */
if (!aborted) aborted = cut_run(39, 0);
if (!aborted) {
/* init_mouse_1 (seg001:02D5): мышь приходит к паре. */
cut_kid_init(CHARID_24_MOUSE, 197, 164, 105);
aborted = cut_run(9, 1);
}
if (!aborted) {
cut_kid_seq(101); /* мышь встаёт на задние лапы */
aborted = cut_run(41, 1);
}
cut_end(aborted);
return 0;
}
static uint8_t intro_run(const pop_cs_cmd_t *script)
{
pop_cutscene_t cs;
intro_skip_t skip;
uint16_t arg;
uint8_t aborted = 0;
pop_cutscene_begin(&cs, script);
intro_skip_begin(&skip);
for (;;) {
pop_cs_event_t ev;
if (intro_skip_requested(&skip)) {
pop_cutscene_abort(&cs);
aborted = 1;
break;
}
ev = pop_cutscene_tick(&cs, &arg);
if (ev == POP_CS_EVENT_END) break;
if (ev == POP_CS_EVENT_PALETTE) intro_prepare_palette((uint8_t)arg);
else if (ev == POP_CS_EVENT_SHOW) {
intro_scene = (uint8_t)arg;
if ((intro_pal_pending ? intro_draw_new_palette(intro_scene) :
intro_draw(intro_scene)) != 0) {
pop_cutscene_abort(&cs);
aborted = 1;
break;
}
} else if (ev == POP_CS_EVENT_FADE_IN) pop_pal_fade_in(INTRO_FADE);
else if (ev == POP_CS_EVENT_FADE_OUT) pop_pal_fade_out(INTRO_FADE);
else if (ev == POP_CS_EVENT_FLASH) intro_flash(arg);
gfx_wait_vsync();
}
return aborted;
}
uint8_t pop_intro_show(void) __banked
{
int pv_result;
/* Title уже заглушил CBL; оставляем это явным lifecycle-правилом, если
* intro позднее будет вызвано отдельно (например, из debug-маршрута). */
pop_sfx_pause();
if (intro_story_transition() != 0) {
/* Ошибка файла не должна оставить старый title без выхода: менее
* эффектный, но корректный fallback остаётся обычным fade-in. */
intro_prepare_palette(INTRO_PAL_STORY);
if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0)
pop_pal_fade_in(INTRO_FADE);
}
/* Первый экран истории — единственное место, где музыка уже звучит.
* Грузим ПОСЛЕ того, как картинка собрана: чтение кусков идёт через
* ESTEX, и при открытом CBL насос не успел бы долить блок (скрежет,
* см. pop_sfx_start). Порядок обязателен: load -> start -> play. */
if (pop_music_load(POP_MUS_STORY_1) == 0) {
(void)pop_sfx_start();
pop_music_play();
}
if (intro_run(intro_before_pv)) {
/* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт
* загрузка уровня, а при открытом CBL ESTEX уходит в диск дольше,
* чем играет буфер, и железо начинает крутить свои 256 байт по
* кругу — на слух ровно тот скрежет, что ловится нажатием клавиши
* на первом экране истории. В обычной ветке ниже pause уже есть,
* а здесь его не было. */
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
/* ДОИГРАТЬ ПОД ЧЁРНЫМ ЭКРАНОМ. Оригинал не обрывает story-трек на
* переходе к PV: fade уводит картинку, а музыка продолжается, и лишь
* когда она смолкла, начинается сцена с принцессой (у SDLPoP это
* общий приём — `while (check_sound_playing())`, seg000:2027).
* Мы освобождали страницы сразу после fade, и трек обрывался на
* полуслове — на слух это и был скрежет в конце затемнения. */
{
intro_skip_t tail;
intro_skip_begin(&tail);
while (pop_music_busy() && !intro_skip_requested(&tail)) pop_wait_edge();
}
pop_music_free();
pop_sfx_pause();
pv_result = intro_pv_animated();
if (pv_result > 0) {
intro_restore_game_palette();
return 1;
}
if (pv_result < 0 && intro_run(intro_pv_fallback)) {
intro_restore_game_palette();
return 1;
}
if (intro_run(intro_after_pv)) {
intro_restore_game_palette();
return 1;
}
/* Abort возможен в любой WAIT и не оставляет загруженных атласов: каждая
* полоса освобождена внутри intro_draw. */
intro_restore_game_palette();
return 0;
}
void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
{
const pop_cs_cmd_t *script = 0;
pop_sfx_pause();
if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) {
if (pre_princess_animated(scene) >= 0) {
intro_restore_game_palette();
return;
}
/* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */
}
/* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */
if (scene == POP_PRE_8 || scene == POP_PRE_9) {
int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9();
if (done == 0) {
intro_restore_game_palette();
return;
}
}
if (scene == POP_PRE_2_6 || scene == POP_PRE_12) script = pre_2_6;
else if (scene == POP_PRE_4) script = pre_4;
else if (scene == POP_PRE_8) script = pre_8;
else if (scene == POP_PRE_9) script = pre_9;
else if (scene == POP_PRE_12_SHORT) script = pre_12_short;
if (!script) return;
intro_run(script);
intro_restore_game_palette();
}
void pop_time_expired_show(void) __banked
{
pop_sfx_pause();
intro_run(time_expired_script);
intro_restore_game_palette();
}
void pop_ending_show(void) __banked
{
pop_sfx_pause();
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
* перед встречей. */
pop_pal_apply(4);
/* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по
* скрипту идёт только текст HAIL, поэтому статический сценарий ниже
* начинается с него; при нехватке ресурсов отыгрывается целиком. */
if (cut_ending() == 0) intro_run(ending_hail_script);
else intro_run(ending_script);
intro_restore_game_palette();
}