Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 623199337e | |||
| 602c3a20fa | |||
| 4ac3584bf9 | |||
| 3bf28da8ee | |||
| 770b946a36 | |||
| 89e9663753 | |||
| ea07a8d7b0 | |||
| d23126983e | |||
| 50570881f5 | |||
| f25ed37d85 | |||
| 3449f6f8c9 |
@@ -40,7 +40,7 @@ FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
|||||||
# образ у себя и связывает его с MAME символьной ссылкой.
|
# образ у себя и связывает его с MAME символьной ссылкой.
|
||||||
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
||||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||||
|
|
||||||
# Optional knobs — see top of file.
|
# Optional knobs — see top of file.
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ EXAMPLE := sprpop
|
|||||||
SRC_DIR := src
|
SRC_DIR := src
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
|
|
||||||
|
MAKE_HDD := tools/make_hdd.sh
|
||||||
|
|
||||||
# huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3.
|
# huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3.
|
||||||
MEMORY ?= huge
|
MEMORY ?= huge
|
||||||
# --max-allocs-per-node: во сколько вариантов размещения регистров упирается
|
# --max-allocs-per-node: во сколько вариантов размещения регистров упирается
|
||||||
@@ -91,7 +93,7 @@ BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_app.c pop_timer.c \
|
|||||||
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
||||||
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
||||||
# 485 байт, то есть 3% страницы.
|
# 485 байт, то есть 3% страницы.
|
||||||
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c
|
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c pop_path_bank.c
|
||||||
BANK11_SRCS := pop_cutscene.c pop_intro.c
|
BANK11_SRCS := pop_cutscene.c pop_intro.c
|
||||||
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
||||||
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | **корень найден и устранён** 2026-08-28 (байты крал KEYSCAN DSS); ждёт полевой проверки |
|
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | **корень найден и устранён** 2026-08-28 (байты крал KEYSCAN DSS); ждёт полевой проверки |
|
||||||
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
||||||
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
||||||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | открыт: диагноз полный, симптом обойдён подгонкой (2026-08-27) |
|
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
||||||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | открыт: замер есть, лечение — удешевить кадр |
|
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
||||||
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1206,7 +1206,15 @@ fade_in по построению пишет все 256 записей из сн
|
|||||||
---
|
---
|
||||||
|
|
||||||
<a id="snd-pace-dead"></a>
|
<a id="snd-pace-dead"></a>
|
||||||
## SND-PACE-DEAD. Пейсинг сцен по насосу CBL не включается
|
## SND-PACE-DEAD. Пейсинг сцен по насосу CBL не включается — СНЯТ 2026-08-28
|
||||||
|
|
||||||
|
> **Закрыт удалением самой ветки.** Сцена с Джафаром переведена на ЕДИНЫЕ
|
||||||
|
> часы — кадры луча (`intro_pv_animated`): вторая шкала по насосу и гонка,
|
||||||
|
> которая её выбирала, убраны целиком. Насос как часы был нужен потому,
|
||||||
|
> что кадр сцены рисовался дольше своего интервала; теперь отрисовка
|
||||||
|
> разложена по интервалам (`pv_restore_bg`), и счёт кадров честен.
|
||||||
|
> Побочный эффект гонки — «кода реплики каждый прогон в другом месте» —
|
||||||
|
> исчез; проверено пользователем. Ниже — исходный разбор.
|
||||||
|
|
||||||
**Замер (MAME, 2026-08-27).** В сцене `intro_pv_scene()` зонды показали
|
**Замер (MAME, 2026-08-27).** В сцене `intro_pv_scene()` зонды показали
|
||||||
`snd_done == 0` на всём прогоне — то есть ветка `if (snd_pace)`, ради
|
`snd_done == 0` на всём прогоне — то есть ветка `if (snd_pace)`, ради
|
||||||
@@ -1237,7 +1245,17 @@ snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально и
|
|||||||
---
|
---
|
||||||
|
|
||||||
<a id="pv-render-bound"></a>
|
<a id="pv-render-bound"></a>
|
||||||
## PV-RENDER-BOUND. Сцена с принцессой не укладывается в свой бюджет кадра
|
## PV-RENDER-BOUND. Сцена с принцессой не укладывается в свой бюджет кадра — ИСПРАВЛЕНО 2026-08-28
|
||||||
|
|
||||||
|
> **Причина оказалась не в цене кадра, а в том, ГДЕ отсчитывался интервал.**
|
||||||
|
> Кадр рисовался целиком, и только потом ждали vsync и ещё четыре — то есть
|
||||||
|
> отрисовка ПРИБАВЛЯЛАСЬ к делителю. Теперь она разложена на блоки, каждый
|
||||||
|
> из которых влезает в кадровый интервал, и после каждого ждём vsync
|
||||||
|
> (`pv_restore_bg` + раскладка кадра в `cut_run` / `pre_room_animated` /
|
||||||
|
> `intro_pv_draw_frame`); фон восстанавливаем только по картинке, 200 строк
|
||||||
|
> вместо 256. Удешевлять сцену не понадобилось: отрисовка ~2 кадра при
|
||||||
|
> бюджете 5. Подгонка `PV_MAGIC_LEAD` снята (0) — проверено пользователем:
|
||||||
|
> кода попадает в нужный кадр. Ниже — исходный замер.
|
||||||
|
|
||||||
**Замер (MAME, 2026-08-27).** Реплика Джафара `m53` — 1403 порции насоса,
|
**Замер (MAME, 2026-08-27).** Реплика Джафара `m53` — 1403 порции насоса,
|
||||||
то есть 16,42 с реального времени. За это время шкала сцены прошла
|
то есть 16,42 с реального времени. За это время шкала сцены прошла
|
||||||
|
|||||||
@@ -89,3 +89,47 @@ raw-клавиатура (`<kbd_raw.h>`), которую мы и так заби
|
|||||||
после выхода — заметный. Делать после того, как закроем стражей и
|
после выхода — заметный. Делать после того, как закроем стражей и
|
||||||
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||||||
возможный возврат `pop_bg` в резидент `--w3`).
|
возможный возврат `pop_bg` в резидент `--w3`).
|
||||||
|
|
||||||
|
## Готовить следующий уровень, пока играет мелодия конца текущего
|
||||||
|
|
||||||
|
**САМАЯ ДАЛЬНЯЯ ВЕРСИЯ.** Не полишинг и не порт: это улучшение ПРОТИВ
|
||||||
|
оригинала. Планируем, но не раньше, чем закроем уровни и полишинг.
|
||||||
|
|
||||||
|
**Идея (пользователь, 2026-08-28).** Пока звучит мелодия конца уровня (и
|
||||||
|
трек заставки между уровнями), экран не меняется — значит в это время можно
|
||||||
|
успеть прочитать с диска следующий уровень, чтобы после музыки он появлялся
|
||||||
|
сразу, а не через секунду-другую загрузки.
|
||||||
|
|
||||||
|
**Что УЖЕ сделано и мерено (2026-08-28).** Половина этого уже работает:
|
||||||
|
трек заставки играет ПОВЕРХ загрузки уровня — так же, как в оригинале
|
||||||
|
(`seg003:68-108`: `load_intro` возвращает управление, `load_level()` идёт под
|
||||||
|
музыку, и только потом `while (check_sound_playing())` на чёрном экране).
|
||||||
|
Насос это переживает: 500 подряд вызовов `pop_sfx_fill` через всю загрузку —
|
||||||
|
максимальный зазор 245 832 такта при дедлайне 251 000, ни одной пропущенной
|
||||||
|
порции. Осталась вторая половина: мелодия конца уровня досиживается на
|
||||||
|
ЖИВОМ экране пройденного уровня (как в оригинале, `play_level_2`), и только
|
||||||
|
после неё начинается загрузка.
|
||||||
|
|
||||||
|
**Оговорки, найденные при разборе.**
|
||||||
|
|
||||||
|
1. **Загрузку придётся разрезать надвое.** Сейчас `pop_level_switch` мешает
|
||||||
|
дисковую работу (файл уровня в EMM-страницу, атласы стража и тайлсета) с
|
||||||
|
экранно-палитровой (`pop_bg_load` переписывает записи палитры и атласы,
|
||||||
|
`pop_guard_load` физически применяет цветовые слоты, обе страницы
|
||||||
|
заливаются, `pop_pal_black`). Вторую половину НЕЛЬЗЯ выполнять, пока на
|
||||||
|
экране ещё живёт пройденный уровень — иначе палитра поедет прямо на
|
||||||
|
картинке. То есть в «музыкальное окно» можно вынести только диск, а всё,
|
||||||
|
что трогает палитру и VRAM, остаётся после.
|
||||||
|
2. **Бюджет кадра.** Ожидание мелодии идёт в игровом цикле (гейт
|
||||||
|
`pop_endmus_left` + `pop_music_busy()`), кадр там обычный, 80-100 мс.
|
||||||
|
Шаг подкачки должен быть такого же размера, как музыкальный — полстраницы
|
||||||
|
(~16 мс), а не страница целиком; иначе кадры на экране пройденного уровня
|
||||||
|
начнут дёргаться.
|
||||||
|
3. **Память.** Страницы следующего уровня придётся держать одновременно со
|
||||||
|
страницами текущего — проверить EMM-бюджет (memory `sprinter_emm_budget`),
|
||||||
|
тяжёлые тут не файл уровня, а атласы тайлсета и стража.
|
||||||
|
4. **Риск невелик, но он в самом хрупком месте.** Палитро-атласный обмен —
|
||||||
|
источник уже пойманных багов (`PAL-DUNGEON-STALE`, `PAL-L1-AFTER-INTRO`),
|
||||||
|
и трогать его ради экономии секунды стоит только на спокойную голову.
|
||||||
|
|
||||||
|
**Выигрыш.** Одна-две секунды один раз на уровень.
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* _pop_file.h — внутренний файловый слой SprPoP для старых версий DSS.
|
||||||
|
*
|
||||||
|
* DSS до исправления относительных путей оставляет рабочий каталог внутри
|
||||||
|
* последнего подкаталога. POP_PATH_CALL восстанавливает абсолютный каталог
|
||||||
|
* приложения до и после операции. Это макрос намеренно: код исполняется в
|
||||||
|
* банке вызывающего, где доступен переданный указатель, и не расходует W1.
|
||||||
|
*/
|
||||||
|
#ifndef _POP_FILE_H
|
||||||
|
#define _POP_FILE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
/* BANK10: получить каталог EXE через APPINFO B=1 и сделать его текущим. */
|
||||||
|
int8_t pop_path_init(void) __banked;
|
||||||
|
int8_t pop_chdir_home(void) __banked;
|
||||||
|
|
||||||
|
/* expression вычисляется только после успешного chdir. errno файловой
|
||||||
|
* операции сохраняется через завершающее восстановление каталога. */
|
||||||
|
#define POP_PATH_CALL(result, expression) do { \
|
||||||
|
if (pop_chdir_home() != 0) { \
|
||||||
|
(result) = -1; \
|
||||||
|
} else { \
|
||||||
|
int _pop_path_errno; \
|
||||||
|
(result) = (expression); \
|
||||||
|
_pop_path_errno = errno; \
|
||||||
|
(void)pop_chdir_home(); \
|
||||||
|
errno = _pop_path_errno; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
|
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
|
||||||
* же банке, что и код, который её читает (см. pop_arc.h). */
|
* же банке, что и код, который её читает (см. pop_arc.h). */
|
||||||
@@ -42,7 +43,7 @@ int8_t pop_arc_open(pop_arc_t *a, const char *path,
|
|||||||
|
|
||||||
a->fd = -1;
|
a->fd = -1;
|
||||||
a->count = 0;
|
a->count = 0;
|
||||||
fd = open(path, O_RDONLY);
|
POP_PATH_CALL(fd, open(path, O_RDONLY));
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
||||||
memcmp(head, "PBA1", 4) != 0 ||
|
memcmp(head, "PBA1", 4) != 0 ||
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "pop_cheat.h"
|
#include "pop_cheat.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "_pop_config.h"
|
#include "_pop_config.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
#define CFG_BASE 0x100
|
#define CFG_BASE 0x100
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ static uint8_t cfg_read_file(uint8_t *buf, uint16_t *size)
|
|||||||
int fd, got;
|
int fd, got;
|
||||||
long n;
|
long n;
|
||||||
|
|
||||||
fd = open("POP.CFG", O_RDONLY);
|
POP_PATH_CALL(fd, open("POP.CFG", O_RDONLY));
|
||||||
if (fd < 0) return 0;
|
if (fd < 0) return 0;
|
||||||
n = lseek(fd, 0, SEEK_END);
|
n = lseek(fd, 0, SEEK_END);
|
||||||
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
|
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
|
||||||
@@ -172,7 +173,7 @@ int8_t pop_cfg_load(void) __banked
|
|||||||
|
|
||||||
if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
|
if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
|
||||||
if (buf[9] != POP_PROFILE_VANILLA)
|
if (buf[9] != POP_PROFILE_VANILLA)
|
||||||
printf("POP.CFG: ENHANCED profile -> VANILLA\n");
|
puts("POP.CFG: ENHANCED profile -> VANILLA\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ int8_t pop_cfg_save(void) __banked
|
|||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
|
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
|
||||||
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
|
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
|
||||||
rc = bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf));
|
POP_PATH_CALL(rc, bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf)));
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
return (rc == sizeof(buf)) ? 1 : 0;
|
return (rc == sizeof(buf)) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "pop_title.h" /* титульная картинка после ввода имени */
|
#include "pop_title.h" /* титульная картинка после ввода имени */
|
||||||
#include "pop_music.h" /* подкачка потоковой темы победы */
|
#include "pop_music.h" /* подкачка потоковой темы победы */
|
||||||
#include "pop_hof.h"
|
#include "pop_hof.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
||||||
#define HOF_COUNT 6
|
#define HOF_COUNT 6
|
||||||
@@ -162,7 +163,7 @@ static void hof_load(void)
|
|||||||
if (!blk) return;
|
if (!blk) return;
|
||||||
page = mem_get_page(blk, 0);
|
page = mem_get_page(blk, 0);
|
||||||
gfx_w0_page_prepare(page);
|
gfx_w0_page_prepare(page);
|
||||||
got = bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
|
POP_PATH_CALL(got, bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
|
||||||
if (got == HOF_SIZE) {
|
if (got == HOF_SIZE) {
|
||||||
gfx_w0_map(page);
|
gfx_w0_map(page);
|
||||||
for (i = 0; i < HOF_SIZE; i++)
|
for (i = 0; i < HOF_SIZE; i++)
|
||||||
@@ -187,7 +188,11 @@ static void hof_save(void)
|
|||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
/* Неудача записи не меняет только что показанную таблицу: следующее
|
/* Неудача записи не меняет только что показанную таблицу: следующее
|
||||||
* открытие разумно начнёт с прежнего файла или с пустого списка. */
|
* открытие разумно начнёт с прежнего файла или с пустого списка. */
|
||||||
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
|
{
|
||||||
|
int saved;
|
||||||
|
POP_PATH_CALL(saved,
|
||||||
|
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
|
||||||
|
}
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+144
-111
@@ -83,26 +83,24 @@ enum {
|
|||||||
PV_MUS_4_LOAD = PV_DIALOG1_START + 54, /* подкачка m52 */
|
PV_MUS_4_LOAD = PV_DIALOG1_START + 54, /* подкачка m52 */
|
||||||
PV_WALK2_START = PV_DIALOG1_START + 176,
|
PV_WALK2_START = PV_DIALOG1_START + 176,
|
||||||
PV_DIALOG2_START = PV_DIALOG1_START + 416,
|
PV_DIALOG2_START = PV_DIALOG1_START + 416,
|
||||||
/* ЗАКЛИНАНИЕ СДВИНУТО РАНЬШЕ СЦЕНАРИЯ — ПОДГОНКА ПОД МУЗЫКУ.
|
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
|
||||||
*
|
*
|
||||||
* Сцена render-bound: кадр рисуется дороже своего бюджета, кадровая
|
* История: сцена была render-bound — кадр рисовался дороже своего
|
||||||
* ветка пейсинга догона не имеет, и анимация идёт ~49 тиков в секунду
|
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
|
||||||
* вместо 60. Реплика Джафара при этом играет по РЕАЛЬНОМУ времени и
|
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
|
||||||
* успевает кончиться, когда шкала стоит на 1854 вместо 2028 (замер в
|
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
|
||||||
* MAME 2026-08-27 зондом на конец трека: длина насосу отдана полная,
|
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
|
||||||
* 1403 порции, курсор дошёл до последней страницы — трек доигрывает
|
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
|
||||||
* целиком, отстаёт именно картинка).
|
* 100 тиков.
|
||||||
*
|
*
|
||||||
* Из-за этого кода реплики приходила примерно на секунду раньше
|
* Причину убрали: отрисовка разложена по кадровым интервалам
|
||||||
* молнии. Честное лечение — удешевить кадр сцены; до тех пор двигаем
|
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
|
||||||
* жест целиком (замах, шаг назад, вспышка), чтобы он не сломался
|
* и подкачка трека оплачивается интервалом (pop_music_load_step).
|
||||||
* внутри себя. Величина подобрана НА СЛУХ и уточняется прогоном:
|
* После этого подгонка стала вредной: молния била больше чем на
|
||||||
* расчётные полсотни тиков (секунда отставания при ~49 тиках/с) на
|
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
|
||||||
* слух почти не изменили картины, поэтому взято вдвое больше.
|
* оставляем на месте — если запись другого набора (mt32/ogg) снова
|
||||||
*
|
* разъедется, крутить надо её, а не тайминги сценария. */
|
||||||
* Эффектов дверей это не касается: у них и звук, и картинка идут по
|
PV_MAGIC_LEAD = 0,
|
||||||
* одной и той же шкале и разъехаться не могут. */
|
|
||||||
PV_MAGIC_LEAD = 100,
|
|
||||||
PV_RAISE_START = PV_DIALOG1_START + 696 - PV_MAGIC_LEAD,
|
PV_RAISE_START = PV_DIALOG1_START + 696 - PV_MAGIC_LEAD,
|
||||||
PV_STEPBACK_START = PV_DIALOG1_START + 703 - PV_MAGIC_LEAD,
|
PV_STEPBACK_START = PV_DIALOG1_START + 703 - PV_MAGIC_LEAD,
|
||||||
PV_MAGIC_START = PV_DIALOG1_START + 822 - PV_MAGIC_LEAD,
|
PV_MAGIC_START = PV_DIALOG1_START + 822 - PV_MAGIC_LEAD,
|
||||||
@@ -430,6 +428,44 @@ static int intro_pv_draw_base(void)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ВОССТАНОВЛЕНИЕ ФОНА ЗАДНЕЙ СТРАНИЦЫ — ДВУМЯ БЛОКАМИ ПО КАДРОВОМУ ИНТЕРВАЛУ.
|
||||||
|
*
|
||||||
|
* Персонажи и живой фон рисуются банком спрайтов и в ОЗУ-копию не оседают,
|
||||||
|
* поэтому «стереть прошлый кадр» = скопировать чистый фон из теневой копии
|
||||||
|
* соседней страницы. Раньше это была ОДНА копия на весь экран, и она стоила
|
||||||
|
* ~547 000 тактов = 1,27 кадра луча (модель blit_cost_model). Хвост кадра
|
||||||
|
* сцены ждал vsync ПОСЛЕ отрисовки, так что эти 1,27 кадра ПРИБАВЛЯЛИСЬ к
|
||||||
|
* пяти интервалам делителя: сцена шла 6+ кадров вместо 5, то есть ~8,2 fps
|
||||||
|
* вместо десяти (замер 49/60 тиков — docs/BUGS_OPEN.md PV-RENDER-BOUND).
|
||||||
|
*
|
||||||
|
* Лечение — не «удешевить», а РАЗЛОЖИТЬ по интервалам: каждый блок заведомо
|
||||||
|
* влезает в кадр, и после него честно ждём vsync. Тогда отрисовка живёт
|
||||||
|
* ВНУТРИ делителя, а не сверх него.
|
||||||
|
* - копируем только картинку (200 строк с POP_YOFF), а не все 256:
|
||||||
|
* сверху и снизу чёрная рамка, туда никто не рисует;
|
||||||
|
* - двумя половинами по 100 строк: ~219 000 тактов каждая, чуть больше
|
||||||
|
* половины кадра — запас двукратный.
|
||||||
|
*
|
||||||
|
* ТРАТИТ РОВНО ДВА кадровых интервала (по gfx_wait_vsync на блок) — это
|
||||||
|
* учтено в раскладке кадра у всех трёх сцен (см. вызовы). */
|
||||||
|
#define PV_BG_Y0 POP_YOFF
|
||||||
|
#define PV_BG_H 200
|
||||||
|
|
||||||
|
static void pv_restore_bg(void)
|
||||||
|
{
|
||||||
|
gfx_rect_t r;
|
||||||
|
|
||||||
|
r.x = 0;
|
||||||
|
r.w = GFX_WIDTH;
|
||||||
|
r.h = PV_BG_H / 2;
|
||||||
|
r.y = PV_BG_Y0;
|
||||||
|
gfx_copy_page(&r, GFX_COPY_DIRECT);
|
||||||
|
gfx_wait_vsync();
|
||||||
|
r.y = PV_BG_Y0 + PV_BG_H / 2;
|
||||||
|
gfx_copy_page(&r, GFX_COPY_DIRECT);
|
||||||
|
gfx_wait_vsync();
|
||||||
|
}
|
||||||
|
|
||||||
static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor)
|
static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor)
|
||||||
{
|
{
|
||||||
const uint8_t *img;
|
const uint8_t *img;
|
||||||
@@ -701,12 +737,10 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
|||||||
uint8_t sand_frame, uint8_t flash,
|
uint8_t sand_frame, uint8_t flash,
|
||||||
uint16_t tick)
|
uint16_t tick)
|
||||||
{
|
{
|
||||||
gfx_rect_t full;
|
|
||||||
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
|
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
|
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
|
||||||
pv_blit_floor(torch, torch_left, 93, 116);
|
pv_blit_floor(torch, torch_left, 93, 116);
|
||||||
@@ -730,7 +764,7 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
|||||||
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
|
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
|
||||||
* правой передней колонной. */
|
* правой передней колонной. */
|
||||||
pv_blit_floor(hourglass, 0, 240, 167);
|
pv_blit_floor(hourglass, 0, 240, 167);
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync(); /* блок 3: актёры и декорации */
|
||||||
gfx_set_visible_page(back);
|
gfx_set_visible_page(back);
|
||||||
if (flash) {
|
if (flash) {
|
||||||
uint8_t pulse;
|
uint8_t pulse;
|
||||||
@@ -743,9 +777,9 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
|||||||
pop_pal_flash_white(0);
|
pop_pal_flash_white(0);
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync();
|
||||||
}
|
}
|
||||||
return 11;
|
return 13; /* три интервала кадра + десять на молнию */
|
||||||
}
|
}
|
||||||
return 1;
|
return 3; /* сколько кадровых интервалов съели */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
|
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
|
||||||
@@ -759,12 +793,7 @@ static int intro_pv_animated(void)
|
|||||||
uint16_t tick;
|
uint16_t tick;
|
||||||
uint16_t frame_end;
|
uint16_t frame_end;
|
||||||
uint16_t render_tick;
|
uint16_t render_tick;
|
||||||
uint16_t beam = 0; /* запасной путь: кадров луча ждали */
|
uint16_t beam = 0; /* кадров луча потрачено с начала сцены */
|
||||||
uint16_t snd_done = 0; /* тиков насоса прошло с начала сцены */
|
|
||||||
uint16_t snd_want = 0; /* сколько их должно было пройти */
|
|
||||||
uint16_t snd_acc = 0; /* дробный остаток пересчёта 57/40 */
|
|
||||||
uint8_t snd_prev = 0;
|
|
||||||
uint8_t snd_pace = 0;
|
|
||||||
uint8_t period;
|
uint8_t period;
|
||||||
uint8_t anim_step = 0;
|
uint8_t anim_step = 0;
|
||||||
uint8_t torch_left = 1;
|
uint8_t torch_left = 1;
|
||||||
@@ -813,18 +842,22 @@ static int intro_pv_animated(void)
|
|||||||
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
||||||
* бандами по 16 строк. */
|
* бандами по 16 строк. */
|
||||||
(void)pop_sfx_start();
|
(void)pop_sfx_start();
|
||||||
/* ЧЕМ МЕРИТЬ ВРЕМЯ. Пока сцена была немой, кадры просто считали
|
/* ЧЕМ МЕРИТЬ ВРЕМЯ — ЕДИНЫЕ ЧАСЫ: КАДРЫ ЛУЧА.
|
||||||
* ожидания vsync — и этого хватало. С музыкой выяснилось, что так
|
*
|
||||||
* считаются НАШИ ожидания, а не прошедшее время: полноэкранная копия
|
* Была вторая шкала — тики насоса CBL, и выбор между ними делался
|
||||||
* страницы (~0,55 млн тактов) съедает больше кадра луча, и сцена
|
* гонкой на старте сцены (одна выборка `pop_snd_tick` через кадр:
|
||||||
* незаметно отстаёт — в MAME молния била уже ПОСЛЕ конца реплики.
|
* успел ли диск раскрутить звук). Значит от прогона к прогону сцена
|
||||||
* Часы берём у насоса CBL: он идёт от расхода буфера железом, 85,4 Гц,
|
* шла то по одним часам, то по другим, и кода реплики приходилась
|
||||||
* и ему безразлично, чем занят главный цикл. Если звук выключен,
|
* каждый раз на другое место картинки (наблюдение пользователя
|
||||||
* часы стоят — тогда работает прежний путь по кадрам луча. */
|
* 2026-08-28; баг SND-PACE-DEAD). Насос как часы был нужен потому,
|
||||||
snd_prev = pop_snd_tick;
|
* что кадр сцены рисовался ДОЛЬШЕ своего интервала и счёт наших
|
||||||
gfx_wait_vsync();
|
* ожиданий врал; теперь отрисовка разложена на блоки по кадровому
|
||||||
snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально идут? */
|
* интервалу (см. pv_restore_bg), и счёт кадров луча честен.
|
||||||
snd_prev = pop_snd_tick;
|
*
|
||||||
|
* Осталось учесть единственную работу, которая в интервал не влезала
|
||||||
|
* сама, — подкачку страницы трека: шаг читает ПОЛСТРАНИЦЫ (~16 мс,
|
||||||
|
* pop_music_load_step) и оплачивается ровно одним интервалом, как
|
||||||
|
* любой другой блок кадра. */
|
||||||
result = 0;
|
result = 0;
|
||||||
princess_actor.x = 120;
|
princess_actor.x = 120;
|
||||||
princess_actor.frame = 11; /* Pstand, facing left */
|
princess_actor.frame = 11; /* Pstand, facing left */
|
||||||
@@ -892,59 +925,42 @@ static int intro_pv_animated(void)
|
|||||||
torch_right, sand_frame, flash,
|
torch_right, sand_frame, flash,
|
||||||
render_tick);
|
render_tick);
|
||||||
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
||||||
|
|
||||||
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
||||||
*
|
*
|
||||||
* Ниже страница следующей реплики читается в ПАУЗЕ кадра, и это
|
* Раньше страница следующей реплики читалась в ПАУЗЕ кадра, и это
|
||||||
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
||||||
* нет: кадр дорогой, шкала работает по принципу «долг не догоняем»
|
* не было: кадр дорогой, шкала работает по принципу «долг не
|
||||||
* (snd_want подтягивается к snd_done), и цикл ожидания выходит
|
* догоняем», и цикл ожидания выходил сразу. Замер в MAME
|
||||||
* сразу. Замер в MAME 2026-08-26: ОДНА страница за семь секунд,
|
* 2026-08-26: ОДНА страница за семь секунд, то есть одиннадцать
|
||||||
* то есть одиннадцать страниц m53 не успевали за всю сцену —
|
* страниц m53 не успевали за всю сцену — pop_music_play() на своём
|
||||||
* pop_music_play() на своём тике не находил готового трека, и
|
* тике не находил готового трека, и реплики Джафара пропадали
|
||||||
* реплики Джафара пропадали совсем.
|
* совсем (memory pv_music_stall_regression).
|
||||||
*
|
*
|
||||||
* Страница стоит 33 мс против кадра сцены в 100-133 мс, поэтому
|
* Поэтому шаг безусловный — и оплачен как обычный блок кадра: он
|
||||||
* один безусловный шаг в кадр сцену не ломает: он лишь тратит
|
* читает полстраницы (~16 мс), а следующий gfx_wait_vsync ставит
|
||||||
* часть той же паузы, а если паузы не было — добавляет к кадру
|
* нас на край интервала, который мы этим и заняли. */
|
||||||
* четверть его длины, и шкала это учтёт сама. */
|
if (pop_music_loading()) {
|
||||||
if (pop_music_loading()) (void)pop_music_load_step();
|
(void)pop_music_load_step();
|
||||||
|
gfx_wait_vsync();
|
||||||
|
beam++;
|
||||||
|
}
|
||||||
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
||||||
* целиком, вместе с принцессой и факелами. */
|
* целиком, вместе с принцессой и факелами. */
|
||||||
if (!faded_in) {
|
if (!faded_in) {
|
||||||
faded_in = 1;
|
faded_in = 1;
|
||||||
if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
||||||
}
|
}
|
||||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
|
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр сцены — 6/8/7 тиков по
|
||||||
* 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425
|
* 1/60 с (pv_seq_period), в кадры луча переводит POP_T60, и цель
|
||||||
* повторяет это с точностью 0,07 % (30 мс на всю сцену) без
|
* КУМУЛЯТИВНА: нецелые 6,67 и 5,83 не копят ошибку.
|
||||||
* деления в кадре и без 32-битной арифметики. */
|
*
|
||||||
if (snd_pace) {
|
* ДОЛГ НЕ ДОГОНЯЕМ: если кадр вышел дороже своего бюджета, цикл
|
||||||
snd_acc += (uint16_t)period * 57u;
|
* ожидания просто не сработает, но beam уже учёл потраченное — и
|
||||||
while (snd_acc >= 40u) { snd_acc -= 40u; snd_want++; }
|
* следующая цель считается от той же общей шкалы. Наверстывать
|
||||||
for (;;) {
|
* пропущенное нельзя: события соседних тиков (створка ворот, дверь
|
||||||
uint8_t now = pop_snd_tick;
|
* покоев) слиплись бы в один кадр, и один из звуков пропал бы. */
|
||||||
|
{
|
||||||
/* Разность в uint8_t: счётчик переполняется каждые 3 с, а
|
|
||||||
* заглядываем мы в него не реже раза в кадр. */
|
|
||||||
snd_done += (uint8_t)(now - snd_prev);
|
|
||||||
snd_prev = now;
|
|
||||||
if (snd_done >= snd_want) break;
|
|
||||||
/* Страницу следующей реплики читаем ЗДЕСЬ, в паузе кадра:
|
|
||||||
* 33 мс диска укладываются в ожидание (кадр сцены — 133 мс),
|
|
||||||
* и подкачка не добавляется к длительности кадра, а
|
|
||||||
* растворяется в ней. */
|
|
||||||
if (pop_music_loading()) (void)pop_music_load_step();
|
|
||||||
else gfx_wait_vsync();
|
|
||||||
}
|
|
||||||
/* ДОЛГ НЕ ДОГОНЯЕМ. Подкачка следующей реплики (страница — 33 мс)
|
|
||||||
* и любой тяжёлый кадр оставляют snd_done впереди snd_want, и
|
|
||||||
* дальше цикл гнал кадры без единого ожидания: пламя факелов
|
|
||||||
* мелькало, а события соседних тиков (створка ворот и дверь
|
|
||||||
* покоев) слипались в один кадр — заявка перезаписывала заявку, и
|
|
||||||
* один из звуков пропадал. Тот же принцип, что у pop_pace_end:
|
|
||||||
* якорь ставится ПО ФАКТУ, пропущенное время не наверстывается. */
|
|
||||||
if (snd_done > snd_want) snd_want = snd_done;
|
|
||||||
} else {
|
|
||||||
uint16_t target = POP_T60(tick);
|
uint16_t target = POP_T60(tick);
|
||||||
|
|
||||||
while (beam < target) { gfx_wait_vsync(); beam++; }
|
while (beam < target) { gfx_wait_vsync(); beam++; }
|
||||||
@@ -995,7 +1011,6 @@ static uint8_t pre_hourglass_state(void)
|
|||||||
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||||
{
|
{
|
||||||
atlas_t hourglass, torch;
|
atlas_t hourglass, torch;
|
||||||
gfx_rect_t full;
|
|
||||||
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
|
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
|
||||||
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
|
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
|
||||||
intro_skip_t skip;
|
intro_skip_t skip;
|
||||||
@@ -1025,7 +1040,6 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
|||||||
|
|
||||||
pop_screen_present(1);
|
pop_screen_present(1);
|
||||||
result = 0;
|
result = 0;
|
||||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
|
||||||
intro_skip_begin(&skip);
|
intro_skip_begin(&skip);
|
||||||
for (frame = 0; frame < frames; frame++) {
|
for (frame = 0; frame < frames; frame++) {
|
||||||
uint8_t back, hold, star;
|
uint8_t back, hold, star;
|
||||||
@@ -1033,7 +1047,7 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
|||||||
if (intro_skip_requested(&skip)) { result = 1; break; }
|
if (intro_skip_requested(&skip)) { result = 1; break; }
|
||||||
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
pv_blit_floor(&torch, torch_left, 93, 116);
|
pv_blit_floor(&torch, torch_left, 93, 116);
|
||||||
pv_blit_floor(&torch, torch_right, 211, 116);
|
pv_blit_floor(&torch, torch_right, 211, 116);
|
||||||
@@ -1046,10 +1060,11 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
|||||||
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
|
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
|
||||||
}
|
}
|
||||||
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
|
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync(); /* блок 3: живой фон */
|
||||||
gfx_set_visible_page(back);
|
gfx_set_visible_page(back);
|
||||||
/* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */
|
/* Три интервала уже потрачены на блоки отрисовки — добираем до
|
||||||
for (hold = 1; hold < 5; hold++) gfx_wait_vsync();
|
* пяти (5/50 с = кадр катсцены оригинала). */
|
||||||
|
for (hold = 3; hold < 5; hold++) gfx_wait_vsync();
|
||||||
|
|
||||||
/* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по
|
/* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по
|
||||||
* пустой комнате, а часы и пламя возникают уже на свету. */
|
* пустой комнате, а часы и пламя возникают уже на свету. */
|
||||||
@@ -1128,8 +1143,14 @@ static void intro_restore_game_palette(void)
|
|||||||
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
|
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
|
||||||
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
|
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
|
||||||
#define CUT_PV2_SPLIT 17
|
#define CUT_PV2_SPLIT 17
|
||||||
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен
|
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен: у
|
||||||
* (cutscene_frame_time SDLPoP; см. pre_room_animated). */
|
* оригинала `cutscene_frame_time = 6` тиков по 1/60 с (reset_cutscene,
|
||||||
|
* seg001:527, и его зовёт load_intro прямо перед сценой) = 100 мс, у нас
|
||||||
|
* 5 кадров луча по 50 Гц = те же 100 мс.
|
||||||
|
*
|
||||||
|
* РАСКЛАДКА ПО ИНТЕРВАЛАМ (иначе отрисовка прибавлялась к делителю, а не
|
||||||
|
* жила внутри него — см. pv_restore_bg): фон половинами 1-2, актёры и
|
||||||
|
* декорации 3, служебный блок (звук/подкачка музыки) 4-5. */
|
||||||
#define CUT_FRAME_VSYNC 5
|
#define CUT_FRAME_VSYNC 5
|
||||||
|
|
||||||
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
|
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
|
||||||
@@ -1290,11 +1311,9 @@ static void cut_shad_init(uint8_t x, uint8_t y, uint8_t seq)
|
|||||||
* прервал сцену (любое НОВОЕ нажатие). */
|
* прервал сцену (любое НОВОЕ нажатие). */
|
||||||
static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
||||||
{
|
{
|
||||||
gfx_rect_t full;
|
|
||||||
uint16_t i;
|
uint16_t i;
|
||||||
uint8_t hold;
|
uint8_t hold;
|
||||||
|
|
||||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
|
||||||
for (i = 0; i < frames; i++) {
|
for (i = 0; i < frames; i++) {
|
||||||
uint8_t back;
|
uint8_t back;
|
||||||
kframe kid_fr, pr_fr;
|
kframe kid_fr, pr_fr;
|
||||||
@@ -1316,8 +1335,9 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
|||||||
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
|
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
|
||||||
* банком спрайтов и потому в неё не оседают. */
|
* банком спрайтов и потому в неё не оседают. Двумя блоками по
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
* кадровому интервалу — см. pv_restore_bg. */
|
||||||
|
pv_restore_bg(); /* блоки 1-2 */
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
|
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
|
||||||
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
|
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
|
||||||
@@ -1339,12 +1359,17 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
|||||||
* сквозь неё — поймано пользователем 2026-08-25). */
|
* сквозь неё — поймано пользователем 2026-08-25). */
|
||||||
pv_blit_floor(&cut_hg, 0, 240, 167);
|
pv_blit_floor(&cut_hg, 0, 240, 167);
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync(); /* блок 3: актёры и декорации */
|
||||||
gfx_set_visible_page(back);
|
gfx_set_visible_page(back);
|
||||||
for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
|
|
||||||
|
|
||||||
|
/* Блок 4 — служебный, и он НЕ добавляется к кадру, а живёт внутри
|
||||||
|
* оставшихся интервалов: подкачка страницы трека стоит ~33 мс, то
|
||||||
|
* есть меньше двух интервалов, которые всё равно надо переждать.
|
||||||
|
* Порядок как в сцене с Джафаром: шаг подкачки — безусловный (см.
|
||||||
|
* memory pv_music_stall_regression). */
|
||||||
pop_sfx_tick();
|
pop_sfx_tick();
|
||||||
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
|
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
|
||||||
|
for (hold = 3; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
|
||||||
cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9);
|
cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9);
|
||||||
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
|
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
|
||||||
cut_sand = (uint8_t)((cut_sand + 1) % 3);
|
cut_sand = (uint8_t)((cut_sand + 1) % 3);
|
||||||
@@ -1662,7 +1687,9 @@ uint8_t pop_intro_show(void) __banked
|
|||||||
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
|
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
|
||||||
* дальше внутри сцены остаётся только пуск, а следующие две реплики
|
* дальше внутри сцены остаётся только пуск, а следующие две реплики
|
||||||
* подкачиваются по странице за кадр. */
|
* подкачиваются по странице за кадр. */
|
||||||
pop_sfx_pause();
|
/* Глушение снято 2026-08-28: массовое чтение страниц насос не морит
|
||||||
|
* (замер на загрузке уровня — ни одной пропущенной порции), а трек
|
||||||
|
* титров может и должен доигрывать. */
|
||||||
(void)pop_music_load(POP_MUS_STORY_2);
|
(void)pop_music_load(POP_MUS_STORY_2);
|
||||||
pv_result = intro_pv_animated();
|
pv_result = intro_pv_animated();
|
||||||
if (pv_result > 0) {
|
if (pv_result > 0) {
|
||||||
@@ -1740,15 +1767,21 @@ uint8_t pop_intro_show(void) __banked
|
|||||||
* дальше, а не чтобы досидеть до конца трека. */
|
* дальше, а не чтобы досидеть до конца трека. */
|
||||||
static void pre_cut_finish(uint8_t aborted)
|
static void pre_cut_finish(uint8_t aborted)
|
||||||
{
|
{
|
||||||
if (!aborted) {
|
/* ТРЕК ПЕРЕЖИВАЕТ СЦЕНУ — как в оригинале. load_intro (seg001:690)
|
||||||
intro_skip_t tail;
|
* ничего не ждёт: гасит экран и возвращает управление, а звук играет
|
||||||
|
* дальше поверх загрузки уровня и первых секунд игры. Мы вместо этого
|
||||||
intro_skip_begin(&tail);
|
* ДОСИЖИВАЛИ трек на чёрном экране (27 — 10,7 с против 2,6 с картинки),
|
||||||
while (pop_music_busy() && !intro_skip_requested(&tail))
|
* то есть дарили игроку восемь секунд пустого экрана, и только потом
|
||||||
pop_wait_edge();
|
* глушили. ЗАМЕР 2026-08-28: снято вместе с глушением на загрузке
|
||||||
|
* уровня; если загрузка не укладывается в дедлайн насоса (251 000
|
||||||
|
* тактов), вернуть оба места разом.
|
||||||
|
*
|
||||||
|
* Пропуск сцены по-прежнему обрывает и музыку: игрок нажал клавишу,
|
||||||
|
* чтобы идти дальше, а не чтобы дослушать. */
|
||||||
|
if (aborted) {
|
||||||
|
pop_music_free();
|
||||||
|
pop_sfx_pause();
|
||||||
}
|
}
|
||||||
pop_music_free();
|
|
||||||
pop_sfx_pause();
|
|
||||||
intro_restore_game_palette();
|
intro_restore_game_palette();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1756,7 +1789,7 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
|
|||||||
{
|
{
|
||||||
const pop_cs_cmd_t *script = 0;
|
const pop_cs_cmd_t *script = 0;
|
||||||
|
|
||||||
pop_sfx_pause();
|
/* Глушение перед чтением трека снято 2026-08-28 (см. замер насоса). */
|
||||||
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
|
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
|
||||||
* сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
|
* сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
|
||||||
* load_intro просто гасит экран и возвращает управление, а звук
|
* load_intro просто гасит экран и возвращает управление, а звук
|
||||||
@@ -1824,7 +1857,7 @@ int pop_intro_hof_bg(uint8_t page) __banked
|
|||||||
|
|
||||||
void pop_time_expired_show(void) __banked
|
void pop_time_expired_show(void) __banked
|
||||||
{
|
{
|
||||||
pop_sfx_pause();
|
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||||||
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
|
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
|
||||||
if (pop_music_load(36) == 0) {
|
if (pop_music_load(36) == 0) {
|
||||||
(void)pop_sfx_start();
|
(void)pop_sfx_start();
|
||||||
@@ -1853,7 +1886,7 @@ void pop_time_expired_show(void) __banked
|
|||||||
|
|
||||||
void pop_ending_show(void) __banked
|
void pop_ending_show(void) __banked
|
||||||
{
|
{
|
||||||
pop_sfx_pause();
|
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||||||
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
|
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
|
||||||
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
|
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
|
||||||
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
|
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
#include "kid_arc.h"
|
#include "kid_arc.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile,
|
/* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile,
|
||||||
* и только поэтому индекс считается сложением. Если порядок собьётся,
|
* и только поэтому индекс считается сложением. Если порядок собьётся,
|
||||||
@@ -75,7 +76,8 @@ int pop_kid_data_load(const char *path) __banked
|
|||||||
blk=mem_alloc_pages(1);
|
blk=mem_alloc_pages(1);
|
||||||
if (!blk) return -1;
|
if (!blk) return -1;
|
||||||
page=mem_get_page(blk,0);
|
page=mem_get_page(blk,0);
|
||||||
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
|
POP_PATH_CALL(n, bank_load_file(page,KD_DATA_OFF,path,
|
||||||
|
16384-KD_DATA_OFF));
|
||||||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||||
mem_free_block(blk); return -1;
|
mem_free_block(blk); return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
||||||
#include "pop_geom.h"
|
#include "pop_geom.h"
|
||||||
#include "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
|
#include "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
|
||||||
|
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
|
||||||
|
#include "pop_music_ticks.h" /* длина мелодии конца уровня */
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
@@ -36,6 +38,14 @@ int8_t knock;
|
|||||||
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
|
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
|
||||||
* легли сюда же без переделки. */
|
* легли сюда же без переделки. */
|
||||||
uint8_t pop_next_level;
|
uint8_t pop_next_level;
|
||||||
|
/* СКОЛЬКО ЛОГИЧЕСКИХ КАДРОВ ЕЩЁ ДЕРЖАТЬ УРОВЕНЬ, пока играет мелодия конца.
|
||||||
|
* У оригинала (seg003:387, play_level_2) уровень не сменяется, пока
|
||||||
|
* `check_sound_playing()`: экран пройденного уровня живёт с анимацией
|
||||||
|
* факелов ровно столько, сколько звучит трек. У нас признак «звучит» —
|
||||||
|
* pop_music_busy(), но он ложен при ВЫКЛЮЧЕННОЙ музыке, а оригинал
|
||||||
|
* выдерживает паузу и молча. Поэтому здесь взводится счётчик по длине
|
||||||
|
* записи (gen/pop_music_ticks.h), а главный цикл ждёт большего из двух. */
|
||||||
|
uint8_t pop_endmus_left;
|
||||||
|
|
||||||
|
|
||||||
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ *
|
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ *
|
||||||
@@ -358,9 +368,17 @@ void play_seq(void)
|
|||||||
else if (ev == 4) {
|
else if (ev == 4) {
|
||||||
/* seg006:646. На четвёртом уровне уход за дверь — это
|
/* seg006:646. На четвёртом уровне уход за дверь — это
|
||||||
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
|
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
|
||||||
* 15-м оригинал молчит. */
|
* 15-м оригинал молчит. Вместе с треком взводим
|
||||||
if (pop_current_level == 4) pop_sfx_play(32);
|
* задержку смены уровня (см. pop_endmus_left): кадров
|
||||||
else if (pop_current_level != 13) pop_sfx_play(41);
|
* = длина записи в тиках -> кадры луча -> делённое на
|
||||||
|
* длину логического кадра (4 кадра луча вне боя). */
|
||||||
|
if (pop_current_level == 4) {
|
||||||
|
pop_sfx_play(32);
|
||||||
|
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_32) / 4);
|
||||||
|
} else if (pop_current_level != 13) {
|
||||||
|
pop_sfx_play(41);
|
||||||
|
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_41) / 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ extern int8_t knock;
|
|||||||
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
|
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
|
||||||
* там рестарт того же уровня; загрузка следующего — levels_plan.md. */
|
* там рестарт того же уровня; загрузка следующего — levels_plan.md. */
|
||||||
extern uint8_t pop_next_level;
|
extern uint8_t pop_next_level;
|
||||||
|
/* Задержка смены уровня на время мелодии конца (см. pop_kid.c). */
|
||||||
|
extern uint8_t pop_endmus_left;
|
||||||
|
|
||||||
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
||||||
* 0 — OK, -1 — ошибка. */
|
* 0 — OK, -1 — ошибка. */
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ extern uint8_t pop_current_level;
|
|||||||
void pop_level_qsave(pop_qs_io_t *io);
|
void pop_level_qsave(pop_qs_io_t *io);
|
||||||
|
|
||||||
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
||||||
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
|
* `LEVELS\res20NN.bin` относительно каталога EXE и выставить
|
||||||
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
|
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
|
||||||
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
|
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
|
||||||
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
|
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "_pop_level.h"
|
#include "_pop_level.h"
|
||||||
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
|
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
|
||||||
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
|
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
|
||||||
@@ -58,7 +59,8 @@ static int level_read_file(const char *path)
|
|||||||
blk=mem_alloc_pages(1);
|
blk=mem_alloc_pages(1);
|
||||||
if (!blk) return -1;
|
if (!blk) return -1;
|
||||||
page=mem_get_page(blk,0);
|
page=mem_get_page(blk,0);
|
||||||
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
|
POP_PATH_CALL(n, bank_load_file(page,LVL_DATA_OFF,path,
|
||||||
|
16384-LVL_DATA_OFF));
|
||||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||||
gfx_w0_page_prepare(page);
|
gfx_w0_page_prepare(page);
|
||||||
gfx_w0_map(page);
|
gfx_w0_map(page);
|
||||||
@@ -80,18 +82,17 @@ int pop_level_load_num(uint8_t n) __banked
|
|||||||
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
||||||
* резидент W1 форматтер, которого здесь больше негде применить. */
|
* резидент W1 форматтер, которого здесь больше негде применить. */
|
||||||
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
|
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
|
||||||
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
|
|
||||||
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
|
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
|
||||||
|
|
||||||
if (n > POP_LEVEL_LAST) return -1;
|
if (n > POP_LEVEL_LAST) return -1;
|
||||||
pri[12] = alt[8] = (char)('0' + n / 10);
|
pri[12] = (char)('0' + n / 10);
|
||||||
pri[13] = alt[9] = (char)('0' + n % 10);
|
pri[13] = (char)('0' + n % 10);
|
||||||
|
|
||||||
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
||||||
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
||||||
* на текущем уровне вместо падения в пустой уровень. */
|
* на текущем уровне вместо падения в пустой уровень. */
|
||||||
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
||||||
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
|
if (level_read_file(pri) != 0) {
|
||||||
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "pop_settings.h"
|
#include "pop_settings.h"
|
||||||
#include "pop_cfg.h"
|
#include "pop_cfg.h"
|
||||||
#include "pop_qsave.h"
|
#include "pop_qsave.h"
|
||||||
|
#include "pop_music.h" /* pop_music_service — подкачка кольца, см. ui_wait_frame */
|
||||||
#include "pop_status.h"
|
#include "pop_status.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
|
|
||||||
@@ -196,10 +197,21 @@ static uint8_t ui_begin(void)
|
|||||||
gfx_rect_t full;
|
gfx_rect_t full;
|
||||||
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
|
|
||||||
/* Полностраничная accel-копия держит DI дольше половины CBL-буфера.
|
/* ЗВУК ЗДЕСЬ БОЛЬШЕ НЕ ГЛУШИМ (снято 2026-08-28).
|
||||||
* Закрываем вывод, иначе железо повторяет старые 128 байт (скрежет).
|
*
|
||||||
* ui_end откроет его уже с заново заполненной тишиной. */
|
* Был вызов pop_sfx_pause() с обоснованием «полностраничная accel-копия
|
||||||
pop_sfx_pause();
|
* держит DI дольше половины CBL-буфера, иначе железо повторяет старые
|
||||||
|
* 128 байт». Обоснование устарело: лист, который зовёт gfx_copy_page
|
||||||
|
* (_bgi_scroll_rows_raw), режет DI бандами по 16 строк — это ~1,6 мс
|
||||||
|
* против дедлайна насоса 11,7 мс, и окно прерываний между бандами есть.
|
||||||
|
* Тот же полностраничный копир каждый кадр делают игровой цикл и
|
||||||
|
* катсцены, и звук там не рвётся.
|
||||||
|
*
|
||||||
|
* А цена была слышимой: pause закрывает CBL целиком (cbl_close), курсор
|
||||||
|
* трека остаётся на месте, и короткая мелодия начала уровня замирала
|
||||||
|
* ровно на время перерисовки меню — «тормозится на кусочек» (жалоба
|
||||||
|
* пользователя 2026-08-28). У оригинала такого нет и быть не может:
|
||||||
|
* там нет насоса, который надо кормить руками. */
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||||
@@ -207,10 +219,32 @@ static uint8_t ui_begin(void)
|
|||||||
return back;
|
return back;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* КАДР МЕНЮ: перед ожиданием фронта — обслужить музыку.
|
||||||
|
*
|
||||||
|
* Насос CBL живёт в прерывании и главный цикл ему не нужен, НО потоковый
|
||||||
|
* трек (титульная тема играется КОЛЬЦОМ, pop_music.c) дочитывается с диска
|
||||||
|
* именно из pop_music_service: очередная страница попадает в слот, который
|
||||||
|
* насос уже прошёл. Пока открыто меню, главный цикл стоит, и без этого
|
||||||
|
* вызова кольцо опустошалось за несколько секунд — музыка замолкала до
|
||||||
|
* закрытия меню, а на перемещениях по пунктам это слышно как паузы
|
||||||
|
* (жалоба пользователя 2026-08-28).
|
||||||
|
*
|
||||||
|
* Место — рядом с ожиданием кадра: получается тот же темп 50 раз в
|
||||||
|
* секунду, что и в игровом цикле, и подкачка не чаще, чем нужна. */
|
||||||
|
static void ui_wait_frame(void)
|
||||||
|
{
|
||||||
|
pop_music_service();
|
||||||
|
pop_wait_edge();
|
||||||
|
}
|
||||||
|
|
||||||
static void ui_end(uint8_t back)
|
static void ui_end(uint8_t back)
|
||||||
{
|
{
|
||||||
pop_wait_edge(); /* готовый кадр показать только на фронте */
|
ui_wait_frame(); /* готовый кадр показать только на фронте */
|
||||||
gfx_set_visible_page(back);
|
gfx_set_visible_page(back);
|
||||||
|
/* Парный к снятому pop_sfx_pause (см. ui_begin) вызов оставлен: он
|
||||||
|
* идемпотентен (открытый CBL возвращает 0 сразу) и чинит случай, когда
|
||||||
|
* в меню вошли при закрытом выводе — например сразу после загрузки
|
||||||
|
* уровня, которая глушит звук намеренно. */
|
||||||
(void)pop_sfx_start();
|
(void)pop_sfx_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +261,7 @@ static void ui_wait_release(void)
|
|||||||
kbd_raw_down(KBD_SPACE) ||
|
kbd_raw_down(KBD_SPACE) ||
|
||||||
kbd_raw_down(KBD_UP) || kbd_raw_down(KBD_DOWN) ||
|
kbd_raw_down(KBD_UP) || kbd_raw_down(KBD_DOWN) ||
|
||||||
kbd_raw_down(KBD_LEFT) || kbd_raw_down(KBD_RIGHT))
|
kbd_raw_down(KBD_LEFT) || kbd_raw_down(KBD_RIGHT))
|
||||||
pop_wait_edge();
|
ui_wait_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Действие = Enter ИЛИ Space: в оригинале выбор подтверждается обеими, и
|
/* Действие = Enter ИЛИ Space: в оригинале выбор подтверждается обеими, и
|
||||||
@@ -250,7 +284,7 @@ static void ui_wait_ack(void)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
esc_prev = esc; act_prev = act;
|
esc_prev = esc; act_prev = act;
|
||||||
pop_wait_edge();
|
ui_wait_frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +512,7 @@ static uint8_t ui_confirm(const char *question)
|
|||||||
confirm_draw(question, sel);
|
confirm_draw(question, sel);
|
||||||
menu_sound(MENU_SND_MOVE);
|
menu_sound(MENU_SND_MOVE);
|
||||||
}
|
}
|
||||||
pop_wait_edge();
|
ui_wait_frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,10 +556,12 @@ static uint8_t settings_finish(uint8_t changed)
|
|||||||
int8_t rc;
|
int8_t rc;
|
||||||
if (!changed) return 0;
|
if (!changed) return 0;
|
||||||
pop_settings_apply(1);
|
pop_settings_apply(1);
|
||||||
/* ESTEX может уйти в диск на много CBL-периодов. */
|
/* Звук не глушим: POP.CFG — десятки байт, а на практике HOF пишется
|
||||||
pop_sfx_pause();
|
* под звучащий «won» и насос это переживает (проверка пользователя
|
||||||
|
* 2026-08-28). Прежняя осторожность «ESTEX может уйти в диск на много
|
||||||
|
* CBL-периодов» относилась к загрузке НАБОРА страниц, а не к записи
|
||||||
|
* одного мелкого файла. */
|
||||||
rc = pop_cfg_save();
|
rc = pop_cfg_save();
|
||||||
(void)pop_sfx_start();
|
|
||||||
ui_notice(rc > 0 ? "SETTINGS SAVED" : "SAVE ERROR");
|
ui_notice(rc > 0 ? "SETTINGS SAVED" : "SAVE ERROR");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -609,17 +645,16 @@ static uint8_t settings_process(void)
|
|||||||
settings_draw(sel);
|
settings_draw(sel);
|
||||||
menu_sound(sound);
|
menu_sound(sound);
|
||||||
}
|
}
|
||||||
pop_wait_edge();
|
ui_wait_frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int8_t menu_quickload(void)
|
static int8_t menu_quickload(void)
|
||||||
{
|
{
|
||||||
uint8_t slots;
|
uint8_t slots;
|
||||||
/* probe делает open/close на D:, поэтому насос в это время выключен. */
|
/* probe — это только open/close на D:, данных не читает; глушить насос
|
||||||
pop_sfx_pause();
|
* ради него незачем (см. settings_finish). */
|
||||||
slots = pop_qsave_probe();
|
slots = pop_qsave_probe();
|
||||||
(void)pop_sfx_start();
|
|
||||||
if (slots & POP_QS_SLOT_SAVE) return POP_MENU_QUICKLOAD;
|
if (slots & POP_QS_SLOT_SAVE) return POP_MENU_QUICKLOAD;
|
||||||
if ((slots & POP_QS_SLOT_BACKUP) && ui_confirm("LOAD BACKUP?"))
|
if ((slots & POP_QS_SLOT_BACKUP) && ui_confirm("LOAD BACKUP?"))
|
||||||
return POP_MENU_QUICKLOAD_BACKUP;
|
return POP_MENU_QUICKLOAD_BACKUP;
|
||||||
@@ -641,14 +676,14 @@ static void menu_erase(uint8_t entry_page)
|
|||||||
/* Из shadow видимой страницы берём кадр без текстов. */
|
/* Из shadow видимой страницы берём кадр без текстов. */
|
||||||
gfx_set_draw_page(hidden);
|
gfx_set_draw_page(hidden);
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||||
pop_wait_edge();
|
ui_wait_frame();
|
||||||
gfx_set_visible_page(hidden);
|
gfx_set_visible_page(hidden);
|
||||||
|
|
||||||
/* Теперь чистая hidden — источник для второй страницы. */
|
/* Теперь чистая hidden — источник для второй страницы. */
|
||||||
gfx_set_draw_page(visible);
|
gfx_set_draw_page(visible);
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||||
if (entry_page != hidden) {
|
if (entry_page != hidden) {
|
||||||
pop_wait_edge();
|
ui_wait_frame();
|
||||||
gfx_set_visible_page(entry_page);
|
gfx_set_visible_page(entry_page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,13 +714,18 @@ int8_t pop_menu_process(void) __banked
|
|||||||
|
|
||||||
/* Открыли по НАЖАТИЮ Esc: дождаться отпускания, иначе то же нажатие
|
/* Открыли по НАЖАТИЮ Esc: дождаться отпускания, иначе то же нажатие
|
||||||
* закроет меню раньше первого кадра. pop_wait_edge опрашивает FIFO. */
|
* закроет меню раньше первого кадра. pop_wait_edge опрашивает FIFO. */
|
||||||
while (kbd_raw_down(KBD_ESC)) pop_wait_edge();
|
while (kbd_raw_down(KBD_ESC)) ui_wait_frame();
|
||||||
|
|
||||||
entry_page = gfx_get_visible_page();
|
entry_page = gfx_get_visible_page();
|
||||||
saved_page = gfx_get_draw_page();
|
saved_page = gfx_get_draw_page();
|
||||||
saved_bank = gfx_get_bank();
|
saved_bank = gfx_get_bank();
|
||||||
getfillsettings(&saved_fill);
|
getfillsettings(&saved_fill);
|
||||||
pop_sfx_pause(); /* snapshot/dim + первый full redraw */
|
/* Звук НЕ глушим (снято 2026-08-28, вторая половина той же заплатки —
|
||||||
|
* см. ui_begin). Здесь под ней были снимок палитры, затемнение фона и
|
||||||
|
* первая полная перерисовка: копия режет DI бандами по 16 строк, а
|
||||||
|
* палитровое затемнение идёт и в титрах с катсценами, где музыку оно не
|
||||||
|
* рвёт. Цена глушения была слышна как пауза в мелодии ровно на
|
||||||
|
* появление меню. */
|
||||||
pop_pal_snapshot();
|
pop_pal_snapshot();
|
||||||
/* 50 % яркости; диапазон UI остаётся ярким. Долю берём от шкалы, а не
|
/* 50 % яркости; диапазон UI остаётся ярким. Долю берём от шкалы, а не
|
||||||
* числом: ступеней стало 32 вместо четырёх, и прежняя «2» означала бы
|
* числом: ступеней стало 32 вместо четырёх, и прежняя «2» означала бы
|
||||||
@@ -742,11 +782,12 @@ int8_t pop_menu_process(void) __banked
|
|||||||
menu_draw(sel);
|
menu_draw(sel);
|
||||||
menu_sound(sound);
|
menu_sound(sound);
|
||||||
}
|
}
|
||||||
pop_wait_edge();
|
ui_wait_frame();
|
||||||
pop_pace_end(pop_pace_n(0)); /* игра стоит — обычный темп */
|
pop_pace_end(pop_pace_n(0)); /* игра стоит — обычный темп */
|
||||||
}
|
}
|
||||||
|
|
||||||
pop_sfx_pause(); /* две full-page копии menu_erase */
|
/* И на закрытии не глушим: menu_erase — те же две полностраничные
|
||||||
|
* копии, что и в ui_begin, с DI бандами по 16 строк. */
|
||||||
menu_erase(entry_page);
|
menu_erase(entry_page);
|
||||||
/* menu_erase вернул обе страницы из тени, снятой ДО меню: всё, что
|
/* menu_erase вернул обе страницы из тени, снятой ДО меню: всё, что
|
||||||
* статус-полосы нарисовали за это время (или сообщение, дотикавшее в
|
* статус-полосы нарисовали за это время (или сообщение, дотикавшее в
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "pop_music_tbl.h"
|
#include "pop_music_tbl.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
uint8_t pop_mus_page[POP_MUS_PAGES];
|
uint8_t pop_mus_page[POP_MUS_PAGES];
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ static uint8_t ld_busy;
|
|||||||
static int ld_fd;
|
static int ld_fd;
|
||||||
static uint8_t ld_slot;
|
static uint8_t ld_slot;
|
||||||
static uint8_t ld_next; /* следующая страница к чтению */
|
static uint8_t ld_next; /* следующая страница к чтению */
|
||||||
|
static uint8_t ld_half; /* 0 = впереди первая половина страницы */
|
||||||
static uint8_t ld_pages;
|
static uint8_t ld_pages;
|
||||||
static uint8_t ld_id;
|
static uint8_t ld_id;
|
||||||
|
|
||||||
@@ -110,13 +112,14 @@ void pop_music_free(void) __banked
|
|||||||
* ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и
|
* ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и
|
||||||
* следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком
|
* следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком
|
||||||
* это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров.
|
* это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров.
|
||||||
* Одна страница стоит 33 мс — меньше четверти кадра, и подкачка растворя-
|
* Шаг стоит 16 мс (полстраницы, см. pop_music_load_step) — меньше одного
|
||||||
* ется в анимации. Кто может позволить себе паузу (титры, чёрный экран
|
* кадрового интервала, и подкачка растворяется в анимации. Кто может позволить себе паузу (титры, чёрный экран
|
||||||
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
|
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
|
||||||
static void load_abort(void)
|
static void load_abort(void)
|
||||||
{
|
{
|
||||||
if (ld_busy) close(ld_fd);
|
if (ld_busy) close(ld_fd);
|
||||||
ld_busy = 0;
|
ld_busy = 0;
|
||||||
|
ld_half = 0;
|
||||||
slot_release(ld_slot);
|
slot_release(ld_slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,30 +142,42 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
|||||||
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
|
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
|
||||||
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
|
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
|
||||||
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
|
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
|
||||||
ld_fd = open(name, O_RDONLY);
|
POP_PATH_CALL(ld_fd, open(name, O_RDONLY));
|
||||||
if (ld_fd < 0) return -1;
|
if (ld_fd < 0) return -1;
|
||||||
slot_blk[n] = mem_alloc_pages(pages);
|
slot_blk[n] = mem_alloc_pages(pages);
|
||||||
if (!slot_blk[n]) { close(ld_fd); return -1; }
|
if (!slot_blk[n]) { close(ld_fd); return -1; }
|
||||||
ld_busy = 1;
|
ld_busy = 1;
|
||||||
ld_slot = n;
|
ld_slot = n;
|
||||||
ld_next = 0;
|
ld_next = 0;
|
||||||
|
ld_half = 0;
|
||||||
ld_pages = pages;
|
ld_pages = pages;
|
||||||
ld_id = id;
|
ld_id = id;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ШАГ = ПОЛСТРАНИЦЫ (8 КБ, ~16 мс), а не страница (~33 мс).
|
||||||
|
*
|
||||||
|
* Шаг вызывают раз в кадр из циклов, где кадр разложен на блоки по одному
|
||||||
|
* кадровому интервалу (20 мс): катсцены после перехода на блочную
|
||||||
|
* отрисовку, см. pop_intro.c/pv_restore_bg. Целая страница в интервал не
|
||||||
|
* влезала и незаметно растягивала кадр сцены; половина влезает, а цена —
|
||||||
|
* вдвое больше вызовов (11 страниц реплики = 22 кадра вместо 11, то есть
|
||||||
|
* меньше полусекунды). Игровому циклу это безразлично: там логический
|
||||||
|
* кадр 80-100 мс. */
|
||||||
int8_t pop_music_load_step(void) __banked
|
int8_t pop_music_load_step(void) __banked
|
||||||
{
|
{
|
||||||
uint8_t p;
|
uint8_t p;
|
||||||
|
|
||||||
if (!ld_busy) return 1; /* нечего грузить */
|
if (!ld_busy) return 1; /* нечего грузить */
|
||||||
p = mem_get_page(slot_blk[ld_slot], ld_next);
|
p = mem_get_page(slot_blk[ld_slot], ld_next);
|
||||||
/* Последняя страница короче — читаем сколько есть; ошибку ловим по
|
/* Последняя страница короче — читаем сколько есть (вторая половина
|
||||||
* отрицательному коду, а не по «не 16384». */
|
* может дать и ноль байт); ошибку ловим по отрицательному коду. */
|
||||||
if (!p || bank_read_page(ld_fd, p, 0, 16384) < 0) {
|
if (!p || bank_read_page(ld_fd, p, ld_half ? 8192 : 0, 8192) < 0) {
|
||||||
load_abort();
|
load_abort();
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
if (!ld_half) { ld_half = 1; return 0; } /* дочитаем в следующий раз */
|
||||||
|
ld_half = 0;
|
||||||
if (++ld_next < ld_pages) return 0;
|
if (++ld_next < ld_pages) return 0;
|
||||||
|
|
||||||
close(ld_fd);
|
close(ld_fd);
|
||||||
@@ -274,7 +289,7 @@ int8_t pop_music_ring_start(uint8_t id) __banked
|
|||||||
pop_music_free(); /* обычные слоты кольцу не нужны */
|
pop_music_free(); /* обычные слоты кольцу не нужны */
|
||||||
pop_music_ring_stop();
|
pop_music_ring_stop();
|
||||||
mus_name(name, id);
|
mus_name(name, id);
|
||||||
ring_fd = open(name, O_RDONLY);
|
POP_PATH_CALL(ring_fd, open(name, O_RDONLY));
|
||||||
if (ring_fd < 0) return -1;
|
if (ring_fd < 0) return -1;
|
||||||
ring_fd_open = 1;
|
ring_fd_open = 1;
|
||||||
ring_blk = mem_alloc_pages(RING_PAGES);
|
ring_blk = mem_alloc_pages(RING_PAGES);
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
#include "pop_guard.h"
|
#include "pop_guard.h"
|
||||||
#include "pop_pal.h"
|
#include "pop_pal.h"
|
||||||
#include "pop_ui.h" /* POP_COL_OUTSIDE */
|
#include "pop_ui.h" /* POP_COL_OUTSIDE */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
static void pal_file_load(const char *path, const char *fallback)
|
static void pal_file_load(const char *path)
|
||||||
{
|
{
|
||||||
if (gfx_pal_fload(0, path) < 0) gfx_pal_fload(0, fallback);
|
int rc;
|
||||||
|
POP_PATH_CALL(rc, gfx_pal_fload(0, path));
|
||||||
gfx_pal_sync();
|
gfx_pal_sync();
|
||||||
pop_ui_palette_snapshot();
|
pop_ui_palette_snapshot();
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ void pop_pal_menu_dim(uint8_t fade) __banked
|
|||||||
|
|
||||||
void pop_pal_game_load(void) __banked
|
void pop_pal_game_load(void) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("KID\\kid.pal", "a:\\kid.pal");
|
pal_file_load("KID\\kid.pal");
|
||||||
/* kid.pal содержит все 256 записей и намеренно затирает эти три
|
/* kid.pal содержит все 256 записей и намеренно затирает эти три
|
||||||
* динамических диапазона (тайлсет, Тень, набор соперника).
|
* динамических диапазона (тайлсет, Тень, набор соперника).
|
||||||
* Восстановить их надо ДО snapshot. */
|
* Восстановить их надо ДО snapshot. */
|
||||||
@@ -110,7 +112,7 @@ void pop_pal_level_load(uint8_t full) __banked
|
|||||||
|
|
||||||
void pop_pal_title_load(void) __banked
|
void pop_pal_title_load(void) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("TITLE\\title.pal", "a:\\title.pal");
|
pal_file_load("TITLE\\title.pal");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
|
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
|
||||||
@@ -120,7 +122,7 @@ void pop_pal_title_load(void) __banked
|
|||||||
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
|
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
|
||||||
void pop_pal_story_load(uint8_t ending) __banked
|
void pop_pal_story_load(uint8_t ending) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("PV\\story.pal", "a:\\story.pal");
|
pal_file_load("PV\\story.pal");
|
||||||
if (!ending) return;
|
if (!ending) return;
|
||||||
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
|
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||||
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
|
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||||
@@ -129,5 +131,5 @@ void pop_pal_story_load(uint8_t ending) __banked
|
|||||||
|
|
||||||
void pop_pal_pv_load(void) __banked
|
void pop_pal_pv_load(void) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("PV\\pv.pal", "a:\\pv.pal");
|
pal_file_load("PV\\pv.pal");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* pop_path_bank.c — каталог приложения и восстановление cwd, BANK10.
|
||||||
|
*
|
||||||
|
* APPINFO B=1 возвращает абсолютный каталог запущенного EXE вместе с диском.
|
||||||
|
* Получаем его один раз при старте. Буфер объявлен const намеренно: у
|
||||||
|
* Sprinter банковые страницы — RAM, а --constseg кладёт объект в BANK10,
|
||||||
|
* не расходуя дефицитную резидентную _DATA в W2. Записывает его только
|
||||||
|
* ESTEX во время pop_path_init().
|
||||||
|
*
|
||||||
|
* ABI: публичные функции __banked; их вызывает POP_PATH_CALL из банка
|
||||||
|
* файлового клиента. После возврата трамплин восстанавливает прежнюю W3,
|
||||||
|
* поэтому переданный самой файловой операции указатель снова доступен.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
|
static const char pop_app_dir[256] = { 0 };
|
||||||
|
|
||||||
|
/* ESTEX APPINFO ($47), B=1: HL=буфер, CF/A=ошибка. */
|
||||||
|
static int appinfo_dir(const char *buf) __naked
|
||||||
|
{
|
||||||
|
(void)buf;
|
||||||
|
__asm
|
||||||
|
push ix
|
||||||
|
ld b, #1
|
||||||
|
ld c, #0x47
|
||||||
|
rst #0x10
|
||||||
|
pop ix
|
||||||
|
jr c, _paid_err
|
||||||
|
ld de, #0
|
||||||
|
ret
|
||||||
|
_paid_err:
|
||||||
|
call __errno_set
|
||||||
|
ld de, #-1
|
||||||
|
ret
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t pop_path_init(void) __banked
|
||||||
|
{
|
||||||
|
const char *dir = pop_app_dir;
|
||||||
|
|
||||||
|
if (appinfo_dir(dir) != 0 || !dir[0]) return -1;
|
||||||
|
return (int8_t)chdir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t pop_chdir_home(void) __banked
|
||||||
|
{
|
||||||
|
return (int8_t)chdir((const char *)pop_app_dir);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "pop_pace.h"
|
#include "pop_pace.h"
|
||||||
#include "pop_timer.h"
|
#include "pop_timer.h"
|
||||||
#include "sprpop_cold.h"
|
#include "sprpop_cold.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
#define QS_VERSION 4
|
#define QS_VERSION 4
|
||||||
#define QS_SAVE 1
|
#define QS_SAVE 1
|
||||||
@@ -154,6 +155,7 @@ static int8_t qs_save(uint8_t page)
|
|||||||
uint8_t tail[4];
|
uint8_t tail[4];
|
||||||
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
|
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
|
||||||
uint16_t size;
|
uint16_t size;
|
||||||
|
int rc;
|
||||||
qs_init(&io,page,QS_LIMIT,0,0);
|
qs_init(&io,page,QS_LIMIT,0,0);
|
||||||
pop_qs_raw_put(&io,h,sizeof(h));
|
pop_qs_raw_put(&io,h,sizeof(h));
|
||||||
qs_payload(&io);
|
qs_payload(&io);
|
||||||
@@ -162,14 +164,18 @@ static int8_t qs_save(uint8_t page)
|
|||||||
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
|
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
|
||||||
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
|
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
|
||||||
if (io.failed) return -1;
|
if (io.failed) return -1;
|
||||||
unlink("POP.NEW");
|
POP_PATH_CALL(rc, unlink("POP.NEW"));
|
||||||
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
|
POP_PATH_CALL(rc, bank_save_file(page,QS_BASE,"POP.NEW",size));
|
||||||
unlink("POP.NEW"); return -1;
|
if (rc != size) {
|
||||||
|
POP_PATH_CALL(rc, unlink("POP.NEW"));
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
unlink("POP.BAK");
|
POP_PATH_CALL(rc, unlink("POP.BAK"));
|
||||||
(void)rename("POP.SAV", "POP.BAK");
|
POP_PATH_CALL(rc, rename("POP.SAV", "POP.BAK"));
|
||||||
if (rename("POP.NEW", "POP.SAV") != 0) {
|
POP_PATH_CALL(rc, rename("POP.NEW", "POP.SAV"));
|
||||||
(void)rename("POP.BAK", "POP.SAV"); return -1;
|
if (rc != 0) {
|
||||||
|
POP_PATH_CALL(rc, rename("POP.BAK", "POP.SAV"));
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -178,7 +184,8 @@ static int8_t qs_load_one(const char *name, uint8_t page)
|
|||||||
{
|
{
|
||||||
uint8_t level;
|
uint8_t level;
|
||||||
uint16_t payload_size;
|
uint16_t payload_size;
|
||||||
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
|
int size;
|
||||||
|
POP_PATH_CALL(size, bank_load_file(page,QS_BASE,name,QS_LIMIT));
|
||||||
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
|
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
|
||||||
&payload_size)) return -1;
|
&payload_size)) return -1;
|
||||||
if (level != pop_current_level) {
|
if (level != pop_current_level) {
|
||||||
@@ -205,11 +212,11 @@ uint8_t pop_qsave_probe(void) __banked
|
|||||||
page = mem_get_page(blk, 0);
|
page = mem_get_page(blk, 0);
|
||||||
gfx_w0_page_prepare(page);
|
gfx_w0_page_prepare(page);
|
||||||
|
|
||||||
size = bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT);
|
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT));
|
||||||
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
||||||
&payload_size))
|
&payload_size))
|
||||||
slots |= POP_QS_SLOT_SAVE;
|
slots |= POP_QS_SLOT_SAVE;
|
||||||
size = bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT);
|
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT));
|
||||||
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
||||||
&payload_size))
|
&payload_size))
|
||||||
slots |= POP_QS_SLOT_BACKUP;
|
slots |= POP_QS_SLOT_BACKUP;
|
||||||
@@ -234,14 +241,16 @@ int8_t pop_qsave_process(void) __banked
|
|||||||
if (!blk) return -1;
|
if (!blk) return -1;
|
||||||
page=mem_get_page(blk,0);
|
page=mem_get_page(blk,0);
|
||||||
gfx_w0_page_prepare(page);
|
gfx_w0_page_prepare(page);
|
||||||
pop_sfx_pause();
|
/* Звук не глушим (2026-08-28). Единственное место из снятых, где по
|
||||||
|
* диску реально едут 16 КБ (~33 мс, три периода насоса): если на F6/F9
|
||||||
|
* появится скрежет — вернуть сюда pop_sfx_pause/pop_sfx_start, это
|
||||||
|
* лечится точечно и только здесь. */
|
||||||
if (request == QS_SAVE) rc = qs_save(page);
|
if (request == QS_SAVE) rc = qs_save(page);
|
||||||
else if (request == QS_LOAD_BACKUP) rc = qs_load_one("POP.BAK", page);
|
else if (request == QS_LOAD_BACKUP) rc = qs_load_one("POP.BAK", page);
|
||||||
else {
|
else {
|
||||||
rc = qs_load_one("POP.SAV",page);
|
rc = qs_load_one("POP.SAV",page);
|
||||||
if (rc < 0) rc = qs_load_one("POP.BAK",page);
|
if (rc < 0) rc = qs_load_one("POP.BAK",page);
|
||||||
}
|
}
|
||||||
pop_sfx_start();
|
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
/* Успех ничего не меняет: нужная строка висит с начала операции.
|
/* Успех ничего не меняет: нужная строка висит с начала операции.
|
||||||
* Отказ переписывает её на NO QUICKSAVE/NO QUICKLOAD — обычной
|
* Отказ переписывает её на NO QUICKSAVE/NO QUICKLOAD — обычной
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */
|
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */
|
||||||
#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */
|
#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */
|
||||||
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
|
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
|
||||||
|
#include "_pop_file.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
@@ -83,7 +84,8 @@ static const char *bg_path(const char *name)
|
|||||||
static int bg_load_tile_pal(const char *name)
|
static int bg_load_tile_pal(const char *name)
|
||||||
{
|
{
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
int fd = open(bg_path(name), O_RDONLY);
|
int fd;
|
||||||
|
POP_PATH_CALL(fd, open(bg_path(name), O_RDONLY));
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
|
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
|
||||||
gfx_pal_load(0, POP_PAL_ENV, 16, buf);
|
gfx_pal_load(0, POP_PAL_ENV, 16, buf);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
#include "pop_ui.h"
|
#include "pop_ui.h"
|
||||||
#include "pop_status.h"
|
#include "pop_status.h"
|
||||||
|
#include "pop_music.h" /* pop_music_service — подкачка кольца в долгих фейдах */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t base;
|
uint8_t base;
|
||||||
@@ -53,8 +55,10 @@ static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
|
|||||||
|
|
||||||
int pop_ui_init(void) __banked
|
int pop_ui_init(void) __banked
|
||||||
{
|
{
|
||||||
|
int rc;
|
||||||
if (font_ready) return 0;
|
if (font_ready) return 0;
|
||||||
if (atlas_load(&font_at, "FONT\\font.atl") != 0) return -1;
|
POP_PATH_CALL(rc, atlas_load(&font_at, "FONT\\font.atl"));
|
||||||
|
if (rc != 0) return -1;
|
||||||
font_ready = 1;
|
font_ready = 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -241,8 +245,10 @@ void pop_ui_palette_black(void) __banked
|
|||||||
uint8_t *black = pop_pal_buf;
|
uint8_t *black = pop_pal_buf;
|
||||||
|
|
||||||
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
|
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
|
||||||
* все четыре четверти палитры, не занимая постоянную память. */
|
* все четыре четверти палитры, не занимая постоянную память.
|
||||||
for (i = 0; i < sizeof(black); i++) black[i] = 0;
|
* sizeof(black) здесь нельзя: black — 16-битный указатель, и тогда
|
||||||
|
* обнулятся лишь первые два байта, а цвет заливки останется случайным. */
|
||||||
|
for (i = 0; i < sizeof(pop_pal_buf); i++) black[i] = 0;
|
||||||
for (chunk = 0; chunk < 4; chunk++) {
|
for (chunk = 0; chunk < 4; chunk++) {
|
||||||
uint8_t start = (uint8_t)(chunk << 6);
|
uint8_t start = (uint8_t)(chunk << 6);
|
||||||
gfx_pal_load(0, start, 64, black);
|
gfx_pal_load(0, start, 64, black);
|
||||||
@@ -292,6 +298,11 @@ static uint8_t fade_run(uint8_t frames, uint8_t up, uint8_t skip)
|
|||||||
pop_ui_palette_dim(step, 0);
|
pop_ui_palette_dim(step, 0);
|
||||||
shown = step;
|
shown = step;
|
||||||
}
|
}
|
||||||
|
/* Fade — это 2,13 с, и всё это время главный цикл стоит. Кольцо
|
||||||
|
* потокового трека дочитывает pop_music_service, иначе музыка
|
||||||
|
* замолкает прямо посреди перехода (та же причина, что у меню:
|
||||||
|
* ui_wait_frame в pop_menu.c). */
|
||||||
|
pop_music_service();
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync();
|
||||||
if (skip) {
|
if (skip) {
|
||||||
kbd_raw_sync();
|
kbd_raw_sync();
|
||||||
@@ -429,6 +440,7 @@ uint8_t pop_screen_present_ltr(void) __banked
|
|||||||
stripe.x = x;
|
stripe.x = x;
|
||||||
stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
|
stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
|
||||||
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
|
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
|
||||||
|
pop_music_service(); /* 80 кадров перехода — см. fade_run */
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync();
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -461,8 +473,10 @@ int pop_screen_blit_atlas_strips(const char * const *parts,
|
|||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
for (part = 0; part < count; part++) {
|
for (part = 0; part < count; part++) {
|
||||||
atlas_t at;
|
atlas_t at;
|
||||||
|
int rc;
|
||||||
|
|
||||||
if (atlas_load(&at, parts[part]) != 0) return -1;
|
POP_PATH_CALL(rc, atlas_load(&at, parts[part]));
|
||||||
|
if (rc != 0) return -1;
|
||||||
strip_blit(&at, x, &y);
|
strip_blit(&at, x, &y);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -40,15 +40,6 @@ int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int pop_vflip_add(const atlas_t *orig, const char *path)
|
|
||||||
{
|
|
||||||
atlas_t m;
|
|
||||||
|
|
||||||
if (!orig || vf_n >= VF_MAX) return -1;
|
|
||||||
if (atlas_load(&m, path) != 0) return -1;
|
|
||||||
return pop_vflip_take(orig, &m);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t pop_vflip_page(uint8_t src)
|
uint8_t pop_vflip_page(uint8_t src)
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
|||||||
@@ -26,11 +26,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprite.h>
|
#include <sprite.h>
|
||||||
|
|
||||||
/* Загрузить зеркальный файл path и связать его с атласом orig.
|
/* Зеркало УЖЕ загружено из архива: таблица
|
||||||
* 0 — OK, -1 — файла нет / не хватило EMM / таблица полна. */
|
|
||||||
int pop_vflip_add(const atlas_t *orig, const char *path);
|
|
||||||
|
|
||||||
/* То же, но зеркало УЖЕ загружено (например, прочитано из архива): таблица
|
|
||||||
* забирает его себе, и дальше он освобождается вместе с остальными. */
|
* забирает его себе, и дальше он освобождается вместе с остальными. */
|
||||||
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror);
|
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,10 @@
|
|||||||
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
|
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
|
||||||
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
|
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
|
||||||
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
|
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
|
||||||
|
#include "_pop_file.h" /* cwd приложения для старых DSS */
|
||||||
|
|
||||||
|
#include <sprinter_exit.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
__sfr __at (0xFE) io_border;
|
__sfr __at (0xFE) io_border;
|
||||||
|
|
||||||
@@ -260,6 +264,11 @@ int main(void)
|
|||||||
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
|
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
|
||||||
uint8_t failed = 0;
|
uint8_t failed = 0;
|
||||||
|
|
||||||
|
/* До первой файловой операции запоминаем абсолютный каталог EXE.
|
||||||
|
* Без этого старый DSS теряет последний компонент cwd после OPEN пути
|
||||||
|
* с подкаталогом и следующий файл ищет уровнем выше. */
|
||||||
|
if (pop_path_init() != 0) return 1;
|
||||||
|
|
||||||
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (sprpop_cold.c):
|
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (sprpop_cold.c):
|
||||||
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
|
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
|
||||||
pop_app_reset();
|
pop_app_reset();
|
||||||
@@ -357,6 +366,7 @@ int main(void)
|
|||||||
* девелоперская сборка может до title держать дворец LEVEL=11. */
|
* девелоперская сборка может до title держать дворец LEVEL=11. */
|
||||||
if (pop_app_state == POP_APP_DEMO) {
|
if (pop_app_state == POP_APP_DEMO) {
|
||||||
pop_next_level = 0;
|
pop_next_level = 0;
|
||||||
|
pop_endmus_left = 0;
|
||||||
pop_seamless = 0;
|
pop_seamless = 0;
|
||||||
pop_checkpoint = 0;
|
pop_checkpoint = 0;
|
||||||
if (pop_level_switch() != 0) {
|
if (pop_level_switch() != 0) {
|
||||||
@@ -765,9 +775,28 @@ int main(void)
|
|||||||
/* Смена уровня целиком уехала в банк 8 (pop_level_change): это
|
/* Смена уровня целиком уехала в банк 8 (pop_level_change): это
|
||||||
* событие раз в несколько минут, а кода на нём было под сотню строк
|
* событие раз в несколько минут, а кода на нём было под сотню строк
|
||||||
* в самом горячем месте программы — резидент W1 у нас в дефиците. */
|
* в самом горячем месте программы — резидент W1 у нас в дефиците. */
|
||||||
if (!is_demo && pop_next_level != pop_current_level &&
|
if (!is_demo && pop_next_level != pop_current_level) {
|
||||||
pop_level_change(back) != 0) {
|
/* КОНЕЦ УРОВНЯ ЖДЁТ МЕЛОДИЮ (порт seg003:387, play_level_2):
|
||||||
failed = 1; quit = 1; break;
|
*
|
||||||
|
* if (next_level == current_level || check_sound_playing())
|
||||||
|
* draw_game_frame(); ... // экран уровня живёт
|
||||||
|
* else { stop_sounds(); return next_level; }
|
||||||
|
*
|
||||||
|
* То есть пройденный уровень остаётся на экране — с анимацией
|
||||||
|
* факелов, идущим таймером и всем прочим — ровно столько,
|
||||||
|
* сколько звучит трек конца уровня; мы же уходили на смену
|
||||||
|
* уровня сразу и обрывали мелодию на первых нотах.
|
||||||
|
*
|
||||||
|
* «Звучит» проверяем двумя способами: pop_music_busy() пока
|
||||||
|
* трек играет и счётчик pop_endmus_left по длине записи (его
|
||||||
|
* взводит play_seq вместе с самим треком) — иначе с
|
||||||
|
* ВЫКЛЮЧЕННОЙ музыкой паузы не было бы вовсе, а оригинал
|
||||||
|
* выдерживает её и молча. */
|
||||||
|
if (pop_endmus_left) pop_endmus_left--;
|
||||||
|
if (!pop_endmus_left && !pop_music_busy() &&
|
||||||
|
pop_level_change(back) != 0) {
|
||||||
|
failed = 1; quit = 1; break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
|
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
|
||||||
dead_frames++;
|
dead_frames++;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
||||||
#include "pop_shadow.h" /* запечённый набор Тени */
|
#include "pop_shadow.h" /* запечённый набор Тени */
|
||||||
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
||||||
|
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
|
||||||
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
||||||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||||||
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
|
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
|
||||||
@@ -226,7 +227,12 @@ int8_t pop_level_change(uint8_t back) __banked
|
|||||||
* почти всегда — поймано пользователем 2026-08-25. Ветка с
|
* почти всегда — поймано пользователем 2026-08-25. Ветка с
|
||||||
* катсценой свободна от этого: pop_pre_cutscene_show глушит
|
* катсценой свободна от этого: pop_pre_cutscene_show глушит
|
||||||
* звук первым действием. */
|
* звук первым действием. */
|
||||||
pop_sfx_pause();
|
/* Глушение снято 2026-08-28 (замер: загрузка уровня насос не
|
||||||
|
* морит — 500 вызовов подряд, максимум 245 832 такта при
|
||||||
|
* дедлайне 251 000). Прежнее обоснование — скрежет на палитре
|
||||||
|
* через BIOS ($A4 с запрещёнными прерываниями), пойманный на
|
||||||
|
* переходе без катсцены 2026-08-25. Если он вернётся, это
|
||||||
|
* единственное место, куда его надо возвращать. */
|
||||||
/* Без fade: старый кадр гасим сразу и обязательно даём
|
/* Без fade: старый кадр гасим сразу и обязательно даём
|
||||||
* показать один чёрный растр ДО диска/перерисовки. */
|
* показать один чёрный растр ДО диска/перерисовки. */
|
||||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||||
@@ -1145,7 +1151,7 @@ int pop_boot(void) __banked
|
|||||||
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
|
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
|
||||||
pop_guard_reset();
|
pop_guard_reset();
|
||||||
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */
|
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */
|
||||||
printf("kid.ani load failed\n");
|
puts("kid.ani load failed\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
||||||
@@ -1227,7 +1233,10 @@ int pop_level_switch(void) __banked
|
|||||||
* дальше, а урон — нет (do_startpos дольёт до потолка). */
|
* дальше, а урон — нет (do_startpos дольёт до потолка). */
|
||||||
hitp_beg_lev = hitp_max;
|
hitp_beg_lev = hitp_max;
|
||||||
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
|
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
|
||||||
pop_sfx_pause(); /* загрузка уровня — ESTEX надолго, звук глушим */
|
/* ЗАМЕР 2026-08-28: глушение снято ВРЕМЕННО, чтобы измерить
|
||||||
|
* зазоры насоса на загрузке уровня (дедлайн 251 000 тактов).
|
||||||
|
* Если зазоры укладываются — снятие остаётся, и трек заставки
|
||||||
|
* доигрывает поверх загрузки, как в оригинале (seg001:690). */
|
||||||
if (pop_level_load_num(pop_next_level) != 0) {
|
if (pop_level_load_num(pop_next_level) != 0) {
|
||||||
pop_next_level = pop_current_level; /* файла нет — остаёмся */
|
pop_next_level = pop_current_level; /* файла нет — остаёмся */
|
||||||
} else {
|
} else {
|
||||||
@@ -1294,6 +1303,34 @@ int pop_level_switch(void) __banked
|
|||||||
/* Атомарный commit на границе растра: впервые показать
|
/* Атомарный commit на границе растра: впервые показать
|
||||||
* готовую комнату уже с окончательной палитрой уровня.
|
* готовую комнату уже с окончательной палитрой уровня.
|
||||||
* Вторую страницу синхронизировать аппаратной копией. */
|
* Вторую страницу синхронизировать аппаратной копией. */
|
||||||
|
/* ЖДЁМ КОНЦА ТРЕКА ЗАСТАВКИ НА ЧЁРНОМ ЭКРАНЕ — порт
|
||||||
|
* seg003:107:
|
||||||
|
* while (check_sound_playing() && !do_paused()) idle();
|
||||||
|
* stop_sounds();
|
||||||
|
* draw_level_first();
|
||||||
|
* Порядок оригинала важен: загрузка уровня идёт ПОД
|
||||||
|
* звучащий трек, и только потом чернота до его конца. Тогда
|
||||||
|
* общее время черноты = max(трек, загрузка), а не их сумма,
|
||||||
|
* и трек не обрывается на полуслове. Раньше у нас было
|
||||||
|
* наоборот: pre_cut_finish досиживал трек, потом глушил
|
||||||
|
* насос и грузил уровень.
|
||||||
|
*
|
||||||
|
* ЗАМЕР 2026-08-28: 500 подряд вызовов насоса через всю
|
||||||
|
* загрузку — максимальный зазор 245 832 такта при дедлайне
|
||||||
|
* 251 000, ни одной пропущенной порции. Поэтому глушение на
|
||||||
|
* загрузке снято и музыка её переживает.
|
||||||
|
*
|
||||||
|
* Счётчик — страховка от вечного ожидания (потоковый трек,
|
||||||
|
* который никогда не кончится): ~20 с и идём дальше. */
|
||||||
|
{
|
||||||
|
uint16_t mus_guard = 1000;
|
||||||
|
|
||||||
|
while (pop_music_busy() && mus_guard) {
|
||||||
|
mus_guard--;
|
||||||
|
pop_wait_edge();
|
||||||
|
}
|
||||||
|
pop_music_free(); /* порт stop_sounds() */
|
||||||
|
}
|
||||||
pop_wait_edge();
|
pop_wait_edge();
|
||||||
gfx_set_visible_page(level_prepared_page);
|
gfx_set_visible_page(level_prepared_page);
|
||||||
gfx_set_draw_page(old_page);
|
gfx_set_draw_page(old_page);
|
||||||
|
|||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/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 (подтверждено).
|
||||||
|
#
|
||||||
|
# Использование:
|
||||||
|
# tools/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ...
|
||||||
|
# tools/make_hdd.sh out.chd prog.exe KID:kid0.atl GUARD:g0.atl ...
|
||||||
|
# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог образа
|
||||||
|
# (каталог создаётся); без префикса файл идёт в корень.
|
||||||
|
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
|
||||||
|
|
||||||
|
# mtools собран с enable-raw-term и, когда ему нужно что-то спросить,
|
||||||
|
# открывает УПРАВЛЯЮЩИЙ ТЕРМИНАЛ напрямую (/dev/tty) — в обход stdin.
|
||||||
|
# Поэтому ни `< /dev/null`, ни перенаправление stdio не спасают: из
|
||||||
|
# терминала сборка молча вставала навсегда на `mmd z:/BG` (подтверждено
|
||||||
|
# lsof: fd 0 = /dev/null, fd 4 = /dev/tty, процесс спит). Лечится только
|
||||||
|
# отсутствием управляющего терминала — запускаем mtools в новой сессии,
|
||||||
|
# тогда открыть /dev/tty нечего и mtools идёт неинтерактивным путём.
|
||||||
|
#
|
||||||
|
# setsid(1) в macOS нет, поэтому делаем то же самое питоном.
|
||||||
|
# Каналов, откуда mtools может ждать ответ, ДВА, и закрывать надо оба:
|
||||||
|
# /dev/tty — убирается новой сессией (os.setsid);
|
||||||
|
# stdin — убирается перенаправлением из /dev/null.
|
||||||
|
# Закрыть только один недостаточно: с одним лишь setsid mmd переключался на
|
||||||
|
# stdin и вис уже на нём (lsof: fd 0 = /dev/ttys002).
|
||||||
|
mt() {
|
||||||
|
python3 -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \
|
||||||
|
"$@" < /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16.
|
||||||
|
echo "--- разметка и формат образа ---"
|
||||||
|
mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
|
||||||
|
mt mformat z:
|
||||||
|
|
||||||
|
echo "--- копирование файлов ---"
|
||||||
|
|
||||||
|
# Аргумент может быть либо файлом (кладём в корень), либо парой
|
||||||
|
# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда.
|
||||||
|
# Имена на Sprinter/DSS — 8.3, каталоги тоже (KID, GUARD, BG, LEVELS...).
|
||||||
|
mt mmd "z:/games" >/dev/null 2>&1 || true
|
||||||
|
mt mmd "z:/games/sprpop" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
for f in "$@"; do
|
||||||
|
case "$f" in
|
||||||
|
*:*) dir="${f%%:*}"; src="${f#*:}"
|
||||||
|
# Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе
|
||||||
|
# непрошедший вопрос mtools снова окажется невидимым.
|
||||||
|
mt mmd "z:/games/sprpop/$dir" >/dev/null 2>&1 || true
|
||||||
|
echo "COPY $src to z:/games/sprpop/$dir/"
|
||||||
|
mt mcopy -o "$src" "z:/games/sprpop/$dir/" ;;
|
||||||
|
*) mt mcopy -o "$f" "z:/games/sprpop/" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "--- содержимое образа ---"
|
||||||
|
mt mdir -/ z:
|
||||||
|
|
||||||
|
# RAW -> CHD (без сжатия = быстро; та же геометрия).
|
||||||
|
echo "--- конвертация RAW -> CHD ---"
|
||||||
|
chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \
|
||||||
|
-chs $CYLS,$HEADS,$SECS -ss 512 -f
|
||||||
|
echo "готово: $OUT_CHD"
|
||||||
Reference in New Issue
Block a user