Files
Sprinter-SDCC/applications/SprPoP/src/pop_qsave.c
T

263 lines
11 KiB
C

/*
* pop_qsave.c — файловая транзакция и схема игрового снимка.
* Живёт в холодном банке: в обычном кадре исполняется только проверка
* резидентного флага запроса, вся тяжёлая работа бывает по F6/F9.
*/
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h> /* rename */
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_kid.h"
#include "pop_guard.h"
#include "pop_map.h"
#include "pop_level.h"
#include "pop_trob.h"
#include "pop_bg.h"
#include "pop_state.h"
#include "pop_tile.h"
#include "pop_sfx.h"
#include "pop_pace.h"
#include "pop_timer.h"
#include "sprpop_cold.h"
#include "_pop_file.h"
#define QS_VERSION 4
#define QS_SAVE 1
#define QS_LOAD 2
#define QS_LOAD_BACKUP 3
#define QS_LIMIT 4096
#define QS_BASE 0x100
extern uint8_t pop_qsave_request;
static void qs_char(pop_qs_io_t *io, pop_char_t *c)
{
pop_qs_u8(io, &c->frame); pop_qs_u8(io, &c->x); pop_qs_u8(io, &c->y);
pop_qs_i8(io, &c->direction); pop_qs_i8(io, &c->curr_col);
pop_qs_i8(io, &c->curr_row); pop_qs_u8(io, &c->action);
pop_qs_i8(io, &c->fall_x); pop_qs_i8(io, &c->fall_y);
pop_qs_u8(io, &c->room); pop_qs_u8(io, &c->repeat);
pop_qs_u8(io, &c->charid); pop_qs_u8(io, &c->sword);
pop_qs_i8(io, &c->alive); pop_qs_u16(io, &c->curr_seq);
}
static void qs_payload(pop_qs_io_t *io)
{
uint8_t speed=pop_speed_mode, immortal=pop_immortal, sound=pop_snd_want;
/* Пользовательские параметры входят в снимок, а не остаются свойством
* текущего запуска. Читаем сначала во временные байты: повреждённые
* значения не должны частично применить остальное игровое состояние. */
pop_qs_u8(io, &speed); pop_qs_u8(io, &immortal); pop_qs_u8(io, &sound);
if (io->reading && io->apply) {
if (speed >= POP_SPEED_MODES || immortal > 2 || sound > 1) {
io->failed=1; return;
}
pop_speed_mode=speed; pop_immortal=immortal; pop_snd_want=sound;
}
/* Глобальный timer сохраняется до состояния уровня: QuickLoad на том же
* уровне вернёт ровно тот логический кадр, без «бесплатной» минуты. */
pop_timer_qsave(io);
qs_char(io, &Kid); qs_char(io, &Guard);
pop_qs_u8(io, &cur_room); pop_qs_u8(io, &kid_room);
pop_qs_u8(io, &pop_checkpoint); pop_qs_u8(io, &pop_next_level);
pop_qs_i8(io, &knock);
pop_qs_u8(io, &hitp_curr); pop_qs_u8(io, &hitp_max);
pop_qs_u8(io, &hitp_beg_lev); pop_qs_i8(io, &hitp_delta);
pop_qs_u8(io, &guardhp_curr); pop_qs_u8(io, &guardhp_max);
pop_qs_i8(io, &guardhp_delta); pop_qs_u8(io, &guard_skill);
pop_qs_i8(io, &guard_refrac); pop_qs_i8(io, &justblocked);
pop_qs_i8(io, &kid_sword_strike); pop_qs_i8(io, &offguard);
pop_qs_i8(io, &holding_sword); pop_qs_u8(io, &pop_guard_hurt);
pop_qs_i8(io, &can_guard_see_kid); pop_qs_i8(io, &is_guard_notice);
pop_qs_i8(io, &pop_united_shadow); pop_qs_u8(io, &pop_shadow_init);
pop_qs_i8(io, &pop_guard_notice_timer);
pop_qs_u16(io, &pop_fight_seed.lo); pop_qs_u16(io, &pop_fight_seed.hi);
pop_qs_u8(io, &pop_kid_dead); pop_qs_u8(io, &pop_kid_hurt);
pop_qs_u8(io, &pop_have_sword); pop_qs_u8(io, &pop_item_taken);
pop_qs_u8(io, &pop_flash_time); pop_qs_u8(io, &pop_flash_color);
pop_qs_u8(io, &pop_feather); pop_qs_u8(io, &pop_upside);
pop_qs_u8(io, &pop_upside_want); pop_qs_u8(io, &pop_upside_dirty);
pop_qs_u8(io, &pop_leave_dir); pop_qs_u8(io, &pop_leave_timer);
pop_qs_u8(io, &pop_loose_fell); pop_qs_u8(io, &pop_ceil_fell);
pop_qs_u8(io, &pop_debris_at); pop_qs_u8(io, &pop_fell_out);
pop_qs_bytes(io, pop_loose_modif, sizeof(pop_loose_modif));
pop_qs_bytes(io, pop_ceil_modif, sizeof(pop_ceil_modif));
pop_qs_u8(io, &pop_loose_landed); pop_qs_u8(io, &pop_loose_exit);
pop_qs_u8(io, &pop_loose_exit_room); pop_qs_u8(io, &pop_droppedout);
pop_qs_u16(io, &pop_leveldoor_open); pop_qs_i8(io, &pop_jumped_mirror);
pop_qs_u8(io, &pop_seamless); pop_qs_u8(io, &pop_nav_hold);
pop_level_qsave(io);
pop_trob_qsave(io);
pop_loose_mob_qsave(io);
pop_map_qsave(io);
pop_guard_ai_qsave(io);
}
static void qs_init(pop_qs_io_t *io, uint8_t page, uint16_t limit,
uint8_t reading, uint8_t apply)
{
io->page=page; io->pos=QS_BASE; io->limit=(uint16_t)(QS_BASE+limit);
io->checksum=0;
io->count=0; io->reading=reading; io->apply=apply; io->failed=0;
}
static uint8_t qs_header_read(pop_qs_io_t *io, uint8_t *level)
{
uint8_t h[7];
if (!pop_qs_raw_get(io,h,sizeof(h))) return 0;
if (h[0]!='P' || h[1]!='O' || h[2]!='P' || h[3]!='Q' ||
h[4] != QS_VERSION || h[5] < POP_LEVEL_FIRST ||
h[5] > POP_LEVEL_LAST || h[6] != 0) return 0;
*level = h[5]; return 1;
}
static uint8_t qs_validate_page(uint8_t page, uint16_t size,
uint8_t *level, uint16_t *payload_size)
{
pop_qs_io_t io;
uint8_t tail[4];
uint16_t n, sum;
if (size < 11) return 0;
qs_init(&io,page,size,1,0);
if (!qs_header_read(&io,level)) return 0;
io.pos=(uint16_t)(QS_BASE+size-4);
if (!pop_qs_raw_get(&io,tail,4)) return 0;
n = (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8));
sum = (uint16_t)(tail[2] | ((uint16_t)tail[3] << 8));
if (n != (uint16_t)(size-11) ||
sum != pop_qs_checksum_page(page,QS_BASE+7,n)) return 0;
*payload_size=n; return 1;
}
static uint8_t qs_apply(uint8_t page, uint16_t size, uint16_t payload_size,
uint8_t *level)
{
pop_qs_io_t io;
qs_init(&io,page,size,1,1);
if (!qs_header_read(&io,level)) return 0;
qs_payload(&io);
return (uint8_t)(!io.failed && io.count == payload_size &&
io.pos == (uint16_t)(QS_BASE+7+payload_size));
}
static int8_t qs_save(uint8_t page)
{
pop_qs_io_t io;
uint8_t tail[4];
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
uint16_t size;
int rc;
qs_init(&io,page,QS_LIMIT,0,0);
pop_qs_raw_put(&io,h,sizeof(h));
qs_payload(&io);
tail[0]=(uint8_t)io.count; tail[1]=(uint8_t)(io.count >> 8);
io.checksum=pop_qs_checksum_page(page,QS_BASE+7,io.count);
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);
if (io.failed) return -1;
POP_PATH_CALL(rc, unlink("POP.NEW"));
POP_PATH_CALL(rc, bank_save_file(page,QS_BASE,"POP.NEW",size));
if (rc != size) {
POP_PATH_CALL(rc, unlink("POP.NEW"));
return -1;
}
POP_PATH_CALL(rc, unlink("POP.BAK"));
POP_PATH_CALL(rc, rename("POP.SAV", "POP.BAK"));
POP_PATH_CALL(rc, rename("POP.NEW", "POP.SAV"));
if (rc != 0) {
POP_PATH_CALL(rc, rename("POP.BAK", "POP.SAV"));
return -1;
}
return 1;
}
static int8_t qs_load_one(const char *name, uint8_t page)
{
uint8_t level;
uint16_t payload_size;
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,
&payload_size)) return -1;
if (level != pop_current_level) {
pop_next_level = level;
if (pop_level_switch() != 0 || pop_current_level != level) return -1;
}
if (!qs_apply(page,(uint16_t)size,payload_size,&level)) return -1;
Char = Kid; Opp = Guard;
pop_qsave_restore_room(cur_room);
return 1;
}
/* Проверка слотов для меню. Это именно probe: не вызывает pop_level_switch,
* не применяет payload и не трогает звук. Отдельная EMM-страница делает
* проверку независимой от живой страницы уровня. */
uint8_t pop_qsave_probe(void) __banked
{
uint8_t blk, page, level, slots = 0;
uint16_t payload_size;
int size;
blk = mem_alloc_pages(1);
if (!blk) return 0;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
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,
&payload_size))
slots |= POP_QS_SLOT_SAVE;
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,
&payload_size))
slots |= POP_QS_SLOT_BACKUP;
mem_free_block(blk);
return slots;
}
int8_t pop_qsave_process(void) __banked
{
uint8_t request = pop_qsave_request;
uint8_t blk,page;
int8_t rc=-1;
pop_qsave_request = 0;
if (!request) return 0;
/* Лейбл — ДО дисковой работы: она длится доли секунды, игра на это
* время замирает, и строка обязана объяснить фриз, а не подводить под
* ним итог. Не удалось — ниже перепишем на NO QUICKSAVE/QUICKLOAD. */
pop_status_show_now((uint8_t)(request == QS_SAVE ? POP_MSG_QUICKSAVE
: POP_MSG_QUICKLOAD));
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
gfx_w0_page_prepare(page);
/* Звук не глушим (2026-08-28). Единственное место из снятых, где по
* диску реально едут 16 КБ (~33 мс, три периода насоса): если на F6/F9
* появится скрежет — вернуть сюда pop_sfx_pause/pop_sfx_start, это
* лечится точечно и только здесь. */
if (request == QS_SAVE) rc = qs_save(page);
else if (request == QS_LOAD_BACKUP) rc = qs_load_one("POP.BAK", page);
else {
rc = qs_load_one("POP.SAV",page);
if (rc < 0) rc = qs_load_one("POP.BAK",page);
}
mem_free_block(blk);
/* Успех ничего не меняет: нужная строка висит с начала операции.
* Отказ переписывает её на NO QUICKSAVE/NO QUICKLOAD — обычной
* заявкой, кадр её подхватит (порт seg000:497/514). */
if (rc <= 0)
pop_status_msg = (uint8_t)(request == QS_SAVE ? POP_MSG_NO_QUICKSAVE
: POP_MSG_NO_QUICKLOAD);
return rc;
}