savegame: move common logic to TRX

This moves the bulk of savegame logic to TRX.
This commit is contained in:
lahm86 2025-04-09 21:13:45 +01:00
parent af29ece2f7
commit 57bc7610b4
15 changed files with 706 additions and 1129 deletions

View file

@ -1,9 +1,163 @@
#include "benchmark.h"
#include "config.h"
#include "debug.h"
#include "enum_map.h"
#include "game/game.h"
#include "game/game_flow.h"
#include "game/gun/const.h"
#include "game/inventory.h"
#include "game/lara.h"
#include "game/objects.h"
#include "game/objects/traps/movable_block.h"
#include "game/pathing/lot.h"
#include "game/savegame.h"
#include "log.h"
#include "memory.h"
#define MAX_STRATEGIES 2
#define SAVES_DIR "saves"
static SAVEGAME_VERSION m_InitialVersion = VERSION_LEGACY;
static SAVEGAME_INFO *m_SavegameInfo = nullptr;
static RESUME_INFO *m_ResumeInfo = nullptr;
static STATS_COMMON *m_DefaultStats = nullptr;
static int32_t m_SaveSlots = 0;
static int32_t m_SavedGames = 0;
static int32_t m_SaveCounter = 0;
static int32_t m_NewestSlot = -1;
static int32_t m_BoundSlot = -1;
static int32_t m_StrategyCount = 0;
static SAVEGAME_STRATEGY m_Strategies[MAX_STRATEGIES];
static void M_ClearSlots(void);
static bool M_FillSlot(
SAVEGAME_STRATEGY strategy, int32_t slot_num, const char *path);
static void M_ScanSavedGamesDir(const char *dir_path);
static void M_LoadPreprocess(void);
static void M_LoadPostprocess(void);
static void M_ClearSlots(void)
{
if (m_SavegameInfo == nullptr) {
return;
}
for (int32_t i = 0; i < m_SaveSlots; i++) {
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i];
savegame_info->format = SAVEGAME_FORMAT_INVALID;
savegame_info->counter = -1;
savegame_info->level_num = -1;
Memory_FreePointer(&savegame_info->full_path);
Memory_FreePointer(&savegame_info->level_title);
}
}
static bool M_FillSlot(
const SAVEGAME_STRATEGY strategy, const int32_t slot_num,
const char *const path)
{
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
if (strategy.format <= savegame_info->format) {
return true;
}
bool result = false;
MYFILE *const fp = File_Open(path, FILE_OPEN_READ);
if (fp != nullptr) {
if (strategy.fill_info_func(fp, savegame_info)) {
savegame_info->format = strategy.format;
Memory_FreePointer(&savegame_info->full_path);
savegame_info->full_path = Memory_DupStr(path);
result = true;
}
File_Close(fp);
}
return result;
}
static void M_ScanSavedGamesDir(const char *const dir_path)
{
void *const dir_handle = File_OpenDirectory(dir_path);
if (dir_handle == nullptr) {
return;
}
while (true) {
const char *const file_name = File_ReadDirectory(dir_handle);
if (file_name == nullptr) {
break;
}
if (strcmp(file_name, ".") == 0 || strcmp(file_name, "..") == 0) {
continue;
}
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (!strategy.allow_load) {
continue;
}
int32_t slot = -1;
const int32_t parsed =
sscanf(file_name, strategy.get_save_file_pattern_func(), &slot);
if (parsed == 1 && slot >= 0 && slot < m_SaveSlots) {
char *file_path = String_Format("%s/%s", dir_path, file_name);
M_FillSlot(strategy, slot, file_path);
Memory_FreePointer(&file_path);
}
}
}
File_CloseDirectory(dir_handle);
}
static void M_LoadPreprocess(void)
{
Savegame_InitCurrentInfo();
}
static void M_LoadPostprocess(void)
{
// TODO: tidy this; skidoo drivers currently require handle_save_func to be
// called immediately on load within the strategies.
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->save_position && obj->shadow_size) {
int16_t room_num = item->room_num;
const SECTOR *const sector = Room_GetSector(
item->pos.x, item->pos.y, item->pos.z, &room_num);
item->floor =
Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z);
}
if (obj->save_flags != 0) {
item->flags &= 0xFF00;
}
#if TR_VERSION == 1
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD);
}
#endif
}
MovableBlock_SetupFloor();
LARA_INFO *const lara = Lara_GetLaraInfo();
#if TR_VERSION == 1
if (Game_GetBonusFlag() != GBF_NONE) {
g_Config.profile.new_game_plus_unlock = true;
}
LOT_ClearLOT(&lara->lot);
#else
if (lara->burn) {
lara->burn = 0;
Lara_CatchFire();
}
#endif
}
SAVEGAME_VERSION Savegame_GetInitialVersion(void)
{
return m_InitialVersion;
@ -30,3 +184,461 @@ int32_t Savegame_GetBoundSlot(void)
{
return m_BoundSlot;
}
int32_t Savegame_GetLevelNumber(const int32_t slot_num)
{
return m_SavegameInfo[slot_num].level_num;
}
bool Savegame_IsSlotFree(const int32_t slot_num)
{
return m_SavegameInfo[slot_num].level_num == -1;
}
int32_t Savegame_GetCounter(void)
{
return m_SaveCounter;
}
int32_t Savegame_GetTotalCount(void)
{
return m_SavedGames;
}
int32_t Savegame_GetHighestSlot(void)
{
return m_NewestSlot;
}
bool Savegame_RestartAvailable(const int32_t slot_num)
{
#if TR_VERSION == 1
if (slot_num == -1) {
return true;
}
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
return savegame_info->features.restart;
#else
return false;
#endif
}
void Savegame_RegisterStrategy(const SAVEGAME_STRATEGY strategy)
{
ASSERT(m_StrategyCount < MAX_STRATEGIES);
m_Strategies[m_StrategyCount] = strategy;
m_StrategyCount++;
}
void Savegame_Init(void)
{
m_ResumeInfo = Memory_Alloc(
sizeof(RESUME_INFO)
* (GF_GetLevelTable(GFLT_MAIN)->count
+ GF_GetLevelTable(GFLT_DEMOS)->count));
m_SaveSlots = Savegame_GetSlotCount();
m_SavegameInfo = Memory_Alloc(sizeof(SAVEGAME_INFO) * m_SaveSlots);
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS);
for (int32_t i = 0; i < level_table->count; i++) {
RESUME_INFO *const resume_info =
Savegame_GetCurrentInfo(&level_table->levels[i]);
resume_info->flags.available = 1;
resume_info->flags.has_pistols = 1;
resume_info->pistol_ammo = 1000;
resume_info->gun_status = LGS_ARMLESS;
resume_info->equipped_gun_type = LGT_PISTOLS;
#if TR_VERSION == 1
resume_info->holsters_gun_type = LGT_PISTOLS;
resume_info->back_gun_type = LGT_UNARMED;
resume_info->lara_hitpoints = LARA_MAX_HITPOINTS;
#endif
}
}
bool Savegame_IsInitialised(void)
{
return m_SavegameInfo != nullptr;
}
void Savegame_Shutdown(void)
{
M_ClearSlots();
Memory_FreePointer(&m_ResumeInfo);
Memory_FreePointer(&m_SavegameInfo);
Memory_FreePointer(&m_DefaultStats);
}
RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level)
{
ASSERT(m_ResumeInfo != nullptr);
ASSERT(level != nullptr);
if (GF_GetLevelTableType(level->type) == GFLT_MAIN) {
return &m_ResumeInfo[level->num];
} else if (level->type == GFL_DEMO) {
return &m_ResumeInfo[GF_GetLevelTable(GFLT_MAIN)->count];
}
LOG_WARNING(
"Warning: unable to get resume info for level %d (type=%s)", level->num,
ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type));
return nullptr;
}
void Savegame_SetCurrentInfo(const int32_t current_slot, const int32_t src_slot)
{
m_ResumeInfo[current_slot] = m_ResumeInfo[src_slot];
}
const SAVEGAME_INFO *Savegame_GetSavegameInfo(const int32_t slot_num)
{
return &m_SavegameInfo[slot_num];
}
void Savegame_InitCurrentInfo(void)
{
// TODO: remove both NG+ checks in this function once the TR1 NG options are
// ported to TR2.
if (TR_VERSION == 2 && Game_IsBonusFlagSet(GBF_NGPLUS)) {
return;
}
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
Savegame_ResetCurrentInfo(level);
Savegame_ApplyLogicToCurrentInfo(level);
Savegame_GetCurrentInfo(level)->flags.available = 0;
}
if (GF_GetGymLevel() != nullptr) {
Savegame_GetCurrentInfo(GF_GetGymLevel())->flags.available = 1;
}
if (GF_GetFirstLevel() != nullptr) {
Savegame_GetCurrentInfo(GF_GetFirstLevel())->flags.available = 1;
}
#if TR_VERSION == 2
Game_SetBonusFlag(GBF_NONE);
#endif
}
void Savegame_ResetCurrentInfo(const GF_LEVEL *const level)
{
LOG_INFO("Resetting resume info for level #%d", level->num);
RESUME_INFO *const current = Savegame_GetCurrentInfo(level);
memset(current, 0, sizeof(RESUME_INFO));
}
void Savegame_CarryCurrentInfoToNextLevel(
const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level)
{
LOG_INFO(
"Copying resume info from level #%d to level #%d", src_level->num,
dst_level->num);
RESUME_INFO *const src_resume = Savegame_GetCurrentInfo(src_level);
RESUME_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level);
memcpy(dst_resume, src_resume, sizeof(RESUME_INFO));
}
void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level)
{
RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);
LARA_INFO *const lara = Lara_GetLaraInfo();
#if TR_VERSION == 1
resume->lara_hitpoints = Lara_GetItem()->hit_points;
#endif
resume->flags.available = 1;
resume->small_medipacks = Inv_RequestItem(O_SMALL_MEDIPACK_ITEM);
resume->large_medipacks = Inv_RequestItem(O_LARGE_MEDIPACK_ITEM);
resume->pistol_ammo = 1000;
if (Inv_RequestItem(O_PISTOL_ITEM)) {
resume->flags.has_pistols = 1;
} else {
resume->flags.has_pistols = 0;
}
if (Inv_RequestItem(O_SHOTGUN_ITEM)) {
resume->flags.has_shotgun = 1;
resume->shotgun_ammo = lara->shotgun_ammo.ammo;
} else {
resume->flags.has_shotgun = 0;
resume->shotgun_ammo =
Inv_RequestItem(O_SHOTGUN_AMMO_ITEM) * SHOTGUN_AMMO_QTY;
}
if (Inv_RequestItem(O_MAGNUM_ITEM)) {
resume->flags.has_magnums = 1;
resume->magnum_ammo = lara->magnum_ammo.ammo;
} else {
resume->flags.has_magnums = 0;
resume->magnum_ammo =
Inv_RequestItem(O_MAGNUM_AMMO_ITEM) * MAGNUM_AMMO_QTY;
}
if (Inv_RequestItem(O_UZI_ITEM)) {
resume->flags.has_uzis = 1;
resume->uzi_ammo = lara->uzi_ammo.ammo;
} else {
resume->flags.has_uzis = 0;
resume->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM) * UZI_AMMO_QTY;
}
#if TR_VERSION == 1
resume->num_scions = Inv_RequestItem(O_SCION_ITEM_1);
resume->equipped_gun_type = lara->gun_type;
resume->holsters_gun_type = lara->holsters_gun_type;
resume->back_gun_type = lara->back_gun_type;
if (lara->gun_status == LGS_READY) {
resume->gun_status = LGS_READY;
} else {
resume->gun_status = LGS_ARMLESS;
}
#elif TR_VERSION == 2
if (Inv_RequestItem(O_M16_ITEM)) {
resume->flags.has_m16 = 1;
resume->m16_ammo = lara->m16_ammo.ammo;
} else {
resume->flags.has_m16 = 0;
resume->m16_ammo = Inv_RequestItem(O_M16_AMMO_ITEM) * M16_AMMO_QTY;
}
if (Inv_RequestItem(O_HARPOON_ITEM)) {
resume->flags.has_harpoon = 1;
resume->harpoon_ammo = lara->harpoon_ammo.ammo;
} else {
resume->flags.has_harpoon = 0;
resume->harpoon_ammo =
Inv_RequestItem(O_HARPOON_AMMO_ITEM) * HARPOON_AMMO_QTY;
}
if (Inv_RequestItem(O_GRENADE_ITEM)) {
resume->flags.has_grenade = 1;
resume->grenade_ammo = lara->grenade_ammo.ammo;
} else {
resume->flags.has_grenade = 0;
resume->grenade_ammo =
Inv_RequestItem(O_GRENADE_AMMO_ITEM) * GRENADE_AMMO_QTY;
}
resume->flares = Inv_RequestItem(O_FLARE_ITEM);
if (lara->gun_type == LGT_FLARE) {
resume->equipped_gun_type = lara->last_gun_type;
} else {
resume->equipped_gun_type = lara->gun_type;
}
resume->gun_status = LGS_ARMLESS;
#endif
}
void Savegame_ProcessItemsBeforeSave(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_SAVE);
}
}
}
void Savegame_ProcessItemsBeforeLoad(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD);
}
}
}
void Savegame_SetDefaultStats(
const GF_LEVEL *const level, const STATS_COMMON stats)
{
if (m_DefaultStats == nullptr) {
m_DefaultStats = Memory_Alloc(
sizeof(STATS_COMMON) * GF_GetLevelTable(GFLT_MAIN)->count);
}
m_DefaultStats[level->num] = stats;
}
STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *const level)
{
if (m_DefaultStats == nullptr
|| (level->type != GFL_NORMAL && level->type != GFL_BONUS)) {
return (STATS_COMMON) {};
}
return m_DefaultStats[level->num];
}
void Savegame_ScanSavedGames(void)
{
BENCHMARK benchmark = Benchmark_Start();
M_ClearSlots();
m_SaveCounter = 0;
m_SavedGames = 0;
m_NewestSlot = -1;
M_ScanSavedGamesDir(SAVES_DIR);
M_ScanSavedGamesDir(".");
for (int32_t i = 0; i < m_SaveSlots; i++) {
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i];
if (savegame_info->level_title != nullptr) {
if (savegame_info->counter > m_SaveCounter) {
m_SaveCounter = savegame_info->counter;
m_NewestSlot = i;
}
m_SavedGames++;
}
}
Benchmark_End(&benchmark, nullptr);
}
bool Savegame_Save(const int32_t slot_idx)
{
bool result = false;
Savegame_BindSlot(slot_idx);
File_CreateDirectory(SAVES_DIR);
const GF_LEVEL *const current_level = Game_GetCurrentLevel();
const char *const level_title = current_level->title;
Savegame_PersistGameToCurrentInfo(current_level);
#if TR_VERSION == 1
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
if (level->type == GFL_CURRENT) {
Savegame_SetCurrentInfo(i, current_level->num);
}
}
#endif
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx];
const bool was_slot_empty = savegame_info->full_path == nullptr;
m_SaveCounter++;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (!strategy.allow_save || strategy.save_to_file_func == nullptr) {
continue;
}
char *file_name =
String_Format(strategy.get_save_file_pattern_func(), slot_idx);
char *full_path = String_Format("%s/%s", SAVES_DIR, file_name);
MYFILE *const fp = File_Open(full_path, FILE_OPEN_WRITE);
if (fp != nullptr) {
strategy.save_to_file_func(fp, savegame_info);
savegame_info->format = strategy.format;
Memory_FreePointer(&savegame_info->full_path);
savegame_info->full_path = Memory_DupStr(File_GetPath(fp));
savegame_info->counter = m_SaveCounter;
savegame_info->level_num = current_level->num;
savegame_info->level_title =
level_title != nullptr ? Memory_DupStr(level_title) : nullptr;
File_Close(fp);
result = true;
}
Memory_FreePointer(&file_name);
Memory_FreePointer(&full_path);
}
if (result) {
m_NewestSlot = slot_idx;
if (was_slot_empty) {
m_SavedGames++;
}
Savegame_HighlightNewestSlot();
} else {
m_SaveCounter--;
}
return result;
}
bool Savegame_Load(const int32_t slot_idx)
{
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx];
ASSERT(savegame_info->format != 0);
M_LoadPreprocess();
bool result = false;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (strategy.format != savegame_info->format) {
continue;
}
MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ);
if (fp != nullptr) {
result = strategy.load_from_file_func(fp);
File_Close(fp);
}
break;
}
M_LoadPostprocess();
m_InitialVersion = m_SavegameInfo[slot_idx].initial_version;
return result;
}
bool Savegame_UpdateDeathCounters(
const int32_t slot_num, const int32_t death_count)
{
ASSERT(slot_num >= 0);
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
ASSERT(savegame_info->format != SAVEGAME_FORMAT_INVALID);
bool ret = false;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (savegame_info->format == strategy.format
&& strategy.update_death_counters_func != nullptr) {
MYFILE *const fp =
File_Open(savegame_info->full_path, FILE_OPEN_READ_WRITE);
if (fp != nullptr) {
ret = strategy.update_death_counters_func(fp, death_count);
File_Close(fp);
}
break;
}
}
return ret;
}
bool Savegame_LoadOnlyResumeInfo(int32_t slot_num)
{
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
ASSERT(savegame_info->format != SAVEGAME_FORMAT_INVALID);
bool ret = false;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (savegame_info->format == strategy.format
&& strategy.load_only_resume_info_func != nullptr) {
MYFILE *const fp =
File_Open(savegame_info->full_path, FILE_OPEN_READ);
if (fp != nullptr) {
ret = strategy.load_only_resume_info_func(fp);
File_Close(fp);
}
break;
}
}
Savegame_SetInitialVersion(m_SavegameInfo[slot_num].initial_version);
return ret;
}

View file

@ -7,3 +7,4 @@ void Lara_Extinguish(void);
int16_t Lara_FloorFront(const ITEM *item, int16_t ang, int32_t dist);
void Lara_GetCollisionInfo(const ITEM *item, COLL_INFO *coll);
extern void Lara_CatchFire(void);

View file

@ -4,7 +4,7 @@
#include "../collision.h"
#include "../items/types.h"
#include "../rooms/enum.h"
#include "../savegame.h"
#include "../savegame/enum.h"
#include "../types.h"
#include <stdint.h>

View file

@ -5,3 +5,4 @@
extern bool LOT_EnableBaddieAI(int16_t item_num, bool always);
extern void LOT_DisableBaddieAI(int16_t item_num);
extern CREATURE *LOT_GetBaddieSlot(int32_t i);
extern void LOT_ClearLOT(LOT_INFO *LOT);

View file

@ -1,12 +1,30 @@
#pragma once
#include "./enum.h"
#include "../game_flow/types.h"
#include "./types.h"
extern void Savegame_RegisterStrategy(SAVEGAME_STRATEGY strategy);
// Loading a saved game is divided into two phases. First, the game reads the
// savegame file contents to look for the level number. The rest of the save
// data is stored in a special buffer in the g_GameInfo. Then the engine
// continues to execute the normal game flow and loads the specified level.
// Second phase occurs after everything finishes loading, e.g. items,
// creatures, triggers etc., and is what actually sets Lara's health, creatures
// status, triggers, inventory etc.
void Savegame_RegisterStrategy(SAVEGAME_STRATEGY strategy);
void Savegame_Init(void);
void Savegame_Shutdown(void);
bool Savegame_IsInitialised(void);
void Savegame_ScanSavedGames(void);
SAVEGAME_VERSION Savegame_GetInitialVersion(void);
void Savegame_SetInitialVersion(SAVEGAME_VERSION version);
int32_t Savegame_GetHighestSlot(void);
int32_t Savegame_GetCounter(void);
int32_t Savegame_GetTotalCount(void);
int32_t Savegame_GetLevelNumber(int32_t slot_num);
bool Savegame_IsSlotFree(int32_t slot_num);
bool Savegame_RestartAvailable(int32_t slot_num);
// Remembers the slot used when the player starts a loaded game.
// Persists across level reloads.
@ -19,10 +37,28 @@ void Savegame_UnbindSlot(void);
// Returns the currently bound slot number. If there is none, returns -1.
int32_t Savegame_GetBoundSlot(void);
void Savegame_ProcessItemsBeforeLoad(void);
void Savegame_ProcessItemsBeforeSave(void);
bool Savegame_Load(int32_t slot_num);
bool Savegame_Save(int32_t slot_num);
bool Savegame_UpdateDeathCounters(int32_t slot_num, int32_t death_count);
bool Savegame_LoadOnlyResumeInfo(int32_t slot_num);
void Savegame_InitCurrentInfo(void);
void Savegame_SetCurrentInfo(int32_t current_slot, int32_t src_slot);
RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level);
const SAVEGAME_INFO *Savegame_GetSavegameInfo(int32_t slot_num);
void Savegame_ResetCurrentInfo(const GF_LEVEL *level);
void Savegame_CarryCurrentInfoToNextLevel(
const GF_LEVEL *src_level, const GF_LEVEL *dst_level);
void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level);
void Savegame_SetDefaultStats(const GF_LEVEL *level, STATS_COMMON stats);
STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *level);
extern int32_t Savegame_GetSlotCount(void);
extern bool Savegame_IsSlotFree(int32_t slot_num);
extern bool Savegame_Load(int32_t slot_num);
extern bool Savegame_Save(int32_t slot_num);
extern void Savegame_HighlightNewestSlot(void);
extern void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level);
#define REGISTER_SAVEGAME_STRATEGY(strategy_) \
__attribute__((__constructor__)) static void M_Register(void) \

View file

@ -1,4 +1,3 @@
#pragma once
#define SAVEGAME_CURRENT_VERSION 8
#define MAX_STRATEGIES 2

View file

@ -23,4 +23,3 @@ int32_t Lara_GetWaterDepth(int32_t x, int32_t y, int32_t z, int16_t room_num);
void Lara_TestWaterDepth(ITEM *item, const COLL_INFO *coll);
bool Lara_TestWaterStepOut(ITEM *item, const COLL_INFO *coll);
bool Lara_TestWaterClimbOut(ITEM *item, COLL_INFO *coll);
void Lara_CatchFire(void);

View file

@ -10,4 +10,3 @@ void LOT_InitialiseArray(void);
void LOT_InitialiseSlot(int16_t item_num, int32_t slot);
void LOT_CreateZone(ITEM *item);
void LOT_InitialiseLOT(LOT_INFO *LOT);
void LOT_ClearLOT(LOT_INFO *LOT);

View file

@ -4,47 +4,5 @@
#include <libtrx/game/savegame.h>
#include <stdint.h>
// Loading a saved game is divided into two phases. First, the game reads the
// savegame file contents to look for the level number. The rest of the save
// data is stored in a special buffer in the g_GameInfo. Then the engine
// continues to execute the normal game flow and loads the specified level.
// Second phase occurs after everything finishes loading, e.g. items,
// creatures, triggers etc., and is what actually sets Lara's health, creatures
// status, triggers, inventory etc.
void Savegame_Init(void);
void Savegame_Shutdown(void);
bool Savegame_IsInitialised(void);
void Savegame_InitCurrentInfo(void);
void Savegame_SetCurrentInfo(int32_t current_slot, int32_t src_slot);
int32_t Savegame_GetLevelNumber(int32_t slot_num);
bool Savegame_UpdateDeathCounters(int32_t slot_num, int32_t death_count);
bool Savegame_LoadOnlyResumeInfo(int32_t slot_num);
void Savegame_ScanSavedGames(void);
void Savegame_FillAvailableSaves(REQUEST_INFO *req);
void Savegame_FillAvailableLevels(REQUEST_INFO *req);
void Savegame_HighlightNewestSlot(void);
bool Savegame_RestartAvailable(int32_t slot_num);
RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level);
void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level);
void Savegame_ResetCurrentInfo(const GF_LEVEL *level);
void Savegame_CarryCurrentInfoToNextLevel(
const GF_LEVEL *src_level, const GF_LEVEL *dst_level);
// Persist Lara's inventory to the current info.
// Used to carry over Lara's inventory between levels.
void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level);
void Savegame_ProcessItemsBeforeLoad(void);
void Savegame_ProcessItemsBeforeSave(void);
int32_t Savegame_GetCounter(void);
int32_t Savegame_GetTotalCount(void);

View file

@ -3,272 +3,21 @@
#include "game/game.h"
#include "game/game_flow.h"
#include "game/game_string.h"
#include "game/inventory.h"
#include "game/items.h"
#include "game/lot.h"
#include "game/objects/vars.h"
#include "game/requester.h"
#include "game/room.h"
#include "global/const.h"
#include "global/types.h"
#include "global/vars.h"
#include <libtrx/benchmark.h>
#include <libtrx/config.h>
#include <libtrx/debug.h>
#include <libtrx/enum_map.h>
#include <libtrx/filesystem.h>
#include <libtrx/game/gun/const.h>
#include <libtrx/game/objects/traps/movable_block.h>
#include <libtrx/memory.h>
#include <libtrx/strings.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define SAVES_DIR "saves"
static int32_t m_SaveSlots = 0;
static int16_t m_NewestSlot = -1;
static int32_t m_SaveCounter = 0;
static int32_t m_SavedGames = 0;
static SAVEGAME_INFO *m_SavegameInfo = nullptr;
static RESUME_INFO *m_ResumeInfo = nullptr;
static int32_t m_StrategyCount = 0;
static SAVEGAME_STRATEGY m_Strategies[MAX_STRATEGIES];
static void M_ClearSlots(void);
static bool M_FillSlot(
const SAVEGAME_STRATEGY strategy, int32_t slot_num, const char *path);
static void M_ScanSavedGamesDir(const char *dir_path);
static void M_LoadPreprocess(void);
static void M_LoadPostprocess(void);
static void M_ClearSlots(void)
int32_t Savegame_GetSlotCount(void)
{
if (m_SavegameInfo == nullptr) {
return;
}
for (int32_t i = 0; i < m_SaveSlots; i++) {
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i];
savegame_info->format = 0;
savegame_info->counter = -1;
savegame_info->level_num = -1;
Memory_FreePointer(&savegame_info->full_path);
Memory_FreePointer(&savegame_info->level_title);
}
return g_Config.gameplay.maximum_save_slots;
}
static bool M_FillSlot(
const SAVEGAME_STRATEGY strategy, const int32_t slot_num,
const char *const path)
void Savegame_HighlightNewestSlot(void)
{
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
if (strategy.format <= savegame_info->format) {
// do not override already filled slots or for less preferred strategies
return true;
}
bool result = false;
MYFILE *const fp = File_Open(path, FILE_OPEN_READ);
if (fp != nullptr) {
if (strategy.fill_info_func(fp, savegame_info)) {
savegame_info->format = strategy.format;
Memory_FreePointer(&savegame_info->full_path);
savegame_info->full_path = Memory_DupStr(path);
result = true;
}
File_Close(fp);
}
return result;
}
static void M_ScanSavedGamesDir(const char *const dir_path)
{
void *dir_handle = File_OpenDirectory(dir_path);
if (dir_handle == nullptr) {
return;
}
while (true) {
const char *file_name = File_ReadDirectory(dir_handle);
if (file_name == nullptr) {
break;
}
if (strcmp(file_name, ".") == 0 || strcmp(file_name, "..") == 0) {
continue;
}
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (!strategy.allow_load) {
continue;
}
int32_t slot = -1;
const int32_t parsed =
sscanf(file_name, strategy.get_save_file_pattern_func(), &slot);
if (parsed == 1 && slot >= 0 && slot < m_SaveSlots) {
char *file_path = String_Format("%s/%s", dir_path, file_name);
M_FillSlot(strategy, slot, file_path);
Memory_FreePointer(&file_path);
}
}
}
File_CloseDirectory(dir_handle);
}
static void M_LoadPreprocess(void)
{
Savegame_InitCurrentInfo();
}
static void M_LoadPostprocess(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->save_position && obj->shadow_size) {
int16_t room_num = item->room_num;
const SECTOR *const sector = Room_GetSector(
item->pos.x, item->pos.y, item->pos.z, &room_num);
item->floor =
Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z);
}
if (obj->save_flags != 0) {
item->flags &= 0xFF00;
}
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_AFTER_LOAD);
}
}
MovableBlock_SetupFloor();
if (Game_GetBonusFlag() != GBF_NONE) {
g_Config.profile.new_game_plus_unlock = true;
}
LOT_ClearLOT(&g_Lara.lot);
}
void Savegame_RegisterStrategy(const SAVEGAME_STRATEGY strategy)
{
ASSERT(m_StrategyCount < MAX_STRATEGIES);
m_Strategies[m_StrategyCount] = strategy;
m_StrategyCount++;
}
void Savegame_Init(void)
{
m_ResumeInfo = Memory_Alloc(
sizeof(RESUME_INFO)
* (GF_GetLevelTable(GFLT_MAIN)->count
+ (GF_GetLevelTable(GFLT_DEMOS)->count >= 0 ? 1 : 0)));
m_SaveSlots = g_Config.gameplay.maximum_save_slots;
m_SavegameInfo = Memory_Alloc(sizeof(SAVEGAME_INFO) * m_SaveSlots);
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS);
for (int32_t i = 0; i < level_table->count; i++) {
RESUME_INFO *const resume_info =
Savegame_GetCurrentInfo(&level_table->levels[i]);
resume_info->flags.available = 1;
resume_info->flags.costume = 0;
resume_info->small_medipacks = 0;
resume_info->large_medipacks = 0;
resume_info->num_scions = 0;
resume_info->flags.has_pistols = 1;
resume_info->flags.has_shotgun = 0;
resume_info->flags.has_magnums = 0;
resume_info->flags.has_uzis = 0;
resume_info->pistol_ammo = 1000;
resume_info->shotgun_ammo = 0;
resume_info->magnum_ammo = 0;
resume_info->uzi_ammo = 0;
resume_info->gun_status = LGS_ARMLESS;
resume_info->equipped_gun_type = LGT_PISTOLS;
resume_info->holsters_gun_type = LGT_PISTOLS;
resume_info->back_gun_type = LGT_UNARMED;
resume_info->lara_hitpoints = LARA_MAX_HITPOINTS;
}
}
RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level)
{
ASSERT(m_ResumeInfo != nullptr);
ASSERT(level != nullptr);
if (GF_GetLevelTableType(level->type) == GFLT_MAIN) {
return &m_ResumeInfo[level->num];
} else if (level->type == GFL_DEMO) {
return &m_ResumeInfo[GF_GetLevelTable(GFLT_MAIN)->count];
}
LOG_WARNING(
"Warning: unable to get resume info for level %d (type=%s)", level->num,
ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type));
return nullptr;
}
void Savegame_SetCurrentInfo(const int32_t current_slot, const int32_t src_slot)
{
m_ResumeInfo[current_slot] = m_ResumeInfo[src_slot];
}
void Savegame_Shutdown(void)
{
M_ClearSlots();
Memory_FreePointer(&m_SavegameInfo);
Memory_FreePointer(&m_ResumeInfo);
}
bool Savegame_IsInitialised(void)
{
return m_SavegameInfo != nullptr;
}
void Savegame_ProcessItemsBeforeLoad(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD);
}
}
}
void Savegame_ProcessItemsBeforeSave(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_SAVE);
}
}
}
void Savegame_InitCurrentInfo(void)
{
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
Savegame_ResetCurrentInfo(level);
Savegame_ApplyLogicToCurrentInfo(level);
Savegame_GetCurrentInfo(level)->flags.available = 0;
}
if (GF_GetGymLevel() != nullptr) {
Savegame_GetCurrentInfo(GF_GetGymLevel())->flags.available = 1;
}
if (GF_GetFirstLevel() != nullptr) {
Savegame_GetCurrentInfo(GF_GetFirstLevel())->flags.available = 1;
}
g_SavegameRequester.requested = MAX(0, Savegame_GetHighestSlot());
}
void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level)
@ -368,266 +117,13 @@ void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level)
}
}
void Savegame_ResetCurrentInfo(const GF_LEVEL *const level)
{
LOG_INFO("Resetting resume info for level #%d", level->num);
RESUME_INFO *const current = Savegame_GetCurrentInfo(level);
memset(current, 0, sizeof(RESUME_INFO));
}
void Savegame_CarryCurrentInfoToNextLevel(
const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level)
{
LOG_INFO(
"Copying resume info from level #%d to level #%d", src_level->num,
dst_level->num);
RESUME_INFO *const src_resume = Savegame_GetCurrentInfo(src_level);
RESUME_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level);
memcpy(dst_resume, src_resume, sizeof(RESUME_INFO));
}
void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level)
{
ASSERT(level != nullptr);
RESUME_INFO *current = Savegame_GetCurrentInfo(level);
current->lara_hitpoints = g_LaraItem->hit_points;
current->flags.available = 1;
current->pistol_ammo = 1000;
if (Inv_RequestItem(O_PISTOL_ITEM)) {
current->flags.has_pistols = 1;
} else {
current->flags.has_pistols = 0;
}
if (Inv_RequestItem(O_MAGNUM_ITEM)) {
current->magnum_ammo = g_Lara.magnum_ammo.ammo;
current->flags.has_magnums = 1;
} else {
current->magnum_ammo =
Inv_RequestItem(O_MAGNUM_AMMO_ITEM) * MAGNUM_AMMO_QTY;
current->flags.has_magnums = 0;
}
if (Inv_RequestItem(O_UZI_ITEM)) {
current->uzi_ammo = g_Lara.uzi_ammo.ammo;
current->flags.has_uzis = 1;
} else {
current->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM) * UZI_AMMO_QTY;
current->flags.has_uzis = 0;
}
if (Inv_RequestItem(O_SHOTGUN_ITEM)) {
current->shotgun_ammo = g_Lara.shotgun_ammo.ammo;
current->flags.has_shotgun = 1;
} else {
current->shotgun_ammo =
Inv_RequestItem(O_SHOTGUN_AMMO_ITEM) * SHOTGUN_AMMO_QTY;
current->flags.has_shotgun = 0;
}
current->small_medipacks = Inv_RequestItem(O_SMALL_MEDIPACK_ITEM);
current->large_medipacks = Inv_RequestItem(O_LARGE_MEDIPACK_ITEM);
current->num_scions = Inv_RequestItem(O_SCION_ITEM_1);
current->equipped_gun_type = g_Lara.gun_type;
current->holsters_gun_type = g_Lara.holsters_gun_type;
current->back_gun_type = g_Lara.back_gun_type;
if (g_Lara.gun_status == LGS_READY) {
current->gun_status = LGS_READY;
} else {
current->gun_status = LGS_ARMLESS;
}
}
int32_t Savegame_GetLevelNumber(const int32_t slot_num)
{
return m_SavegameInfo[slot_num].level_num;
}
int32_t Savegame_GetSlotCount(void)
{
return m_SaveSlots;
}
bool Savegame_IsSlotFree(const int32_t slot_num)
{
return m_SavegameInfo[slot_num].level_num == -1;
}
bool Savegame_Load(const int32_t slot_num)
{
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
ASSERT(savegame_info->format != SAVEGAME_FORMAT_INVALID);
M_LoadPreprocess();
bool ret = false;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (savegame_info->format == strategy.format) {
MYFILE *const fp =
File_Open(savegame_info->full_path, FILE_OPEN_READ);
if (fp != nullptr) {
ret = strategy.load_from_file_func(fp);
File_Close(fp);
}
break;
}
}
if (ret) {
M_LoadPostprocess();
}
Savegame_SetInitialVersion(m_SavegameInfo[slot_num].initial_version);
return ret;
}
bool Savegame_Save(const int32_t slot_num)
{
bool ret = false;
Savegame_BindSlot(slot_num);
File_CreateDirectory(SAVES_DIR);
const GF_LEVEL *const current_level = Game_GetCurrentLevel();
Savegame_PersistGameToCurrentInfo(current_level);
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
if (level->type == GFL_CURRENT) {
Savegame_SetCurrentInfo(i, current_level->num);
}
}
const char *const level_title = Game_GetCurrentLevel()->title;
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
const bool was_slot_empty = savegame_info->full_path == nullptr;
m_SaveCounter++;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (!strategy.allow_save || strategy.save_to_file_func == nullptr) {
continue;
}
char *filename =
String_Format(strategy.get_save_file_pattern_func(), slot_num);
char *full_path = String_Format("%s/%s", SAVES_DIR, filename);
MYFILE *const fp = File_Open(full_path, FILE_OPEN_WRITE);
if (fp != nullptr) {
strategy.save_to_file_func(fp, savegame_info);
savegame_info->format = strategy.format;
Memory_FreePointer(&savegame_info->full_path);
savegame_info->full_path = Memory_DupStr(File_GetPath(fp));
savegame_info->counter = m_SaveCounter;
savegame_info->level_num = current_level->num;
savegame_info->level_title =
level_title != nullptr ? Memory_DupStr(level_title) : nullptr;
File_Close(fp);
ret = true;
}
Memory_FreePointer(&filename);
Memory_FreePointer(&full_path);
}
if (ret) {
m_NewestSlot = slot_num;
if (was_slot_empty) {
m_SavedGames++;
}
Savegame_HighlightNewestSlot();
} else {
m_SaveCounter--;
}
return ret;
}
bool Savegame_UpdateDeathCounters(
const int32_t slot_num, const int32_t death_count)
{
ASSERT(slot_num >= 0);
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
ASSERT(savegame_info->format != SAVEGAME_FORMAT_INVALID);
bool ret = false;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (savegame_info->format == strategy.format
&& strategy.update_death_counters_func != nullptr) {
MYFILE *const fp =
File_Open(savegame_info->full_path, FILE_OPEN_READ_WRITE);
if (fp != nullptr) {
ret = strategy.update_death_counters_func(fp, death_count);
File_Close(fp);
}
break;
}
}
return ret;
}
bool Savegame_LoadOnlyResumeInfo(int32_t slot_num)
{
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
ASSERT(savegame_info->format != SAVEGAME_FORMAT_INVALID);
bool ret = false;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (savegame_info->format == strategy.format) {
MYFILE *const fp =
File_Open(savegame_info->full_path, FILE_OPEN_READ);
if (fp != nullptr) {
ret = strategy.load_only_resume_info_func(fp);
File_Close(fp);
}
break;
}
}
Savegame_SetInitialVersion(m_SavegameInfo[slot_num].initial_version);
return ret;
}
void Savegame_ScanSavedGames(void)
{
BENCHMARK benchmark = Benchmark_Start();
M_ClearSlots();
m_SaveCounter = 0;
m_SavedGames = 0;
m_NewestSlot = -1;
M_ScanSavedGamesDir(SAVES_DIR);
M_ScanSavedGamesDir(".");
for (int32_t i = 0; i < m_SaveSlots; i++) {
SAVEGAME_INFO *savegame_info = &m_SavegameInfo[i];
if (savegame_info->level_title != nullptr) {
if (savegame_info->counter > m_SaveCounter) {
m_SaveCounter = savegame_info->counter;
m_NewestSlot = i;
}
m_SavedGames++;
}
}
Benchmark_End(&benchmark, nullptr);
}
void Savegame_FillAvailableSaves(REQUEST_INFO *req)
void Savegame_FillAvailableSaves(REQUEST_INFO *const req)
{
Requester_ClearTextstrings(req);
Requester_Init(req, Savegame_GetSlotCount());
for (int i = 0; i < req->max_items; i++) {
SAVEGAME_INFO *savegame_info = &m_SavegameInfo[i];
for (int32_t i = 0; i < req->max_items; i++) {
const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(i);
if (savegame_info->level_title != nullptr) {
Requester_AddItem(
@ -645,7 +141,7 @@ void Savegame_FillAvailableSaves(REQUEST_INFO *req)
}
}
void Savegame_FillAvailableLevels(REQUEST_INFO *req)
void Savegame_FillAvailableLevels(REQUEST_INFO *const req)
{
ASSERT(req != nullptr);
const int32_t slot_num = g_GameInfo.select_save_slot;
@ -653,7 +149,8 @@ void Savegame_FillAvailableLevels(REQUEST_INFO *req)
return;
}
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
const SAVEGAME_INFO *const savegame_info =
Savegame_GetSavegameInfo(slot_num);
if (!savegame_info->features.select_level) {
Requester_AddItem(req, true, "%s", GS(PASSPORT_LEGACY_SELECT_LEVEL_1));
Requester_AddItem(req, true, "%s", GS(PASSPORT_LEGACY_SELECT_LEVEL_2));
@ -678,28 +175,3 @@ void Savegame_FillAvailableLevels(REQUEST_INFO *req)
req->requested = 0;
req->line_offset = 0;
}
void Savegame_HighlightNewestSlot(void)
{
g_SavegameRequester.requested = MAX(0, m_NewestSlot);
}
bool Savegame_RestartAvailable(int32_t slot_num)
{
if (slot_num == -1) {
return true;
}
SAVEGAME_INFO *savegame_info = &m_SavegameInfo[slot_num];
return savegame_info->features.restart;
}
int32_t Savegame_GetCounter(void)
{
return m_SaveCounter;
}
int32_t Savegame_GetTotalCount(void)
{
return m_SavedGames;
}

View file

@ -72,8 +72,6 @@ void Lara_SwimCollision(ITEM *item, COLL_INFO *coll);
void Lara_WaterCurrent(COLL_INFO *coll);
void Lara_CatchFire(void);
void Lara_TouchLava(ITEM *item);
// Returns true if Lara has the M16 equipped and is in either anim state: 0

View file

@ -7,4 +7,3 @@
void LOT_InitialiseArray(void);
void LOT_InitialiseSlot(int16_t item_num, int32_t slot);
void LOT_CreateZone(ITEM *item);
void LOT_ClearLOT(LOT_INFO *LOT);

View file

@ -1,32 +1,8 @@
#pragma once
#include "game/game_flow/types.h"
#include "global/types.h"
#include <libtrx/filesystem.h>
#include <libtrx/game/savegame.h>
void Savegame_Init(void);
void Savegame_Shutdown(void);
void Savegame_InitCurrentInfo(void);
void Savegame_ResetCurrentInfo(const GF_LEVEL *level);
RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *level);
void Savegame_CarryCurrentInfoToNextLevel(
const GF_LEVEL *src_level, const GF_LEVEL *dst_level);
void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *level);
void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *level);
void Savegame_ScanSavedGames(void);
void Savegame_FillAvailableSaves(REQUEST_INFO *req);
void Savegame_FillAvailableLevels(REQUEST_INFO *req);
void Savegame_HighlightNewestSlot(void);
int32_t Savegame_GetLevelNumber(int32_t slot_idx);
int32_t Savegame_GetCounter(void);
int32_t Savegame_GetTotalCount(void);
void Savegame_ProcessItemsBeforeSave(void);
void Savegame_ProcessItemsBeforeLoad(void);
void Savegame_SetDefaultStats(const GF_LEVEL *level, STATS_COMMON stats);
STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *level);

View file

@ -1,337 +1,31 @@
#include "game/game.h"
#include "game/game_flow.h"
#include "game/game_string.h"
#include "game/inventory.h"
#include "game/lara/misc.h"
#include "game/requester.h"
#include "game/savegame.h"
#include "global/types_decomp.h"
#include "global/vars.h"
#include <libtrx/benchmark.h>
#include <libtrx/debug.h>
#include <libtrx/enum_map.h>
#include <libtrx/filesystem.h>
#include <libtrx/game/gun/const.h>
#include <libtrx/game/lara.h>
#include <libtrx/game/objects/traps/movable_block.h>
#include <libtrx/game/savegame.h>
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <libtrx/strings.h>
#include <libtrx/utils.h>
#define SAVES_DIR "saves"
static STATS_COMMON *m_DefaultStats = nullptr;
static RESUME_INFO *m_ResumeInfos = nullptr;
static int32_t m_SaveSlots = 0;
static int32_t m_NewestSlot = -1;
static int32_t m_SaveCounter = 0;
static int32_t m_SavedGames = 0;
static SAVEGAME_INFO *m_SavegameInfo = nullptr;
// TODO: make configurable
#define MAX_SAVE_SLOTS MAX_REQUESTER_ITEMS
static uint32_t m_ReqFlags1[MAX_REQUESTER_ITEMS] = {};
static uint32_t m_ReqFlags2[MAX_REQUESTER_ITEMS] = {};
static int32_t m_StrategyCount = 0;
static SAVEGAME_STRATEGY m_Strategies[MAX_STRATEGIES];
static void M_ClearSlots(void);
static bool M_FillSlot(
SAVEGAME_STRATEGY strategy, int32_t slot_num, const char *path);
static void M_ScanSavedGamesDir(const char *dir_path);
static void M_ClearSlots(void)
{
if (m_SavegameInfo == nullptr) {
return;
}
for (int32_t i = 0; i < m_SaveSlots; i++) {
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i];
savegame_info->format = SAVEGAME_FORMAT_INVALID;
savegame_info->counter = -1;
savegame_info->level_num = -1;
Memory_FreePointer(&savegame_info->full_path);
Memory_FreePointer(&savegame_info->level_title);
}
}
static bool M_FillSlot(
const SAVEGAME_STRATEGY strategy, const int32_t slot_num,
const char *const path)
{
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
if (strategy.format <= savegame_info->format) {
return true;
}
bool result = false;
MYFILE *const fp = File_Open(path, FILE_OPEN_READ);
if (fp != nullptr) {
if (strategy.fill_info_func(fp, savegame_info)) {
savegame_info->format = strategy.format;
Memory_FreePointer(&savegame_info->full_path);
savegame_info->full_path = Memory_DupStr(path);
result = true;
}
File_Close(fp);
}
return result;
}
static void M_ScanSavedGamesDir(const char *const dir_path)
{
void *dir_handle = File_OpenDirectory(dir_path);
if (dir_handle == nullptr) {
return;
}
while (true) {
const char *file_name = File_ReadDirectory(dir_handle);
if (file_name == nullptr) {
break;
}
if (strcmp(file_name, ".") == 0 || strcmp(file_name, "..") == 0) {
continue;
}
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (!strategy.allow_load) {
continue;
}
int32_t slot = -1;
const int32_t parsed =
sscanf(file_name, strategy.get_save_file_pattern_func(), &slot);
if (parsed == 1 && slot >= 0 && slot < m_SaveSlots) {
char *file_path = String_Format("%s/%s", dir_path, file_name);
M_FillSlot(strategy, slot, file_path);
Memory_FreePointer(&file_path);
}
}
}
File_CloseDirectory(dir_handle);
}
static void M_LoadPreprocess(void)
{
Savegame_InitCurrentInfo();
}
static void M_LoadPostprocess(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->save_position && obj->shadow_size != 0) {
int16_t room_num = item->room_num;
const SECTOR *const sector = Room_GetSector(
item->pos.x, item->pos.y, item->pos.z, &room_num);
item->floor =
Room_GetHeight(sector, item->pos.x, item->pos.y, item->pos.z);
}
if (obj->save_flags != 0) {
item->flags &= 0xFF00;
}
}
MovableBlock_SetupFloor();
LARA_INFO *const lara = Lara_GetLaraInfo();
if (lara->burn) {
lara->burn = 0;
Lara_CatchFire();
}
}
void Savegame_RegisterStrategy(const SAVEGAME_STRATEGY strategy)
{
ASSERT(m_StrategyCount < MAX_STRATEGIES);
m_Strategies[m_StrategyCount] = strategy;
m_StrategyCount++;
}
void Savegame_Init(void)
{
m_ResumeInfos = Memory_Alloc(
sizeof(RESUME_INFO)
* (GF_GetLevelTable(GFLT_MAIN)->count
+ GF_GetLevelTable(GFLT_DEMOS)->count));
m_SaveSlots = MAX_SAVE_SLOTS; // TODO: make configurable
m_SavegameInfo = Memory_Alloc(sizeof(SAVEGAME_INFO) * m_SaveSlots);
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_DEMOS);
for (int32_t i = 0; i < level_table->count; i++) {
RESUME_INFO *const resume_info =
Savegame_GetCurrentInfo(&level_table->levels[i]);
resume_info->flags.available = 1;
resume_info->flags.has_pistols = 1;
resume_info->pistol_ammo = 1000;
resume_info->gun_status = LGS_ARMLESS;
resume_info->equipped_gun_type = LGT_PISTOLS;
}
}
void Savegame_Shutdown(void)
{
M_ClearSlots();
Memory_FreePointer(&m_ResumeInfos);
Memory_FreePointer(&m_SavegameInfo);
Memory_FreePointer(&m_DefaultStats);
}
int32_t Savegame_GetSlotCount(void)
{
return MAX_SAVE_SLOTS;
}
bool Savegame_IsSlotFree(const int32_t slot_idx)
{
return m_SavegameInfo[slot_idx].level_num == -1;
}
int32_t Savegame_GetLevelNumber(const int32_t slot_idx)
{
return m_SavegameInfo[slot_idx].level_num;
}
void Savegame_ScanSavedGames(void)
{
BENCHMARK benchmark = Benchmark_Start();
M_ClearSlots();
m_SaveCounter = 0;
m_SavedGames = 0;
m_NewestSlot = -1;
M_ScanSavedGamesDir(SAVES_DIR);
M_ScanSavedGamesDir(".");
for (int32_t i = 0; i < m_SaveSlots; i++) {
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i];
if (savegame_info->level_title != nullptr) {
if (savegame_info->counter > m_SaveCounter) {
m_SaveCounter = savegame_info->counter;
m_NewestSlot = i;
}
m_SavedGames++;
}
}
Benchmark_End(&benchmark, nullptr);
}
void Savegame_FillAvailableSaves(REQUEST_INFO *const req)
{
Requester_Init(req);
for (int32_t i = 0; i < MAX_SAVE_SLOTS; i++) {
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i];
if (savegame_info->level_title != nullptr) {
char save_num_text[16];
sprintf(save_num_text, "%d", savegame_info->counter);
Requester_AddItem(
req, savegame_info->level_title, REQ_ALIGN_LEFT, save_num_text,
REQ_ALIGN_RIGHT);
} else {
Requester_AddItem(req, GS(MISC_EMPTY_SLOT), 0, 0, 0);
}
}
Requester_SetSize(req, 10, -32);
if (req->selected >= req->visible_count) {
req->line_offset = req->selected - req->visible_count + 1;
} else if (req->selected < req->line_offset) {
req->line_offset = req->selected;
}
memcpy(m_ReqFlags1, g_RequesterFlags1, sizeof(m_ReqFlags1));
memcpy(m_ReqFlags2, g_RequesterFlags2, sizeof(m_ReqFlags2));
}
void Savegame_FillAvailableLevels(REQUEST_INFO *const req)
{
ASSERT(req != nullptr);
Requester_Init(req);
Requester_SetSize(req, 10, -32);
Requester_SetHeading(req, GS(PASSPORT_SELECT_LEVEL), 0, nullptr, 0);
req->ready = true;
req->selected = 0;
Requester_RemoveAllItems(req);
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
if (level->type != GFL_GYM) {
Requester_AddItem(req, level->title, 0, nullptr, 0);
}
}
}
void Savegame_HighlightNewestSlot(void)
{
g_SaveGameRequester.selected = MAX(0, m_NewestSlot);
g_LoadGameRequester.selected = MAX(0, m_NewestSlot);
}
int32_t Savegame_GetCounter(void)
{
return m_SaveCounter;
}
int32_t Savegame_GetTotalCount(void)
{
return m_SavedGames;
}
void Savegame_ProcessItemsBeforeSave(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_SAVE);
}
}
}
void Savegame_ProcessItemsBeforeLoad(void)
{
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
ITEM *const item = Item_Get(i);
const OBJECT *const obj = Object_Get(item->object_id);
if (obj->handle_save_func != nullptr) {
obj->handle_save_func(item, SAVEGAME_STAGE_BEFORE_LOAD);
}
}
}
void Savegame_InitCurrentInfo(void)
{
if (Game_IsBonusFlagSet(GBF_NGPLUS)) {
return;
}
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
Savegame_ResetCurrentInfo(level);
Savegame_ApplyLogicToCurrentInfo(level);
Savegame_GetCurrentInfo(level)->flags.available = 0;
}
if (GF_GetGymLevel() != nullptr) {
Savegame_GetCurrentInfo(GF_GetGymLevel())->flags.available = 1;
}
if (GF_GetFirstLevel() != nullptr) {
Savegame_GetCurrentInfo(GF_GetFirstLevel())->flags.available = 1;
}
Game_SetBonusFlag(GBF_NONE);
const int32_t slot = Savegame_GetHighestSlot();
g_SaveGameRequester.selected = MAX(0, slot);
g_LoadGameRequester.selected = MAX(0, slot);
}
void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level)
@ -445,214 +139,48 @@ void Savegame_ApplyLogicToCurrentInfo(const GF_LEVEL *const level)
resume->stats.max_secret_count = default_stats.max_secret_count;
}
void Savegame_ResetCurrentInfo(const GF_LEVEL *const level)
void Savegame_FillAvailableSaves(REQUEST_INFO *const req)
{
RESUME_INFO *const current = Savegame_GetCurrentInfo(level);
memset(current, 0, sizeof(RESUME_INFO));
Requester_Init(req);
for (int32_t i = 0; i < MAX_SAVE_SLOTS; i++) {
const SAVEGAME_INFO *const savegame_info = Savegame_GetSavegameInfo(i);
if (savegame_info->level_title != nullptr) {
char save_num_text[16];
sprintf(save_num_text, "%d", savegame_info->counter);
Requester_AddItem(
req, savegame_info->level_title, REQ_ALIGN_LEFT, save_num_text,
REQ_ALIGN_RIGHT);
} else {
Requester_AddItem(req, GS(MISC_EMPTY_SLOT), 0, 0, 0);
}
}
Requester_SetSize(req, 10, -32);
if (req->selected >= req->visible_count) {
req->line_offset = req->selected - req->visible_count + 1;
} else if (req->selected < req->line_offset) {
req->line_offset = req->selected;
}
memcpy(m_ReqFlags1, g_RequesterFlags1, sizeof(m_ReqFlags1));
memcpy(m_ReqFlags2, g_RequesterFlags2, sizeof(m_ReqFlags2));
}
void Savegame_CarryCurrentInfoToNextLevel(
const GF_LEVEL *const src_level, const GF_LEVEL *const dst_level)
void Savegame_FillAvailableLevels(REQUEST_INFO *const req)
{
LOG_INFO(
"Copying resume info from level #%d to level #%d", src_level->num,
dst_level->num);
RESUME_INFO *const src_resume = Savegame_GetCurrentInfo(src_level);
RESUME_INFO *const dst_resume = Savegame_GetCurrentInfo(dst_level);
memcpy(dst_resume, src_resume, sizeof(RESUME_INFO));
}
void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level)
{
RESUME_INFO *const resume = Savegame_GetCurrentInfo(level);
resume->flags.available = 1;
if (Inv_RequestItem(O_PISTOL_ITEM)) {
resume->flags.has_pistols = 1;
resume->pistol_ammo = 1000;
} else {
resume->flags.has_pistols = 0;
resume->pistol_ammo = 1000;
}
if (Inv_RequestItem(O_SHOTGUN_ITEM)) {
resume->flags.has_shotgun = 1;
resume->shotgun_ammo = g_Lara.shotgun_ammo.ammo;
} else {
resume->flags.has_shotgun = 0;
resume->shotgun_ammo =
Inv_RequestItem(O_SHOTGUN_AMMO_ITEM) * SHOTGUN_AMMO_QTY;
}
if (Inv_RequestItem(O_MAGNUM_ITEM)) {
resume->flags.has_magnums = 1;
resume->magnum_ammo = g_Lara.magnum_ammo.ammo;
} else {
resume->flags.has_magnums = 0;
resume->magnum_ammo =
Inv_RequestItem(O_MAGNUM_AMMO_ITEM) * MAGNUM_AMMO_QTY;
}
if (Inv_RequestItem(O_UZI_ITEM)) {
resume->flags.has_uzis = 1;
resume->uzi_ammo = g_Lara.uzi_ammo.ammo;
} else {
resume->flags.has_uzis = 0;
resume->uzi_ammo = Inv_RequestItem(O_UZI_AMMO_ITEM) * UZI_AMMO_QTY;
}
if (Inv_RequestItem(O_M16_ITEM)) {
resume->flags.has_m16 = 1;
resume->m16_ammo = g_Lara.m16_ammo.ammo;
} else {
resume->flags.has_m16 = 0;
resume->m16_ammo = Inv_RequestItem(O_M16_AMMO_ITEM) * M16_AMMO_QTY;
}
if (Inv_RequestItem(O_HARPOON_ITEM)) {
resume->flags.has_harpoon = 1;
resume->harpoon_ammo = g_Lara.harpoon_ammo.ammo;
} else {
resume->flags.has_harpoon = 0;
resume->harpoon_ammo =
Inv_RequestItem(O_HARPOON_AMMO_ITEM) * HARPOON_AMMO_QTY;
}
if (Inv_RequestItem(O_GRENADE_ITEM)) {
resume->flags.has_grenade = 1;
resume->grenade_ammo = g_Lara.grenade_ammo.ammo;
} else {
resume->flags.has_grenade = 0;
resume->grenade_ammo =
Inv_RequestItem(O_GRENADE_AMMO_ITEM) * GRENADE_AMMO_QTY;
}
resume->flares = Inv_RequestItem(O_FLARE_ITEM);
resume->small_medipacks = Inv_RequestItem(O_SMALL_MEDIPACK_ITEM);
resume->large_medipacks = Inv_RequestItem(O_LARGE_MEDIPACK_ITEM);
if (g_Lara.gun_type == LGT_FLARE) {
resume->equipped_gun_type = g_Lara.last_gun_type;
} else {
resume->equipped_gun_type = g_Lara.gun_type;
}
resume->gun_status = LGS_ARMLESS;
}
bool Savegame_Save(const int32_t slot_idx)
{
bool result = false;
Savegame_BindSlot(slot_idx);
File_CreateDirectory(SAVES_DIR);
const GF_LEVEL *const current_level = Game_GetCurrentLevel();
const char *const level_title = current_level->title;
Savegame_PersistGameToCurrentInfo(current_level);
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx];
const bool was_slot_empty = savegame_info->full_path == nullptr;
m_SaveCounter++;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (!strategy.allow_save || strategy.save_to_file_func == nullptr) {
continue;
}
char *file_name =
String_Format(strategy.get_save_file_pattern_func(), slot_idx);
char *full_path = String_Format("%s/%s", SAVES_DIR, file_name);
MYFILE *const fp = File_Open(full_path, FILE_OPEN_WRITE);
if (fp != nullptr) {
strategy.save_to_file_func(fp, savegame_info);
savegame_info->format = strategy.format;
Memory_FreePointer(&savegame_info->full_path);
savegame_info->full_path = Memory_DupStr(File_GetPath(fp));
savegame_info->counter = m_SaveCounter;
savegame_info->level_num = current_level->num;
savegame_info->level_title =
level_title != nullptr ? Memory_DupStr(level_title) : nullptr;
File_Close(fp);
result = true;
}
Memory_FreePointer(&file_name);
Memory_FreePointer(&full_path);
}
if (result) {
m_NewestSlot = slot_idx;
if (was_slot_empty) {
m_SavedGames++;
}
Savegame_HighlightNewestSlot();
} else {
m_SaveCounter--;
}
return result;
}
bool Savegame_Load(const int32_t slot_idx)
{
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx];
ASSERT(savegame_info->format != 0);
M_LoadPreprocess();
bool result = false;
for (int32_t i = 0; i < m_StrategyCount; i++) {
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
if (strategy.format != savegame_info->format) {
continue;
}
MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ);
if (fp != nullptr) {
result = strategy.load_from_file_func(fp);
File_Close(fp);
}
break;
}
M_LoadPostprocess();
Savegame_SetInitialVersion(m_SavegameInfo[slot_idx].initial_version);
return result;
}
RESUME_INFO *Savegame_GetCurrentInfo(const GF_LEVEL *const level)
{
ASSERT(m_ResumeInfos != nullptr);
ASSERT(level != nullptr);
if (GF_GetLevelTableType(level->type) == GFLT_MAIN) {
return &m_ResumeInfos[level->num];
} else if (level->type == GFL_DEMO) {
return &m_ResumeInfos[GF_GetLevelTable(GFLT_MAIN)->count];
}
LOG_WARNING(
"Warning: unable to get resume info for level %d (type=%s)", level->num,
ENUM_MAP_TO_STRING(GF_LEVEL_TYPE, level->type));
return nullptr;
}
void Savegame_SetDefaultStats(
const GF_LEVEL *const level, const STATS_COMMON stats)
{
if (m_DefaultStats == nullptr) {
m_DefaultStats = Memory_Alloc(
sizeof(STATS_COMMON) * GF_GetLevelTable(GFLT_MAIN)->count);
}
m_DefaultStats[level->num] = stats;
}
STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *const level)
{
if (m_DefaultStats == nullptr
|| (level->type != GFL_NORMAL && level->type != GFL_BONUS)) {
return (STATS_COMMON) {};
}
return m_DefaultStats[level->num];
ASSERT(req != nullptr);
Requester_Init(req);
Requester_SetSize(req, 10, -32);
Requester_SetHeading(req, GS(PASSPORT_SELECT_LEVEL), 0, nullptr, 0);
req->ready = true;
req->selected = 0;
Requester_RemoveAllItems(req);
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
if (level->type != GFL_GYM) {
Requester_AddItem(req, level->title, 0, nullptr, 0);
}
}
}

View file

@ -33,7 +33,6 @@
#define MAX_LEVEL_NAME_SIZE 50 // TODO: get rid of this limit
#define MAX_DEMO_FILES MAX_LEVELS
#define MAX_REQUESTER_ITEMS 24
#define MAX_SAVE_SLOTS 16
#define DEATH_WAIT (5 * 2 * FRAMES_PER_SECOND) // = 300
#define DEATH_WAIT_INPUT (2 * FRAMES_PER_SECOND) // = 60