mirror of
https://github.com/HarbourMasters/Shipwright.git
synced 2024-11-22 09:22:18 -05:00
Modified sectional saving to utilize an integer ID for calling specific sections to cut down on string copying. Utilizes enum values for non-mod sections to allow for cross-project use of those IDs, and sets up AddSaveFunction
to add mod sections after that using max value enum.
This commit is contained in:
parent
4bc0d7d60d
commit
50fbe5d00c
@ -61,6 +61,15 @@ typedef struct {
|
||||
u8 isRoom;
|
||||
} SceneTimestamp;
|
||||
|
||||
typedef enum { // Pre-existing IDs for save sections in base code
|
||||
SECTION_ID_BASE,
|
||||
SECTION_ID_RANDOMIZER,
|
||||
SECTION_ID_STATS,
|
||||
SECTION_ID_ENTRANCES,
|
||||
SECTION_ID_SCENES,
|
||||
SECTION_ID_MAX
|
||||
} SaveFuncIDs;
|
||||
|
||||
typedef struct {
|
||||
/* */ char buildVersion[50];
|
||||
/* */ s16 buildVersionMajor;
|
||||
|
@ -231,8 +231,8 @@ void LoadStatsVersion1() {
|
||||
});
|
||||
}
|
||||
|
||||
void SaveStats(SaveContext* saveContext, const std::string& subSection) {
|
||||
if (subSection == "all") {
|
||||
void SaveStats(SaveContext* saveContext, int sectionID) {
|
||||
if (sectionID == SECTION_ID_BASE) {
|
||||
std::string buildVersion;
|
||||
SaveManager::Instance->LoadData("buildVersion", buildVersion);
|
||||
strncpy(gSaveContext.sohStats.buildVersion, buildVersion.c_str(), ARRAY_COUNT(gSaveContext.sohStats.buildVersion) - 1);
|
||||
@ -268,12 +268,12 @@ void SaveStats(SaveContext* saveContext, const std::string& subSection) {
|
||||
SaveManager::Instance->LoadData("", gSaveContext.sohStats.locationsSkipped[i]);
|
||||
});
|
||||
}
|
||||
if (subSection == "entrances" || subSection == "all") {
|
||||
if (sectionID == SECTION_ID_ENTRANCES || sectionID == SECTION_ID_BASE) {
|
||||
SaveManager::Instance->SaveArray("entrancesDiscovered", ARRAY_COUNT(saveContext->sohStats.entrancesDiscovered), [&](size_t i) {
|
||||
SaveManager::Instance->SaveData("", saveContext->sohStats.entrancesDiscovered[i]);
|
||||
});
|
||||
}
|
||||
if (subSection == "scenes" || subSection == "all") {
|
||||
if (sectionID == SECTION_ID_SCENES || sectionID == SECTION_ID_BASE) {
|
||||
SaveManager::Instance->SaveArray("scenesDiscovered", ARRAY_COUNT(saveContext->sohStats.scenesDiscovered), [&](size_t i) {
|
||||
SaveManager::Instance->SaveData("", saveContext->sohStats.scenesDiscovered[i]);
|
||||
});
|
||||
@ -821,7 +821,10 @@ extern "C" void InitStatTracker() {
|
||||
SetupDisplayNames();
|
||||
SetupDisplayColors();
|
||||
SaveManager::Instance->AddLoadFunction("sohStats", 1, LoadStatsVersion1);
|
||||
SaveManager::Instance->AddSaveFunction("sohStats", 1, SaveStats);
|
||||
// Add main section save, no parent
|
||||
SaveManager::Instance->AddSaveFunction("sohStats", 1, SaveStats, true, SECTION_PARENT_NONE);
|
||||
// Add subsections, parent of "sohStats". Not sure how to do this without the redundant references to "SaveStats"
|
||||
SaveManager::Instance->AddSaveFunction("entrances", 1, SaveStats, false, SECTION_ID_STATS);
|
||||
SaveManager::Instance->AddSaveFunction("scenes", 1, SaveStats, false, SECTION_ID_STATS);
|
||||
SaveManager::Instance->AddInitFunction(InitStats);
|
||||
SaveManager::Instance->RegisterGameSaveSection("sohStats");
|
||||
}
|
@ -769,7 +769,8 @@ void Entrance_SetSceneDiscovered(u8 sceneNum) {
|
||||
u32 sceneBit = 1 << (sceneNum - (idx * bitsPerIndex));
|
||||
gSaveContext.sohStats.scenesDiscovered[idx] |= sceneBit;
|
||||
}
|
||||
Save_SaveSection("sohStats.scenes");
|
||||
// Save scenesDiscovered
|
||||
Save_SaveSection(SECTION_ID_SCENES);
|
||||
}
|
||||
|
||||
u8 Entrance_GetIsEntranceDiscovered(u16 entranceIndex) {
|
||||
@ -802,5 +803,6 @@ void Entrance_SetEntranceDiscovered(u16 entranceIndex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Save_SaveSection("sohStats.entrances");
|
||||
// Save entrancesDiscovered
|
||||
Save_SaveSection(SECTION_ID_ENTRANCES);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
#include <array>
|
||||
|
||||
extern "C" SaveContext gSaveContext;
|
||||
using namespace std::string_literals;
|
||||
|
||||
void SaveManager::WriteSaveFile(const std::filesystem::path& savePath, const uintptr_t addr, void* dramAddr,
|
||||
const size_t size) {
|
||||
@ -46,17 +47,20 @@ std::filesystem::path SaveManager::GetFileName(int fileNum) {
|
||||
}
|
||||
|
||||
SaveManager::SaveManager() {
|
||||
coreSectionIDsByName["base"] = SECTION_ID_BASE;
|
||||
coreSectionIDsByName["randomizer"] = SECTION_ID_RANDOMIZER;
|
||||
coreSectionIDsByName["sohStats"] = SECTION_ID_STATS;
|
||||
coreSectionIDsByName["entrances"] = SECTION_ID_ENTRANCES;
|
||||
coreSectionIDsByName["scenes"] = SECTION_ID_SCENES;
|
||||
AddLoadFunction("base", 1, LoadBaseVersion1);
|
||||
AddLoadFunction("base", 2, LoadBaseVersion2);
|
||||
AddLoadFunction("base", 3, LoadBaseVersion3);
|
||||
AddLoadFunction("base", 4, LoadBaseVersion4);
|
||||
AddSaveFunction("base", 4, SaveBase);
|
||||
RegisterGameSaveSection("base");
|
||||
AddSaveFunction("base", 4, SaveBase, true, SECTION_PARENT_NONE);
|
||||
|
||||
AddLoadFunction("randomizer", 1, LoadRandomizerVersion1);
|
||||
AddLoadFunction("randomizer", 2, LoadRandomizerVersion2);
|
||||
AddSaveFunction("randomizer", 2, SaveRandomizer);
|
||||
RegisterGameSaveSection("randomizer");
|
||||
AddSaveFunction("randomizer", 2, SaveRandomizer, true, SECTION_PARENT_NONE);
|
||||
|
||||
AddInitFunction(InitFileImpl);
|
||||
|
||||
@ -86,20 +90,6 @@ SaveManager::SaveManager() {
|
||||
}
|
||||
}
|
||||
|
||||
// GameSaveSection functions affect the list of sections that save their data when a game save is triggered
|
||||
void SaveManager::RegisterGameSaveSection(std::string section) {
|
||||
if (std::find(gameSaveRegistry.begin(), gameSaveRegistry.end(), section) == gameSaveRegistry.end()) {
|
||||
gameSaveRegistry.push_back(section);
|
||||
}
|
||||
}
|
||||
|
||||
void SaveManager::UnregisterAutosaveSection(std::string section) {
|
||||
auto find = std::find(gameSaveRegistry.begin(), gameSaveRegistry.end(), section);
|
||||
if (find != gameSaveRegistry.end()) {
|
||||
gameSaveRegistry.erase(find);
|
||||
}
|
||||
}
|
||||
|
||||
void SaveManager::LoadRandomizerVersion1() {
|
||||
for (int i = 0; i < ARRAY_COUNT(gSaveContext.itemLocations); i++) {
|
||||
SaveManager::Instance->LoadStruct("get" + std::to_string(i), [&]() {
|
||||
@ -278,7 +268,7 @@ void SaveManager::LoadRandomizerVersion2() {
|
||||
});
|
||||
}
|
||||
|
||||
void SaveManager::SaveRandomizer(SaveContext* saveContext, const std::string& subString) {
|
||||
void SaveManager::SaveRandomizer(SaveContext* saveContext, int sectionID) {
|
||||
|
||||
if(!saveContext->n64ddFlag) return;
|
||||
|
||||
@ -700,40 +690,42 @@ void SaveManager::InitFileDebug() {
|
||||
gSaveContext.sceneFlags[5].swch = 0x40000000;
|
||||
}
|
||||
|
||||
// Threaded SaveFile takes copy of gSaveContext for local unmodified storage
|
||||
void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, const std::string& sectionString) {
|
||||
void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int sectionID) {
|
||||
// Needed for first time save, hasn't changed in forever anyway
|
||||
saveBlock["version"] = 1;
|
||||
size_t period = sectionString.find(".");
|
||||
std::string section = sectionString;
|
||||
std::string subsection = "";
|
||||
if (period != std::string::npos) {
|
||||
subsection = sectionString.substr(period + 1, std::string::npos);
|
||||
section = sectionString.substr(0, sectionString.length() - (subsection.length() + 1));
|
||||
}
|
||||
if (sectionString == "all") {
|
||||
for (auto& sectionHandler : sectionSaveHandlers) {
|
||||
if (std::find(gameSaveRegistry.begin(), gameSaveRegistry.end(), sectionHandler.first) != gameSaveRegistry.end()) {
|
||||
nlohmann::json& sectionBlock = saveBlock["sections"][sectionHandler.first];
|
||||
sectionBlock["version"] = sectionHandler.second.first;
|
||||
// If any save file is loaded for medatata, or a spoiler log is loaded (not sure which at this point), there is still data in the "randomizer" section
|
||||
// This clears the randomizer data block if and only if the section being called is "randomizer" and n64ddFlag is false.
|
||||
if (sectionHandler.first == "randomizer" && !gSaveContext.n64ddFlag) {
|
||||
sectionBlock["data"] = nlohmann::json::object();
|
||||
}
|
||||
|
||||
currentJsonContext = §ionBlock["data"];
|
||||
sectionHandler.second.second(saveContext, "all");
|
||||
if (sectionID == SECTION_ID_BASE) {
|
||||
for (auto& sectionHandlerPair : sectionSaveHandlers) {
|
||||
auto& saveFuncInfo = sectionHandlerPair.second;
|
||||
// Don't call SaveFuncs for sections that aren't tied to game save
|
||||
if (!saveFuncInfo.saveWithBase) {
|
||||
continue;
|
||||
}
|
||||
nlohmann::json& sectionBlock = saveBlock["sections"][saveFuncInfo.name];
|
||||
sectionBlock["version"] = sectionHandlerPair.second.version;
|
||||
// If any save file is loaded for medatata, or a spoiler log is loaded (not sure which at this point), there is still data in the "randomizer" section
|
||||
// This clears the randomizer data block if and only if the section being called is "randomizer" and n64ddFlag is false.
|
||||
if (sectionHandlerPair.second.name == "randomizer" && !gSaveContext.n64ddFlag) {
|
||||
sectionBlock["data"] = nlohmann::json::object();
|
||||
continue;
|
||||
}
|
||||
|
||||
currentJsonContext = §ionBlock["data"];
|
||||
sectionHandlerPair.second.func(saveContext, sectionID);
|
||||
}
|
||||
} else if (sectionSaveHandlers.contains(section)) {
|
||||
SectionSaveHandler handler = sectionSaveHandlers.find(section)->second;
|
||||
nlohmann::json& sectionBlock = saveBlock["sections"][section];
|
||||
sectionBlock["version"] = handler.first;
|
||||
currentJsonContext = §ionBlock["data"];
|
||||
handler.second(saveContext, subsection);
|
||||
} else {
|
||||
// save function for specified section does not exist. should this error?
|
||||
return;
|
||||
SaveFuncInfo svi = sectionSaveHandlers.find(sectionID)->second;
|
||||
auto& sectionName = svi.name;
|
||||
auto sectionVersion = svi.version;
|
||||
// If section has a parentSection, it is a subsection. Load parentSection version and set sectionBlock to parent string
|
||||
if (svi.parentSection != -1 && svi.parentSection < sectionIndex) {
|
||||
auto parentSvi = sectionSaveHandlers.find(svi.parentSection)->second;
|
||||
sectionName = parentSvi.name;
|
||||
sectionVersion = parentSvi.version;
|
||||
}
|
||||
nlohmann::json& sectionBlock = saveBlock["sections"][sectionName];
|
||||
sectionBlock["version"] = sectionVersion;
|
||||
currentJsonContext = §ionBlock["data"];
|
||||
svi.func(saveContext, sectionID);
|
||||
}
|
||||
|
||||
#if defined(__SWITCH__) || defined(__WIIU__)
|
||||
@ -751,18 +743,24 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, const
|
||||
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(fileNum);
|
||||
}
|
||||
|
||||
void SaveManager::SaveSection(int fileNum, const std::string& sectionString) {
|
||||
// SaveSection creates a copy of gSaveContext to prevent mid-save data modification, and passes its reference to SaveFileThreaded
|
||||
void SaveManager::SaveSection(int fileNum, int sectionID) {
|
||||
if (fileNum == 0xFF) {
|
||||
return;
|
||||
}
|
||||
// Can't think of any time the promise would be needed, so use push_task instead of submit
|
||||
// Don't save a nonexistent section
|
||||
if (sectionID >= sectionIndex) {
|
||||
SPDLOG_ERROR("SaveSection: Section ID not registered.");
|
||||
return;
|
||||
}
|
||||
auto saveContext = new SaveContext;
|
||||
memcpy(saveContext, &gSaveContext, sizeof(gSaveContext));
|
||||
smThreadPool->push_task_back(&SaveManager::SaveFileThreaded, this, fileNum, saveContext, sectionString);
|
||||
// Can't think of any time the promise would be needed, so use push_task instead of submit
|
||||
smThreadPool->push_task_back(&SaveManager::SaveFileThreaded, this, fileNum, saveContext, sectionID);
|
||||
}
|
||||
|
||||
void SaveManager::SaveFile(int fileNum) {
|
||||
SaveSection(fileNum, "all");
|
||||
SaveSection(fileNum, SECTION_ID_BASE);
|
||||
}
|
||||
|
||||
void SaveManager::SaveGlobal() {
|
||||
@ -860,14 +858,21 @@ void SaveManager::AddLoadFunction(const std::string& name, int version, LoadFunc
|
||||
sectionLoadHandlers[name][version] = func;
|
||||
}
|
||||
|
||||
void SaveManager::AddSaveFunction(const std::string& name, int version, SaveFunc func) {
|
||||
if (sectionSaveHandlers.contains(name)) {
|
||||
void SaveManager::AddSaveFunction(const std::string& name, int version, SaveFunc func, bool saveWithBase, int parentSection = -1) {
|
||||
if (sectionRegistry.contains(name)) {
|
||||
SPDLOG_ERROR("Adding save function for section that already has one: " + name);
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
sectionSaveHandlers[name] = std::make_pair(version, func);
|
||||
int index = sectionIndex;
|
||||
if (coreSectionIDsByName.contains(name)) {
|
||||
index = coreSectionIDsByName.find(name)->second;
|
||||
} else {
|
||||
sectionIndex++;
|
||||
}
|
||||
SaveFuncInfo sfi = { name, version, func, saveWithBase, parentSection };
|
||||
sectionSaveHandlers.emplace(index, sfi);
|
||||
sectionRegistry.emplace(name);
|
||||
}
|
||||
|
||||
void SaveManager::AddPostFunction(const std::string& name, PostFunc func) {
|
||||
@ -1623,7 +1628,7 @@ void SaveManager::LoadBaseVersion4() {
|
||||
SaveManager::Instance->LoadData("dogParams", gSaveContext.dogParams);
|
||||
}
|
||||
|
||||
void SaveManager::SaveBase(SaveContext* saveContext, const std::string& subString) {
|
||||
void SaveManager::SaveBase(SaveContext* saveContext, int sectionID) {
|
||||
SaveManager::Instance->SaveData("entranceIndex", saveContext->entranceIndex);
|
||||
SaveManager::Instance->SaveData("linkAge", saveContext->linkAge);
|
||||
SaveManager::Instance->SaveData("cutsceneIndex", saveContext->cutsceneIndex);
|
||||
@ -2290,8 +2295,8 @@ extern "C" void Save_SaveFile(void) {
|
||||
SaveManager::Instance->SaveFile(gSaveContext.fileNum);
|
||||
}
|
||||
|
||||
extern "C" void Save_SaveSection(char* sectionString) {
|
||||
SaveManager::Instance->SaveSection(gSaveContext.fileNum, sectionString);
|
||||
extern "C" void Save_SaveSection(int sectionID) {
|
||||
SaveManager::Instance->SaveSection(gSaveContext.fileNum, sectionID);
|
||||
}
|
||||
|
||||
extern "C" void Save_SaveGlobal(void) {
|
||||
@ -2307,8 +2312,8 @@ extern "C" void Save_AddLoadFunction(char* name, int version, SaveManager::LoadF
|
||||
SaveManager::Instance->AddLoadFunction(name, version, func);
|
||||
}
|
||||
|
||||
extern "C" void Save_AddSaveFunction(char* name, int version, SaveManager::SaveFunc func) {
|
||||
SaveManager::Instance->AddSaveFunction(name, version, func);
|
||||
extern "C" void Save_AddSaveFunction(char* name, int version, SaveManager::SaveFunc func, bool saveWithBase, int parentSection = SECTION_PARENT_NONE) {
|
||||
SaveManager::Instance->AddSaveFunction(name, version, func, saveWithBase, parentSection);
|
||||
}
|
||||
|
||||
extern "C" SaveFileMetaInfo* Save_GetSaveMetaInfo(int fileNum) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <libultraship/libultra/gbi.h>
|
||||
|
||||
#define SECTION_PARENT_NONE -1
|
||||
typedef struct {
|
||||
u8 valid;
|
||||
u16 deaths;
|
||||
@ -27,6 +28,7 @@ typedef struct {
|
||||
#include <tuple>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <filesystem>
|
||||
#include "thread-pool/BS_thread_pool.hpp"
|
||||
|
||||
@ -38,6 +40,7 @@ extern "C" {
|
||||
|
||||
class SaveManager {
|
||||
public:
|
||||
|
||||
static SaveManager* Instance;
|
||||
|
||||
static void WriteSaveFile(const std::filesystem::path& savePath, uintptr_t addr, void* dramAddr, size_t size);
|
||||
@ -45,15 +48,23 @@ public:
|
||||
|
||||
using InitFunc = void(*)(bool isDebug);
|
||||
using LoadFunc = void(*)();
|
||||
using SaveFunc = void(*)(SaveContext* saveContext, const std::string& subSection);
|
||||
using PostFunc = void(*)(int version);
|
||||
using SaveFunc = void(*)(SaveContext* saveContext, int sectionID);
|
||||
using PostFunc = void (*)(int version);
|
||||
|
||||
typedef struct {
|
||||
std::string name;
|
||||
int version;
|
||||
SaveManager::SaveFunc func;
|
||||
bool saveWithBase;
|
||||
int parentSection;
|
||||
} SaveFuncInfo;
|
||||
|
||||
SaveManager();
|
||||
|
||||
void Init();
|
||||
void InitFile(bool isDebug);
|
||||
void SaveFile(int fileNum);
|
||||
void SaveSection(int fileNum, const std::string& sectionString);
|
||||
void SaveSection(int fileNum, int sectionID);
|
||||
void SaveGlobal();
|
||||
void LoadFile(int fileNum);
|
||||
bool SaveFile_Exist(int fileNum);
|
||||
@ -66,7 +77,7 @@ public:
|
||||
void AddLoadFunction(const std::string& name, int version, LoadFunc func);
|
||||
|
||||
// Adds a function that is called when saving. This should only be called once for each function, the version is filled in automatically.
|
||||
void AddSaveFunction(const std::string& name, int version, SaveFunc func);
|
||||
void AddSaveFunction(const std::string& name, int version, SaveFunc func, bool saveWithBase, int parentSection);
|
||||
|
||||
// Adds a function to be called after loading is complete. This is to handle any cleanup required from loading old versions.
|
||||
void AddPostFunction(const std::string& name, PostFunc func);
|
||||
@ -119,9 +130,6 @@ public:
|
||||
static const int MaxFiles = 3;
|
||||
std::array<SaveFileMetaInfo, MaxFiles> fileMetaInfo;
|
||||
|
||||
void RegisterGameSaveSection(std::string section);
|
||||
void UnregisterAutosaveSection(std::string section);
|
||||
|
||||
private:
|
||||
std::filesystem::path GetFileName(int fileNum);
|
||||
nlohmann::json saveBlock;
|
||||
@ -129,7 +137,7 @@ public:
|
||||
void ConvertFromUnversioned();
|
||||
void CreateDefaultGlobal();
|
||||
|
||||
void SaveFileThreaded(int fileNum, SaveContext* saveContext, const std::string& sectionString);
|
||||
void SaveFileThreaded(int fileNum, SaveContext* saveContext, int sectionID);
|
||||
|
||||
void InitMeta(int slotNum);
|
||||
static void InitFileImpl(bool isDebug);
|
||||
@ -138,23 +146,23 @@ public:
|
||||
|
||||
static void LoadRandomizerVersion1();
|
||||
static void LoadRandomizerVersion2();
|
||||
static void SaveRandomizer(SaveContext* saveContext, const std::string& subString);
|
||||
static void SaveRandomizer(SaveContext* saveContext, int sectionID);
|
||||
|
||||
static void LoadBaseVersion1();
|
||||
static void LoadBaseVersion2();
|
||||
static void LoadBaseVersion3();
|
||||
static void LoadBaseVersion4();
|
||||
static void SaveBase(SaveContext* saveContext, const std::string& subString);
|
||||
static void SaveBase(SaveContext* saveContext, int sectionID);
|
||||
|
||||
std::vector<InitFunc> initFuncs;
|
||||
|
||||
using SectionLoadHandler = std::map<int, LoadFunc>;
|
||||
std::map<std::string, SectionLoadHandler> sectionLoadHandlers;
|
||||
|
||||
using SectionSaveHandler = std::pair<int, SaveFunc>;
|
||||
std::map<std::string, SectionSaveHandler> sectionSaveHandlers;
|
||||
// tracks sections to save during game saves
|
||||
std::vector<std::string> gameSaveRegistry;
|
||||
int sectionIndex = SECTION_ID_MAX;
|
||||
std::map<std::string, int> coreSectionIDsByName;
|
||||
std::map<int, SaveFuncInfo> sectionSaveHandlers;
|
||||
std::set<std::string> sectionRegistry;
|
||||
|
||||
std::map<std::string, PostFunc> postHandlers;
|
||||
|
||||
@ -168,16 +176,16 @@ public:
|
||||
// TODO feature parity to the C++ interface. We need Save_AddInitFunction and Save_AddPostFunction at least
|
||||
|
||||
typedef void (*Save_LoadFunc)(void);
|
||||
typedef void (*Save_SaveFunc)(const SaveContext* saveContext);
|
||||
typedef void (*Save_SaveFunc)(const SaveContext* saveContext, int sectionID);
|
||||
|
||||
void Save_Init(void);
|
||||
void Save_InitFile(int isDebug);
|
||||
void Save_SaveFile(void);
|
||||
void Save_SaveSection(char* sectionString);
|
||||
void Save_SaveSection(int sectionID);
|
||||
void Save_SaveGlobal(void);
|
||||
void Save_LoadGlobal(void);
|
||||
void Save_AddLoadFunction(char* name, int version, Save_LoadFunc func);
|
||||
void Save_AddSaveFunction(char* name, int version, Save_SaveFunc func);
|
||||
void Save_AddSaveFunction(char* name, int version, Save_SaveFunc func, bool saveWithBase, int parentSection);
|
||||
SaveFileMetaInfo* Save_GetSaveMetaInfo(int fileNum);
|
||||
void Save_CopyFile(int from, int to);
|
||||
void Save_DeleteFile(int fileNum);
|
||||
|
Loading…
Reference in New Issue
Block a user