Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):
* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
(UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
ABOUT. About показывает тот же текст, что стартовый Sprinter screen,
минус строка про клавиши — общий about_text(), чтобы экраны не
разъехались. CONTROLS собирается таблицей и центрируется по
фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
(SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
а не пункт меню. POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах. Нижняя:
порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
24 тика). Верхняя отладочная переведена с палочек на текст
«Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
своим форматированием чисел (без printf и без деления).
Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
не растёт, весь рендер в банке. Бюджет после правок не изменился
(_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
не блитятся вовсе, а вход в комнату заливает только игровое поле
(pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
статус-строке: геометрия, семантика text_time_total как идентификатора
сообщения, мигание, рестарт по истечении 36/288.
Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,11 @@ ENGINE_DIR := $(abspath $(CURDIR)/..)
|
||||
# t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
|
||||
# абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве
|
||||
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
|
||||
# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88).
|
||||
CODE_LOC := 0x1000
|
||||
# набора обязан начинаться выше его конца. С добавлением таблицы кадров
|
||||
# КАТСЦЕН блоб вырос до 4150 Б (0x100 + 4150 = 0x1136), и прежние 0x1000
|
||||
# уже резали его хвост — набор падал как «main() не позвал ни одной
|
||||
# проверки». Держим запас: следующая таблица снова сдвинет границу.
|
||||
CODE_LOC := 0x1400
|
||||
# Данные — выше 0x8000: набор t_phys линкует уже полдвижка (геометрия, Kid,
|
||||
# карта/физика, управление, страж) и его код перевалил за 0x8000. Раньше
|
||||
# это проявлялось как «тест зациклился»: хвост кода ложился поверх _DATA.
|
||||
@@ -42,6 +45,9 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
|
||||
OBJS_grab := $(OBJS_phys)
|
||||
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
|
||||
OBJS_wall := $(OBJS_phys)
|
||||
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
|
||||
# (нужны pop_map + физика, то есть тот же состав).
|
||||
OBJS_gate := $(OBJS_phys)
|
||||
# t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень).
|
||||
OBJS_char := $(OBJS_phys)
|
||||
# t_mouse — спецсобытие уровня 8 (мышь). ЕДИНСТВЕННЫЙ набор, куда линкуется
|
||||
@@ -55,9 +61,39 @@ OBJS_mouse := $(OBJS_phys) build/eng_guards.rel
|
||||
OBJS_shadow := $(OBJS_phys) build/eng_guards.rel
|
||||
# t_jaffar — спецсобытия уровня 13 (встреча, победа, выход, падающая гряда).
|
||||
OBJS_jaffar := $(OBJS_phys) build/eng_guards.rel
|
||||
# t_cfg — весь codec POP.CFG из единственного прикладного модуля.
|
||||
OBJS_cfg := build/eng_pop_config.rel build/bank_stub.rel
|
||||
# pop_app — чистая таблица переходов, без DSS/графики.
|
||||
OBJS_app := build/eng_pop_app.rel build/bank_stub.rel
|
||||
# pop_demo — запись управления; t_demo подменяет выход control и raw input.
|
||||
OBJS_demo := build/eng_pop_demo.rel build/bank_stub.rel
|
||||
# pop_timer — чистое ядро логического времени; поток QuickSave заглушен
|
||||
# memory-stream'ом в t_timer.c.
|
||||
OBJS_timer := build/eng_pop_timer.rel build/bank_stub.rel
|
||||
# pop_cutscene — общий интерпретатор статических сценариев. Он не знает
|
||||
# графики, поэтому проверяется отдельно от PV/title-ресурсов.
|
||||
OBJS_cutscene := build/eng_pop_cutscene.rel build/bank_stub.rel
|
||||
# pop_flow — чистая таблица маршрута перед загрузкой следующего уровня.
|
||||
OBJS_flow := build/eng_pop_flow.rel build/bank_stub.rel
|
||||
|
||||
include $(TESTKIT)/host-tests.mk
|
||||
|
||||
# У host-теста нет DSS/EMM: ему нужен тот же codec/state, но не target-I/O.
|
||||
build/eng_pop_config.rel: $(ENGINE_DIR)/pop_config.c | $(BUILD)
|
||||
@$(SDCC) $(CFLAGS) -DPOP_CONFIG_HOST_TEST -c -o $@ $<
|
||||
|
||||
build/eng_pop_timer.rel: $(ENGINE_DIR)/pop_timer.c | $(BUILD)
|
||||
@$(SDCC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
build/eng_pop_demo.rel: $(ENGINE_DIR)/pop_demo.c | $(BUILD)
|
||||
@$(SDCC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
build/eng_pop_cutscene.rel: $(ENGINE_DIR)/pop_cutscene.c | $(BUILD)
|
||||
@$(SDCC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
build/eng_pop_flow.rel: $(ENGINE_DIR)/pop_flow.c | $(BUILD)
|
||||
@$(SDCC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
# --- t_phys: блоб таблиц анимации ------------------------------------
|
||||
KID_BIN := $(abspath $(CURDIR)/../../poc/res/kid/kid_data.bin)
|
||||
|
||||
|
||||
@@ -131,6 +131,13 @@ uint8_t kbd_raw_down(uint16_t code)
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t kbd_raw_any_down(void)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < TK_KEYS_MAX; i++) if (tk_keys[i]) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tk_keys_clear(void)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/* pop_app — протокол верхнего автомата: допустимые и запрещённые переходы. */
|
||||
#include "tcheck.h"
|
||||
#include "pop_app.h"
|
||||
|
||||
TC_TEST(boot_to_title)
|
||||
{
|
||||
pop_app_reset();
|
||||
TC_EQ(pop_app_state, POP_APP_BOOT);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_BOOT_READY));
|
||||
TC_EQ(pop_app_state, POP_APP_BUILD_INFO);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_BUILD_DONE));
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
}
|
||||
|
||||
TC_TEST(level_load_roundtrip)
|
||||
{
|
||||
pop_app_reset();
|
||||
pop_app_dispatch(POP_APP_EV_BOOT_READY);
|
||||
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
|
||||
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
|
||||
TC_EQ(pop_app_state, POP_APP_PLAYING);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_PAUSE));
|
||||
TC_EQ(pop_app_state, POP_APP_PAUSE_MENU);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_RESUME));
|
||||
TC_EQ(pop_app_state, POP_APP_PLAYING);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
|
||||
TC_EQ(pop_app_state, POP_APP_PLAYING);
|
||||
}
|
||||
|
||||
TC_TEST(rejects_wrong_event)
|
||||
{
|
||||
pop_app_reset();
|
||||
TC_FALSE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
|
||||
TC_EQ(pop_app_state, POP_APP_BOOT);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_QUIT));
|
||||
TC_EQ(pop_app_state, POP_APP_QUIT);
|
||||
TC_FALSE(pop_app_dispatch(POP_APP_EV_BOOT_READY));
|
||||
}
|
||||
|
||||
TC_TEST(future_routes_are_declared)
|
||||
{
|
||||
pop_app_reset();
|
||||
pop_app_state = POP_APP_TITLE;
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
|
||||
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
|
||||
TC_EQ(pop_app_state, POP_APP_PLAYING);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_TIME_EXPIRED));
|
||||
TC_EQ(pop_app_state, POP_APP_TIME_EXPIRED);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
}
|
||||
|
||||
TC_TEST(prelevel_cutscene_route)
|
||||
{
|
||||
pop_app_state = POP_APP_PLAYING;
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED));
|
||||
TC_EQ(pop_app_state, POP_APP_CUTSCENE);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
|
||||
}
|
||||
|
||||
TC_TEST(demo_routes)
|
||||
{
|
||||
pop_app_reset();
|
||||
pop_app_dispatch(POP_APP_EV_BOOT_READY);
|
||||
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_INTRO_STARTED));
|
||||
TC_EQ(pop_app_state, POP_APP_INTRO);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_DEMO);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_DEMO_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_DEMO_STARTED));
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
|
||||
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
|
||||
}
|
||||
|
||||
TC_TEST(intro_skip_starts_new_game)
|
||||
{
|
||||
pop_app_reset();
|
||||
pop_app_dispatch(POP_APP_EV_BOOT_READY);
|
||||
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
|
||||
pop_app_dispatch(POP_APP_EV_INTRO_STARTED);
|
||||
TC_EQ(pop_app_state, POP_APP_INTRO);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
|
||||
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
|
||||
}
|
||||
|
||||
TC_TEST(restart_game_returns_to_intro)
|
||||
{
|
||||
pop_app_state = POP_APP_PLAYING;
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_PAUSE));
|
||||
TC_EQ(pop_app_state, POP_APP_PAUSE_MENU);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_RESTART_INTRO));
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
}
|
||||
|
||||
/* Финал ВРЕМЕННО ведёт мимо Hall of Fame прямо в title: экран HOF после
|
||||
* победы показывает чёрное, и застревать в нём хуже, чем не показывать
|
||||
* (решение пользователя 2026-08-24, BUGS_OPEN.md#hof-after-ending). Сам
|
||||
* маршрут HOF -> TITLE остаётся живым: в него ходит title-экран. */
|
||||
TC_TEST(ending_to_title_route)
|
||||
{
|
||||
pop_app_state = POP_APP_PLAYING;
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_STARTED));
|
||||
TC_EQ(pop_app_state, POP_APP_ENDING);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
}
|
||||
|
||||
TC_TEST(hof_route_still_exits_to_title)
|
||||
{
|
||||
pop_app_state = POP_APP_HALL_OF_FAME;
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_HOF_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
TC_RUN(boot_to_title);
|
||||
TC_RUN(level_load_roundtrip);
|
||||
TC_RUN(rejects_wrong_event);
|
||||
TC_RUN(future_routes_are_declared);
|
||||
TC_RUN(prelevel_cutscene_route);
|
||||
TC_RUN(demo_routes);
|
||||
TC_RUN(intro_skip_starts_new_game);
|
||||
TC_RUN(restart_game_returns_to_intro);
|
||||
TC_RUN(ending_to_title_route);
|
||||
TC_RUN(hof_route_still_exits_to_title);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* t_cfg — полный codec POP.CFG: заголовок, размер, checksum и payload.
|
||||
*
|
||||
* DSS/EMM под host не живут, но pure codec лежит в том же pop_config.c, что
|
||||
* и target-I/O. Поэтому проверяем весь формат, а не только payload.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "tcheck.h"
|
||||
#include "pop_settings.h"
|
||||
#include "pop_pace.h" /* POP_SPEED_MODES */
|
||||
#include "_pop_config.h"
|
||||
|
||||
/* Runtime-владельцы нужны только для линковки pop_settings_apply(); codec
|
||||
* их не вызывает. */
|
||||
uint8_t pop_speed_mode, pop_cheats, pop_snd_want;
|
||||
void pop_sfx_toggle(void) __banked { pop_snd_want = (uint8_t)!pop_snd_want; }
|
||||
|
||||
static void set_all(uint8_t snd, uint8_t spd, uint8_t prof, uint8_t cheats,
|
||||
uint8_t info, uint16_t flags, uint8_t time_limit,
|
||||
uint8_t debug_bar)
|
||||
{
|
||||
pop_settings.sound_enabled = snd;
|
||||
pop_settings.speed_mode = spd;
|
||||
pop_settings.gameplay_profile = prof;
|
||||
pop_settings.cheats_enabled = cheats;
|
||||
pop_settings.show_build_info = info;
|
||||
pop_settings.enhancement_flags = flags;
|
||||
pop_settings.time_limit_enabled = time_limit;
|
||||
pop_settings.show_debug_bar = debug_bar;
|
||||
}
|
||||
|
||||
/* ser→parse должен возвращать ровно те же значения (кроме профиля:
|
||||
* ENHANCED осознанно садится в VANILLA). */
|
||||
TC_TEST(cfg_roundtrip)
|
||||
{
|
||||
uint8_t buf[POP_CFG_SIZE];
|
||||
set_all(1, POP_SPEED_FAST, POP_PROFILE_VANILLA, 1, 1, 0xBEEF, 0, 0);
|
||||
pop_cfg_encode(buf);
|
||||
memset(&pop_settings, 0, sizeof(pop_settings)); /* стереть */
|
||||
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
|
||||
TC_EQ(pop_settings.sound_enabled, 1);
|
||||
TC_EQ(pop_settings.speed_mode, POP_SPEED_FAST);
|
||||
TC_EQ(pop_settings.cheats_enabled, 1);
|
||||
TC_EQ(pop_settings.show_build_info, 1);
|
||||
TC_EQ(pop_settings.enhancement_flags, 0xBEEF);
|
||||
TC_EQ(pop_settings.time_limit_enabled, 0);
|
||||
TC_EQ(pop_settings.show_debug_bar, 0);
|
||||
}
|
||||
|
||||
/* ENHANCED из файла применяется как VANILLA (режим ещё не реализован). */
|
||||
TC_TEST(cfg_enhanced_downgrades_to_vanilla)
|
||||
{
|
||||
uint8_t buf[POP_CFG_SIZE];
|
||||
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_ENHANCED, 0, 0, 0x0003, 1, 1);
|
||||
pop_cfg_encode(buf);
|
||||
memset(&pop_settings, 0, sizeof(pop_settings));
|
||||
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
|
||||
TC_EQ(pop_settings.gameplay_profile, POP_PROFILE_VANILLA);
|
||||
/* Остальные поля при этом применяются. */
|
||||
TC_EQ(pop_settings.enhancement_flags, 0x0003);
|
||||
}
|
||||
|
||||
/* Мусор в ЛЮБОМ поле => отказ без частичного применения. */
|
||||
TC_TEST(cfg_garbage_rejected_whole)
|
||||
{
|
||||
uint8_t buf[POP_CFG_SIZE];
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < POP_CFG_PAYLOAD_SIZE; i++) {
|
||||
set_all(1, POP_SPEED_FASTEST, POP_PROFILE_VANILLA, 1, 0, 0x0001, 1, 1);
|
||||
pop_cfg_encode(buf);
|
||||
switch (i) {
|
||||
case 0: buf[7 + i] = 2; break; /* sound > 1 */
|
||||
case 1: buf[7 + i] = (uint8_t)POP_SPEED_MODES; break;
|
||||
case 2: buf[7 + i] = 2; break; /* profile > ENHANCED */
|
||||
case 3: buf[7 + i] = 2; break; /* cheats > 1 */
|
||||
case 4: buf[7 + i] = 2; break; /* buildinfo > 1 */
|
||||
case 7: buf[7 + i] = 2; break; /* timer > 1 */
|
||||
case 8: buf[7 + i] = 2; break; /* debug bar > 1 */
|
||||
default: buf[7 + i] = 0xFF; break; /* флаги — любые */
|
||||
}
|
||||
/* Изменённый payload обязан иметь корректную checksum: иначе мы
|
||||
* тестируем checksum, а не диапазон поля. */
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = 0;
|
||||
{
|
||||
uint8_t j;
|
||||
for (j = 0; j < POP_CFG_PAYLOAD_SIZE; j++)
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] ^= buf[7 + j];
|
||||
}
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] =
|
||||
(uint8_t)~buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE];
|
||||
if (i == 5 || i == 6) { /* flags не валидируются */
|
||||
memset(&pop_settings, 0, sizeof(pop_settings));
|
||||
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
|
||||
continue;
|
||||
}
|
||||
memset(&pop_settings, 0, sizeof(pop_settings));
|
||||
TC_FALSE(pop_cfg_decode(buf, sizeof(buf)));
|
||||
/* Настройки не тронуты — нули после memset остались нулями. */
|
||||
TC_EQ(pop_settings.sound_enabled, 0);
|
||||
TC_EQ(pop_settings.speed_mode, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TC_TEST(cfg_header_checksum_and_future_tail)
|
||||
{
|
||||
uint8_t buf[POP_CFG_MAX_SIZE];
|
||||
uint8_t i, sum;
|
||||
set_all(1, POP_SPEED_FAST, POP_PROFILE_VANILLA, 0, 1, 0xA55A, 1, 1);
|
||||
pop_cfg_encode(buf);
|
||||
|
||||
buf[0] = 'X';
|
||||
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
|
||||
pop_cfg_encode(buf);
|
||||
buf[4] = 2;
|
||||
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
|
||||
pop_cfg_encode(buf);
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] ^= 1;
|
||||
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
|
||||
pop_cfg_encode(buf);
|
||||
TC_FALSE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_SIZE - 1)));
|
||||
|
||||
/* Будущий v1: два незнакомых байта перед checksum должны пропуститься. */
|
||||
pop_cfg_encode(buf);
|
||||
buf[5] = POP_CFG_PAYLOAD_SIZE + 2;
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = 0x12;
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] = 0x34;
|
||||
sum = 0;
|
||||
for (i = 0; i < POP_CFG_PAYLOAD_SIZE + 2; i++) sum ^= buf[7 + i];
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 2] = sum;
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 3] = (uint8_t)~sum;
|
||||
memset(&pop_settings, 0, sizeof(pop_settings));
|
||||
TC_TRUE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_SIZE + 2)));
|
||||
TC_EQ(pop_settings.enhancement_flags, 0xA55A);
|
||||
}
|
||||
|
||||
/* Созданный до FG4 файл (семь payload-байтов) остаётся валиден и обязан
|
||||
* включить лимит: это исторический дефолт 60 минут, не значение в _DATA. */
|
||||
TC_TEST(cfg_legacy_enables_time_limit)
|
||||
{
|
||||
uint8_t buf[POP_CFG_SIZE];
|
||||
uint8_t sum = 0, i;
|
||||
|
||||
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_VANILLA, 1, 0, 0, 0, 0);
|
||||
pop_cfg_encode(buf);
|
||||
buf[5] = POP_CFG_LEGACY_PAYLOAD_SIZE;
|
||||
for (i = 0; i < POP_CFG_LEGACY_PAYLOAD_SIZE; i++) sum ^= buf[7 + i];
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_LEGACY_PAYLOAD_SIZE] = sum;
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_LEGACY_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
|
||||
pop_settings.time_limit_enabled = 0;
|
||||
TC_TRUE(pop_cfg_decode(buf, POP_CFG_MIN_SIZE));
|
||||
TC_EQ(pop_settings.time_limit_enabled, 1);
|
||||
}
|
||||
|
||||
/* Файл, записанный ДО тумблера отладочной полосы (восемь payload-байтов),
|
||||
* обязан остаться валидным и включить полосу: это её исторический дефолт. */
|
||||
TC_TEST(cfg_legacy_enables_debug_bar)
|
||||
{
|
||||
uint8_t buf[POP_CFG_SIZE];
|
||||
uint8_t sum = 0, i;
|
||||
|
||||
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_VANILLA, 1, 0, 0, 1, 0);
|
||||
pop_cfg_encode(buf);
|
||||
buf[5] = POP_CFG_TIMELIMIT_PAYLOAD_SIZE;
|
||||
for (i = 0; i < POP_CFG_TIMELIMIT_PAYLOAD_SIZE; i++) sum ^= buf[7 + i];
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_TIMELIMIT_PAYLOAD_SIZE] = sum;
|
||||
buf[POP_CFG_HEADER_SIZE + POP_CFG_TIMELIMIT_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
|
||||
pop_settings.show_debug_bar = 0;
|
||||
TC_TRUE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_HEADER_SIZE +
|
||||
POP_CFG_TIMELIMIT_PAYLOAD_SIZE +
|
||||
POP_CFG_TAIL_SIZE)));
|
||||
TC_EQ(pop_settings.show_debug_bar, 1);
|
||||
TC_EQ(pop_settings.time_limit_enabled, 1);
|
||||
}
|
||||
|
||||
TC_TEST(settings_apply_syncs_runtime)
|
||||
{
|
||||
set_all(0, POP_SPEED_FASTEST, POP_PROFILE_VANILLA, 0, 0, 0, 1, 1);
|
||||
pop_snd_want = 1;
|
||||
pop_settings_apply(1);
|
||||
TC_EQ(pop_speed_mode, POP_SPEED_FASTEST);
|
||||
TC_EQ(pop_cheats, 0);
|
||||
TC_EQ(pop_snd_want, 0);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
TC_RUN(cfg_roundtrip);
|
||||
TC_RUN(cfg_enhanced_downgrades_to_vanilla);
|
||||
TC_RUN(cfg_garbage_rejected_whole);
|
||||
TC_RUN(cfg_header_checksum_and_future_tail);
|
||||
TC_RUN(cfg_legacy_enables_time_limit);
|
||||
TC_RUN(cfg_legacy_enables_debug_bar);
|
||||
TC_RUN(settings_apply_syncs_runtime);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* pop_cutscene — порядок событий, точная семантика WAIT и прерывание. */
|
||||
#include <stdint.h>
|
||||
#include "tcheck.h"
|
||||
#include "pop_cutscene.h"
|
||||
|
||||
static const pop_cs_cmd_t script[] = {
|
||||
{ POP_CS_PALETTE, 1 }, { POP_CS_SHOW, 7 }, { POP_CS_WAIT, 2 },
|
||||
{ POP_CS_FLASH, 3 }, { POP_CS_WAIT, 1 }, { POP_CS_FADE_OUT, 0 },
|
||||
{ POP_CS_END, 0 }
|
||||
};
|
||||
|
||||
TC_TEST(script_events_and_waits)
|
||||
{
|
||||
pop_cutscene_t cs;
|
||||
uint16_t arg;
|
||||
|
||||
pop_cutscene_begin(&cs, script);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_PALETTE); TC_EQ(arg, 1);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_SHOW); TC_EQ(arg, 7);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_FLASH); TC_EQ(arg, 3);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_FADE_OUT);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_END);
|
||||
}
|
||||
|
||||
TC_TEST(skip_aborts_at_any_wait)
|
||||
{
|
||||
pop_cutscene_t cs;
|
||||
uint16_t arg;
|
||||
|
||||
pop_cutscene_begin(&cs, script);
|
||||
pop_cutscene_tick(&cs, &arg);
|
||||
pop_cutscene_tick(&cs, &arg);
|
||||
pop_cutscene_tick(&cs, &arg); /* WAIT(2), ещё есть один кадр */
|
||||
pop_cutscene_abort(&cs);
|
||||
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_END);
|
||||
TC_EQ(cs.ended, 1);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
TC_RUN(script_events_and_waits);
|
||||
TC_RUN(skip_aborts_at_any_wait);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/* pop_demo — запись level 0: смена команд и прерывание любым вводом. */
|
||||
#include <stdint.h>
|
||||
#include "tcheck.h"
|
||||
#include "pop_demo.h"
|
||||
|
||||
static int8_t moves[255];
|
||||
static uint8_t moves_n;
|
||||
static uint8_t any_key;
|
||||
static uint8_t fight_ai;
|
||||
uint8_t hitp_curr;
|
||||
|
||||
uint8_t pop_demo_kid_ai(void) __banked
|
||||
{
|
||||
return fight_ai;
|
||||
}
|
||||
|
||||
void pop_ctrl_auto_move(int8_t move) __banked
|
||||
{
|
||||
moves[moves_n++] = move;
|
||||
}
|
||||
|
||||
uint8_t kbd_raw_any_down(void)
|
||||
{
|
||||
return any_key;
|
||||
}
|
||||
|
||||
TC_TEST(replays_sdlpop_move_table)
|
||||
{
|
||||
uint8_t done = 0;
|
||||
|
||||
moves_n = 0;
|
||||
fight_ai = 0;
|
||||
hitp_curr = 4;
|
||||
pop_demo_begin();
|
||||
TC_FALSE(pop_demo_tick());
|
||||
TC_EQ(moves_n, 1);
|
||||
TC_EQ(moves[0], 0); /* t=1, запись {0, nothing} */
|
||||
while (moves_n < 14) {
|
||||
TC_FALSE(pop_demo_tick());
|
||||
}
|
||||
TC_EQ(moves[1], 1); /* t=2, запись {1, forward} */
|
||||
TC_EQ(moves[11], 1); /* t=12 всё ещё forward */
|
||||
TC_EQ(moves[12], 0); /* t=13, запись {0x0D, nothing} */
|
||||
|
||||
while (!done) done = pop_demo_tick();
|
||||
TC_EQ(moves_n, 254); /* SDLPoP останавливает demo_time на FE */
|
||||
TC_EQ(moves[253], -1); /* хвост после записи {0xE9, -1} */
|
||||
}
|
||||
|
||||
TC_TEST(fight_ai_pauses_scripted_moves)
|
||||
{
|
||||
moves_n = 0;
|
||||
fight_ai = 1;
|
||||
hitp_curr = 4;
|
||||
pop_demo_begin();
|
||||
TC_FALSE(pop_demo_tick());
|
||||
TC_EQ(moves_n, 0); /* во время боя demo_time не идёт */
|
||||
|
||||
fight_ai = 0;
|
||||
TC_FALSE(pop_demo_tick());
|
||||
TC_EQ(moves_n, 1);
|
||||
TC_EQ(moves[0], 0); /* первый scripted тик не потерян */
|
||||
}
|
||||
|
||||
TC_TEST(death_stops_demo_before_combat_ai)
|
||||
{
|
||||
moves_n = 0;
|
||||
fight_ai = 1;
|
||||
hitp_curr = 0;
|
||||
pop_demo_begin();
|
||||
TC_TRUE(pop_demo_tick());
|
||||
TC_EQ(moves_n, 0);
|
||||
}
|
||||
|
||||
TC_TEST(any_raw_key_interrupts_demo)
|
||||
{
|
||||
any_key = 0;
|
||||
TC_FALSE(pop_demo_user_input());
|
||||
any_key = 1;
|
||||
TC_TRUE(pop_demo_user_input());
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
TC_RUN(replays_sdlpop_move_table);
|
||||
TC_RUN(fight_ai_pauses_scripted_moves);
|
||||
TC_RUN(death_stops_demo_before_combat_ai);
|
||||
TC_RUN(any_raw_key_interrupts_demo);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* pop_flow — только канонические pre-level маршруты и ветка scene 12. */
|
||||
#include <stdint.h>
|
||||
#include "tcheck.h"
|
||||
#include "pop_flow.h"
|
||||
|
||||
TC_TEST(only_original_levels_have_pre_scenes)
|
||||
{
|
||||
uint8_t level;
|
||||
|
||||
for (level = 0; level <= 14; level++) {
|
||||
pop_pre_cutscene_t scene = pop_flow_pre_cutscene(level, 60);
|
||||
if (level == 2 || level == 6) TC_EQ(scene, POP_PRE_2_6);
|
||||
else if (level == 4) TC_EQ(scene, POP_PRE_4);
|
||||
else if (level == 8) TC_EQ(scene, POP_PRE_8);
|
||||
else if (level == 9) TC_EQ(scene, POP_PRE_9);
|
||||
else if (level == 12) TC_EQ(scene, POP_PRE_12);
|
||||
else TC_EQ(scene, POP_PRE_NONE);
|
||||
}
|
||||
TC_EQ(pop_flow_pre_cutscene(15, 60), POP_PRE_NONE);
|
||||
}
|
||||
|
||||
TC_TEST(scene_12_short_time_boundary)
|
||||
{
|
||||
TC_EQ(pop_flow_pre_cutscene(12, 6), POP_PRE_12);
|
||||
TC_EQ(pop_flow_pre_cutscene(12, 5), POP_PRE_12_SHORT);
|
||||
TC_EQ(pop_flow_pre_cutscene(12, 0), POP_PRE_12_SHORT);
|
||||
}
|
||||
|
||||
TC_TEST(level_14_is_ending_without_number)
|
||||
{
|
||||
const pop_level_flow_t *flow = pop_flow_level(14);
|
||||
TC_TRUE(flow != 0);
|
||||
TC_FALSE(flow->show_level_number);
|
||||
TC_TRUE(flow->ending_rule);
|
||||
TC_TRUE(pop_flow_level(15) == 0);
|
||||
}
|
||||
|
||||
TC_TEST(ending_room_is_exact)
|
||||
{
|
||||
TC_TRUE(pop_flow_is_ending(14, 5));
|
||||
TC_FALSE(pop_flow_is_ending(14, 4));
|
||||
TC_FALSE(pop_flow_is_ending(13, 5));
|
||||
TC_FALSE(pop_flow_is_ending(15, 5));
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
TC_RUN(only_original_levels_have_pre_scenes);
|
||||
TC_RUN(scene_12_short_time_boundary);
|
||||
TC_RUN(level_14_is_ending_without_number);
|
||||
TC_RUN(ending_room_is_exact);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* t_gate.c — ОТКРЫТЫЕ ворота не должны быть «стеной» для осторожного шага.
|
||||
*
|
||||
* Наблюдение (пользователь, 2026-08-24): перед ОТКРЫТОЙ решёткой Кид,
|
||||
* идущий слева направо, делает лишний мелкий шаг — чтобы подойти к краю,
|
||||
* Shift+вперёд надо нажимать дважды.
|
||||
*
|
||||
* Механика: длину осторожного шага выбирает сам safe_step (seg005:0604) по
|
||||
* числу, которое вернул get_edge_distance — seq 29..42 это «шаг ровно на
|
||||
* distance пикселей». А get_edge_distance для тайла-стены зовёт
|
||||
* dist_from_wall_forward, и та у ОРИГИНАЛА первой строкой отвечает −1, если
|
||||
* тайл — ворота, сквозь которые можно пройти (seg004:0A18:
|
||||
* `if (tiletype == tiles_4_gate && !can_bump_into_gate()) return -1`).
|
||||
* Минус единица уводит разбор дальше, где открытые ворота опознаются как
|
||||
* обычный ПОЛ: distance = 11, полный шаг. Без этой ветки расстояние
|
||||
* считается до створки, тип края — EDGE_WALL, и шаг выходит коротким.
|
||||
*
|
||||
* Проверяем не поведение через кадры, а сам ответ get_edge_distance: он и
|
||||
* есть источник длины шага, и утверждение «открытая решётка == обычный
|
||||
* пол» переживёт любую перенастройку последовательностей.
|
||||
*/
|
||||
#include "tcheck.h"
|
||||
#include "scene.h"
|
||||
#include "stubs.h"
|
||||
#include "pop_kid.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_cdraw.h" /* pop_cd[].fph — высоту кадра ставит отрисовка */
|
||||
|
||||
#define F 1 /* floor */
|
||||
#define G 4 /* gate */
|
||||
#define W 20 /* wall */
|
||||
|
||||
/* Ряд 0: пол до колонки 4, решётка в колонке 5, пол дальше — «коридор с
|
||||
* решёткой» уровня 1 (комната 5) в чистом виде. */
|
||||
static const uint8_t room_gate[30] = {
|
||||
F, F, F, F, F, G, F, F, F, F,
|
||||
W, W, W, W, W, W, W, W, W, W,
|
||||
W, W, W, W, W, W, W, W, W, W,
|
||||
};
|
||||
|
||||
/* Тот же ряд, но вместо решётки обычный пол — эталон «свободного» края. */
|
||||
static const uint8_t room_floor[30] = {
|
||||
F, F, F, F, F, F, F, F, F, F,
|
||||
W, W, W, W, W, W, W, W, W, W,
|
||||
W, W, W, W, W, W, W, W, W, W,
|
||||
};
|
||||
|
||||
/* Кид на колонке 4 лицом вправо, решётка (или пол) — следующим тайлом.
|
||||
* modif — «открытость» ворот: 188 = подняты (проход открыт), 0 = опущены.
|
||||
* Возврат — distance, тип края остаётся в pop_edge_type(). */
|
||||
static int edge_at_gate_x(const uint8_t *fg, uint8_t modif, uint8_t x)
|
||||
{
|
||||
int d;
|
||||
sc_room(fg, 1);
|
||||
tk_modif[5] = modif;
|
||||
if (x) sc_kid_at_x(4, 0, x, 0 /* лицом вправо */);
|
||||
else sc_kid_at(4, 0, 0);
|
||||
/* «Открыты ли ворота» решается сравнением подъёма створки с ВЫСОТОЙ
|
||||
* КАДРА персонажа (can_bump_into_gate, seg004:0373). В продукте её
|
||||
* пишет отрисовка (pop_cdraw), которой в наборе нет, — значит задаём
|
||||
* сами. Порог важен только тем, что 6 (опущенная створка) под
|
||||
* высотой кадра, а 53 (поднятая) над ней. */
|
||||
pop_cd[POP_CH_KID].fpw = 12;
|
||||
pop_cd[POP_CH_KID].fph = 41; /* габарит стойки, снят в MAME */
|
||||
pop_loadkid();
|
||||
d = pop_edge_distance();
|
||||
pop_savekid();
|
||||
return d;
|
||||
}
|
||||
|
||||
static int edge_at_gate(const uint8_t *fg, uint8_t modif)
|
||||
{
|
||||
return edge_at_gate_x(fg, modif, 0);
|
||||
}
|
||||
|
||||
/* ОТКРЫТАЯ решётка — не препятствие: и расстояние, и тип края обязаны
|
||||
* совпасть с обычным полом, иначе осторожный шаг окажется коротким. */
|
||||
TC_TEST(open_gate_is_not_a_wall)
|
||||
{
|
||||
int d_floor, d_gate;
|
||||
uint8_t t_floor, t_gate;
|
||||
|
||||
d_floor = edge_at_gate(room_floor, 0);
|
||||
t_floor = pop_edge_type();
|
||||
d_gate = edge_at_gate(room_gate, 188);
|
||||
t_gate = pop_edge_type();
|
||||
|
||||
TC_EQ(d_gate, d_floor);
|
||||
TC_EQ(t_gate, t_floor);
|
||||
}
|
||||
|
||||
/* ЗАКРЫТАЯ решётка обязана остаться стеной — иначе фикс выше открыл бы
|
||||
* проход сквозь опущенные ворота. */
|
||||
/* ЗАКРЫТАЯ решётка обязана остаться стеной: фикс выше не должен открывать
|
||||
* проход сквозь опущенные ворота. X берём у самой створки — в центре тайла
|
||||
* она ещё «далеко» и честно классифицируется как пол. */
|
||||
TC_TEST(closed_gate_is_a_wall)
|
||||
{
|
||||
int d = edge_at_gate_x(room_gate, 0, 136);
|
||||
TC_EQ(pop_edge_type(), EDGE_WALL);
|
||||
TC_EQ(d < 11, 1);
|
||||
}
|
||||
|
||||
/* ...а ОТКРЫТАЯ на том же X ведёт себя как чистый пол — это и есть длина
|
||||
* осторожного шага: 11, а не обрезок до створки. */
|
||||
TC_TEST(open_gate_steps_like_floor)
|
||||
{
|
||||
int d_gate, d_floor;
|
||||
uint8_t t_gate, t_floor;
|
||||
|
||||
d_gate = edge_at_gate_x(room_gate, 188, 136);
|
||||
t_gate = pop_edge_type();
|
||||
d_floor = edge_at_gate_x(room_floor, 0, 136);
|
||||
t_floor = pop_edge_type();
|
||||
|
||||
TC_EQ(d_gate, d_floor);
|
||||
TC_EQ(t_gate, t_floor);
|
||||
}
|
||||
|
||||
/* ---- check_gate_push (seg004:0833) --------------------------------- *
|
||||
* Кид стоит в тайле решётки, решётка опускается — оригинал ВЫТАЛКИВАЕТ его
|
||||
* из тайла (иначе Кид «в суперпозиции»: створка рисуется то перед ним, то
|
||||
* за ним, и уйти он может в обе стороны). Прогоняем кадр физики и смотрим,
|
||||
* сдвинулся ли X. */
|
||||
static int8_t push_col; /* curr_col, с которым сработал толчок */
|
||||
|
||||
static int push_from_dir(uint8_t x, uint8_t modif, int8_t dir)
|
||||
{
|
||||
sc_room(room_gate, 1);
|
||||
tk_modif[5] = modif;
|
||||
/* Габарит ПОСЛЕДНЕГО НАРИСОВАННОГО кадра — его в продукте пишет
|
||||
* отрисовка (pop_cdraw), которой в наборе нет. Значения сняты с живой
|
||||
* сцены в MAME для стойки Кида: ширина 12, высота 41. */
|
||||
pop_cd[POP_CH_KID].fpw = 12;
|
||||
pop_cd[POP_CH_KID].fph = 41;
|
||||
sc_kid_at_x(5, 0, x, dir);
|
||||
sc_run(0, 1); /* кадр «стоим» — флаги перекрытия за прошлый */
|
||||
push_col = Kid.curr_col;
|
||||
sc_run(0, 1); /* ...и за этот: check_gate_push смотрит оба */
|
||||
return (int)Kid.x - (int)x;
|
||||
}
|
||||
|
||||
static int push_from(uint8_t x, uint8_t modif)
|
||||
{
|
||||
return push_from_dir(x, modif, 0);
|
||||
}
|
||||
|
||||
/* СТОРОНА толчка — формула оригинала `Char.x += 5 - (orig_col <= tile_col) * 10`:
|
||||
* пока тайл ПЕРСОНАЖА и есть решётка, его выталкивает ВЛЕВО (−5); как только
|
||||
* персонаж отнесён к следующей колонке (решётка стала левым соседом) — ВПРАВО
|
||||
* (+5). Граница между сторонами, стало быть, ровно там, где меняется
|
||||
* curr_col, и проверяется здесь на обоих направлениях взгляда.
|
||||
*
|
||||
* Решётка — колонка 5, значит push_col == 5 обязан давать −5, push_col == 6
|
||||
* (+5), а вне зоны касания створки толчка нет вовсе. */
|
||||
static uint8_t check_side(int8_t dir, uint8_t *nleft, uint8_t *nright)
|
||||
{
|
||||
uint8_t x, bad = 0;
|
||||
for (x = 130; x <= 162; x++) {
|
||||
int dx = push_from_dir(x, 0, dir);
|
||||
if (dx == 0) continue;
|
||||
/* Величина — кратная 5: пока персонаж остаётся в зоне створки,
|
||||
* толчок повторяется каждый кадр (так же и в оригинале), поэтому
|
||||
* проверяем СТОРОНУ и шаг, а не одно конкретное число. */
|
||||
if (push_col == 5) { if (dx >= 0 || dx % 5) bad++; else (*nleft)++; }
|
||||
else if (push_col == 6) { if (dx <= 0 || dx % 5) bad++; else (*nright)++; }
|
||||
else bad++; /* толчок вне тайлов решётки — ошибка */
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
TC_TEST(closing_gate_pushes_kid)
|
||||
{
|
||||
uint8_t nleft = 0, nright = 0, bad;
|
||||
|
||||
bad = check_side(0, &nleft, &nright); /* лицом вправо */
|
||||
bad += check_side(-1, &nleft, &nright); /* лицом влево */
|
||||
|
||||
TC_EQ(bad, 0);
|
||||
TC_EQ(nleft > 0, 1); /* обе стороны реально есть */
|
||||
TC_EQ(nright > 0, 1);
|
||||
}
|
||||
|
||||
/* ОТКРЫТУЮ решётку выталкивание не трогает вовсе (can_bump_into_gate). */
|
||||
TC_TEST(open_gate_does_not_push)
|
||||
{
|
||||
uint8_t x, moved = 0;
|
||||
for (x = 130; x <= 162; x++)
|
||||
if (push_from_dir(x, 188, 0) != 0) moved++;
|
||||
TC_EQ(moved, 0);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
sc_init();
|
||||
TC_RUN(open_gate_is_not_a_wall);
|
||||
TC_RUN(closed_gate_is_a_wall);
|
||||
TC_RUN(open_gate_steps_like_floor);
|
||||
TC_RUN(closing_gate_pushes_kid);
|
||||
TC_RUN(open_gate_does_not_push);
|
||||
return 0;
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "stubs.h"
|
||||
#include "pop_kid.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_ctrl.h"
|
||||
#include "pop_guard.h" /* pop_do_delta_hp — урон сводится дельтой */
|
||||
|
||||
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
|
||||
@@ -166,6 +167,19 @@ TC_TEST(phys_turn_around)
|
||||
SC_CHECK(golden_turn, "turn");
|
||||
}
|
||||
|
||||
TC_TEST(demo_backward_is_not_screen_mirrored)
|
||||
{
|
||||
/* demo_moves уже задаёт forward/backward относительно Кида. На плите
|
||||
* 0,6 он смотрит вправо, и move=2 обязан начать seq_5 turn, а не
|
||||
* превратиться через user_control() в seq_1 run вправо к пропасти. */
|
||||
sc_room(room_flat, 1);
|
||||
sc_kid_at(4, 1, 0);
|
||||
pop_ctrl_auto_move(0);
|
||||
pop_ctrl_auto_move(2);
|
||||
/* SEQTBL_BASE+0x01CB — начало seq_5 в исходном kid_data.bin. */
|
||||
TC_EQ((uint16_t)(Kid.curr_seq - SEQTBL_BASE), 0x01CB);
|
||||
}
|
||||
|
||||
TC_TEST(phys_crouch_and_stand_up)
|
||||
{
|
||||
sc_room(room_flat, 1);
|
||||
@@ -354,6 +368,7 @@ void main(void)
|
||||
TC_RUN(phys_run_right);
|
||||
TC_RUN(phys_step_with_shift);
|
||||
TC_RUN(phys_turn_around);
|
||||
TC_RUN(demo_backward_is_not_screen_mirrored);
|
||||
TC_RUN(phys_crouch_and_stand_up);
|
||||
TC_RUN(phys_jump_up_nothing_above);
|
||||
TC_RUN(phys_run_off_ledge);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/* pop_timer — логические тики, pause/unlimited, читы и QuickSave state. */
|
||||
#include <stdint.h>
|
||||
#include "tcheck.h"
|
||||
#include "pop_timer.h"
|
||||
|
||||
/* Минимальный memory-stream вместо EMM. pop_timer_qsave использует ровно
|
||||
* u8/u16, поэтому этот stub одновременно проверяет его endian-контракт. */
|
||||
static uint8_t stream[3];
|
||||
static uint8_t stream_pos;
|
||||
|
||||
void pop_qs_u8(pop_qs_io_t *io, uint8_t *value)
|
||||
{
|
||||
if (io->reading) {
|
||||
if (io->apply) *value = stream[stream_pos];
|
||||
} else {
|
||||
stream[stream_pos] = *value;
|
||||
}
|
||||
++stream_pos;
|
||||
}
|
||||
|
||||
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value)
|
||||
{
|
||||
uint8_t lo, hi;
|
||||
if (io->reading) {
|
||||
lo = stream[stream_pos++]; hi = stream[stream_pos++];
|
||||
if (io->apply) *value = (uint16_t)(lo | ((uint16_t)hi << 8));
|
||||
} else {
|
||||
stream[stream_pos++] = (uint8_t)*value;
|
||||
stream[stream_pos++] = (uint8_t)(*value >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
TC_TEST(timer_starts_and_only_runs_when_allowed)
|
||||
{
|
||||
pop_timer_new_game();
|
||||
TC_EQ(pop_timer_minutes, POP_TIMER_START_MINUTES);
|
||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||
TC_FALSE(pop_timer_tick(0, 1)); /* Settings: unlimited */
|
||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||
TC_FALSE(pop_timer_tick(1, 0)); /* pause/HDD/cutscene */
|
||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||
|
||||
pop_timer_minutes = 2;
|
||||
pop_timer_ticks = 2;
|
||||
TC_FALSE(pop_timer_tick(1, 1));
|
||||
TC_EQ(pop_timer_minutes, 2);
|
||||
TC_EQ(pop_timer_ticks, 1);
|
||||
TC_FALSE(pop_timer_tick(1, 1));
|
||||
TC_EQ(pop_timer_minutes, 1);
|
||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||
}
|
||||
|
||||
TC_TEST(timer_expiry_is_one_shot)
|
||||
{
|
||||
pop_timer_minutes = 1;
|
||||
pop_timer_ticks = 1;
|
||||
TC_TRUE(pop_timer_tick(1, 1));
|
||||
TC_EQ(pop_timer_minutes, 0);
|
||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||
TC_FALSE(pop_timer_tick(1, 1));
|
||||
TC_FALSE(pop_timer_tick(0, 1));
|
||||
}
|
||||
|
||||
TC_TEST(timer_cheats_match_sdlpop_rules)
|
||||
{
|
||||
pop_timer_minutes = 5;
|
||||
pop_timer_cheat_subtract();
|
||||
TC_EQ(pop_timer_minutes, 4);
|
||||
pop_timer_minutes = 1;
|
||||
pop_timer_cheat_subtract();
|
||||
TC_EQ(pop_timer_minutes, 1); /* minus не создаёт expiry */
|
||||
pop_timer_minutes = 0;
|
||||
pop_timer_cheat_add();
|
||||
TC_EQ(pop_timer_minutes, 1);
|
||||
pop_timer_minutes = 0xFF;
|
||||
pop_timer_cheat_add();
|
||||
TC_EQ(pop_timer_minutes, 0xFF); /* не сворачиваемся в 0 */
|
||||
}
|
||||
|
||||
TC_TEST(timer_qsave_restores_exact_state_and_rejects_bad_tick)
|
||||
{
|
||||
pop_qs_io_t io;
|
||||
|
||||
pop_timer_minutes = 42;
|
||||
pop_timer_ticks = 103;
|
||||
stream_pos = 0;
|
||||
io.reading = io.apply = io.failed = 0;
|
||||
pop_timer_qsave(&io);
|
||||
TC_EQ(stream_pos, 3);
|
||||
|
||||
pop_timer_minutes = 7;
|
||||
pop_timer_ticks = 400;
|
||||
stream_pos = 0;
|
||||
io.reading = io.apply = 1; io.failed = 0;
|
||||
pop_timer_qsave(&io);
|
||||
TC_FALSE(io.failed);
|
||||
TC_EQ(pop_timer_minutes, 42);
|
||||
TC_EQ(pop_timer_ticks, 103);
|
||||
|
||||
stream[0] = 9; stream[1] = 0; stream[2] = 0; /* tick 0 — impossible */
|
||||
pop_timer_minutes = 33;
|
||||
pop_timer_ticks = 444;
|
||||
stream_pos = 0; io.failed = 0;
|
||||
pop_timer_qsave(&io);
|
||||
TC_TRUE(io.failed);
|
||||
TC_EQ(pop_timer_minutes, 33);
|
||||
TC_EQ(pop_timer_ticks, 444);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
TC_RUN(timer_starts_and_only_runs_when_allowed);
|
||||
TC_RUN(timer_expiry_is_one_shot);
|
||||
TC_RUN(timer_cheats_match_sdlpop_rules);
|
||||
TC_RUN(timer_qsave_restores_exact_state_and_rejects_bad_tick);
|
||||
}
|
||||
Reference in New Issue
Block a user