717 lines
16 KiB
C++
717 lines
16 KiB
C++
#include "skse64/Serialization.h"
|
|
#include "common/IFileStream.h"
|
|
#include "skse64/PluginManager.h"
|
|
#include "GameAPI.h"
|
|
#include "skse64_common/skse_version.h"
|
|
#include <vector>
|
|
#include <shlobj.h>
|
|
#include "GameData.h"
|
|
#include "skse64/InternalSerialization.h"
|
|
#include "skse64/GameSettings.h"
|
|
#include "skse64/ScaleformCallbacks.h"
|
|
|
|
namespace Serialization
|
|
{
|
|
const char * kSavegamePath = "\\My Games\\Skyrim Special Edition\\";
|
|
|
|
// file format internals
|
|
|
|
// general format:
|
|
// Header header
|
|
// PluginHeader plugin[header.numPlugins]
|
|
// ChunkHeader chunk[plugin.numChunks]
|
|
// UInt8 data[chunk.length]
|
|
|
|
struct Header
|
|
{
|
|
enum
|
|
{
|
|
kSignature = MACRO_SWAP32('SKSE'), // endian-swapping so the order matches
|
|
kVersion = 1,
|
|
|
|
kVersion_Invalid = 0
|
|
};
|
|
|
|
UInt32 signature;
|
|
UInt32 formatVersion;
|
|
UInt32 skseVersion;
|
|
UInt32 runtimeVersion;
|
|
UInt32 numPlugins;
|
|
};
|
|
|
|
struct PluginHeader
|
|
{
|
|
UInt32 signature;
|
|
UInt32 numChunks;
|
|
UInt32 length; // length of following data including ChunkHeader
|
|
};
|
|
|
|
struct ChunkHeader
|
|
{
|
|
UInt32 type;
|
|
UInt32 version;
|
|
UInt32 length;
|
|
};
|
|
|
|
// locals
|
|
|
|
std::string s_savePath;
|
|
IFileStream s_currentFile;
|
|
|
|
typedef std::vector <PluginCallbacks> PluginCallbackList;
|
|
PluginCallbackList s_pluginCallbacks;
|
|
|
|
PluginHandle s_currentPlugin = 0;
|
|
|
|
Header s_fileHeader = { 0 };
|
|
|
|
UInt64 s_pluginHeaderOffset = 0;
|
|
PluginHeader s_pluginHeader = { 0 };
|
|
|
|
bool s_chunkOpen = false;
|
|
UInt64 s_chunkHeaderOffset = 0;
|
|
ChunkHeader s_chunkHeader = { 0 };
|
|
|
|
// utilities
|
|
|
|
// make full path from save name
|
|
std::string MakeSavePath(std::string name, const char * extension)
|
|
{
|
|
char path[MAX_PATH];
|
|
ASSERT(SUCCEEDED(SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, path)));
|
|
|
|
std::string result = path;
|
|
result += kSavegamePath;
|
|
Setting* localSavePath = GetINISetting("sLocalSavePath:General");
|
|
if(localSavePath && (localSavePath->GetType() == Setting::kType_String))
|
|
result += localSavePath->data.s;
|
|
else
|
|
result += "Saves\\";
|
|
|
|
result += "\\";
|
|
result += name;
|
|
if (extension)
|
|
result += extension;
|
|
return result;
|
|
}
|
|
|
|
PluginCallbacks * GetPluginInfo(PluginHandle plugin)
|
|
{
|
|
if(plugin >= s_pluginCallbacks.size())
|
|
s_pluginCallbacks.resize(plugin + 1);
|
|
|
|
return &s_pluginCallbacks[plugin];
|
|
}
|
|
|
|
// plugin API
|
|
void SetUniqueID(PluginHandle plugin, UInt32 uid)
|
|
{
|
|
// check existing plugins
|
|
for(PluginCallbackList::iterator iter = s_pluginCallbacks.begin(); iter != s_pluginCallbacks.end(); ++iter)
|
|
{
|
|
if(iter->hadUID && (iter->uid == uid))
|
|
{
|
|
UInt32 collidingID = iter - s_pluginCallbacks.begin();
|
|
|
|
_ERROR("plugin serialization UID collision (uid = %08X, plugins = %d %d)", plugin, uid, collidingID);
|
|
}
|
|
}
|
|
|
|
PluginCallbacks * info = GetPluginInfo(plugin);
|
|
|
|
ASSERT(!info->hadUID);
|
|
|
|
info->uid = uid;
|
|
info->hadUID = true;
|
|
}
|
|
|
|
void SetRevertCallback(PluginHandle plugin, SKSESerializationInterface::EventCallback callback)
|
|
{
|
|
GetPluginInfo(plugin)->revert = callback;
|
|
}
|
|
|
|
void SetSaveCallback(PluginHandle plugin, SKSESerializationInterface::EventCallback callback)
|
|
{
|
|
GetPluginInfo(plugin)->save = callback;
|
|
}
|
|
|
|
void SetLoadCallback(PluginHandle plugin, SKSESerializationInterface::EventCallback callback)
|
|
{
|
|
GetPluginInfo(plugin)->load = callback;
|
|
}
|
|
|
|
void SetFormDeleteCallback(PluginHandle plugin, SKSESerializationInterface::FormDeleteCallback callback)
|
|
{
|
|
GetPluginInfo(plugin)->formDelete = callback;
|
|
}
|
|
|
|
void SetSaveName(const char * name)
|
|
{
|
|
if(name)
|
|
{
|
|
std::string save_name(name);
|
|
|
|
if (save_name.length() >= 4 && _stricmp(name+save_name.length()-4, ".ess") == 0)
|
|
save_name = save_name.substr(0, save_name.length() - 4);
|
|
|
|
_MESSAGE("save name is %s", save_name.c_str());
|
|
s_savePath = MakeSavePath(save_name, ".skse");
|
|
_MESSAGE("full save path: %s", s_savePath.c_str());
|
|
}
|
|
else
|
|
{
|
|
_MESSAGE("cleared save path");
|
|
s_savePath.clear();
|
|
}
|
|
}
|
|
|
|
bool WriteRecord(UInt32 type, UInt32 version, const void * buf, UInt32 length)
|
|
{
|
|
if(!OpenRecord(type, version))
|
|
return false;
|
|
|
|
return WriteRecordData(buf, length);
|
|
}
|
|
|
|
// flush a chunk header to the file if one is currently open
|
|
static void FlushWriteChunk(void)
|
|
{
|
|
if(!s_chunkOpen)
|
|
return;
|
|
|
|
UInt64 curOffset = s_currentFile.GetOffset();
|
|
UInt64 chunkSize = curOffset - s_chunkHeaderOffset - sizeof(s_chunkHeader);
|
|
|
|
ASSERT(chunkSize < 0x80000000); // stupidity check
|
|
|
|
s_chunkHeader.length = (UInt32)chunkSize;
|
|
|
|
s_currentFile.SetOffset(s_chunkHeaderOffset);
|
|
s_currentFile.WriteBuf(&s_chunkHeader, sizeof(s_chunkHeader));
|
|
|
|
s_currentFile.SetOffset(curOffset);
|
|
|
|
s_pluginHeader.length += chunkSize + sizeof(s_chunkHeader);
|
|
|
|
s_chunkOpen = false;
|
|
}
|
|
|
|
bool OpenRecord(UInt32 type, UInt32 version)
|
|
{
|
|
if(!s_pluginHeader.numChunks)
|
|
{
|
|
ASSERT(!s_chunkOpen);
|
|
|
|
s_pluginHeaderOffset = s_currentFile.GetOffset();
|
|
s_currentFile.Skip(sizeof(s_pluginHeader));
|
|
}
|
|
|
|
FlushWriteChunk();
|
|
|
|
s_chunkHeaderOffset = s_currentFile.GetOffset();
|
|
s_currentFile.Skip(sizeof(s_chunkHeader));
|
|
|
|
s_pluginHeader.numChunks++;
|
|
|
|
s_chunkHeader.type = type;
|
|
s_chunkHeader.version = version;
|
|
s_chunkHeader.length = 0;
|
|
|
|
s_chunkOpen = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool WriteRecordData(const void * buf, UInt32 length)
|
|
{
|
|
s_currentFile.WriteBuf(buf, length);
|
|
|
|
return true;
|
|
}
|
|
|
|
static void FlushReadRecord(void)
|
|
{
|
|
if(s_chunkOpen)
|
|
{
|
|
if(s_chunkHeader.length)
|
|
{
|
|
// _WARNING("plugin didn't finish reading chunk");
|
|
s_currentFile.Skip(s_chunkHeader.length);
|
|
}
|
|
|
|
s_chunkOpen = false;
|
|
}
|
|
}
|
|
|
|
bool GetNextRecordInfo(UInt32 * type, UInt32 * version, UInt32 * length)
|
|
{
|
|
FlushReadRecord();
|
|
|
|
if(!s_pluginHeader.numChunks)
|
|
return false;
|
|
|
|
s_pluginHeader.numChunks--;
|
|
|
|
s_currentFile.ReadBuf(&s_chunkHeader, sizeof(s_chunkHeader));
|
|
|
|
*type = s_chunkHeader.type;
|
|
*version = s_chunkHeader.version;
|
|
*length = s_chunkHeader.length;
|
|
|
|
s_chunkOpen = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
UInt32 ReadRecordData(void * buf, UInt32 length)
|
|
{
|
|
ASSERT(s_chunkOpen);
|
|
|
|
if(length > s_chunkHeader.length)
|
|
length = s_chunkHeader.length;
|
|
|
|
s_currentFile.ReadBuf(buf, length);
|
|
|
|
s_chunkHeader.length -= length;
|
|
|
|
return length;
|
|
}
|
|
|
|
bool ResolveFormId(UInt32 formId, UInt32 * formIdOut)
|
|
{
|
|
UInt32 modID = formId >> 24;
|
|
if (modID == 0xFF)
|
|
{
|
|
*formIdOut = formId;
|
|
return true;
|
|
}
|
|
|
|
if (modID == 0xFE)
|
|
{
|
|
modID = formId >> 12;
|
|
}
|
|
|
|
UInt32 loadedModID = ResolveModIndex(modID);
|
|
if (loadedModID < 0xFF)
|
|
{
|
|
*formIdOut = (formId & 0x00FFFFFF) | (((UInt32)loadedModID) << 24);
|
|
return true;
|
|
}
|
|
else if (loadedModID > 0xFF)
|
|
{
|
|
*formIdOut = (loadedModID << 12) | (formId & 0x00000FFF);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ResolveHandle(UInt64 handle, UInt64 * handleOut)
|
|
{
|
|
UInt32 modID = (handle & 0xFF000000) >> 24;
|
|
if (modID == 0xFF)
|
|
{
|
|
*handleOut = handle;
|
|
return true;
|
|
}
|
|
|
|
if (modID == 0xFE)
|
|
{
|
|
modID = (handle >> 12) & 0xFFFFF;
|
|
}
|
|
|
|
UInt64 loadedModID = (UInt64)ResolveModIndex(modID);
|
|
if (loadedModID < 0xFF)
|
|
{
|
|
*handleOut = (handle & 0xFFFFFFFF00FFFFFF) | (((UInt64)loadedModID) << 24);
|
|
return true;
|
|
}
|
|
else if (loadedModID > 0xFF)
|
|
{
|
|
*handleOut = (handle & 0xFFFFFFFF00000FFF) | (loadedModID << 12);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// internal event handlers
|
|
void HandleRevertGlobalData(void)
|
|
{
|
|
for(UInt32 i = 0; i < s_pluginCallbacks.size(); i++)
|
|
if(s_pluginCallbacks[i].revert)
|
|
s_pluginCallbacks[i].revert(&g_SKSESerializationInterface);
|
|
}
|
|
|
|
void HandleSaveGlobalData(void)
|
|
{
|
|
_MESSAGE("creating co-save");
|
|
|
|
DeleteFile(s_savePath.c_str());
|
|
|
|
if(!s_currentFile.Create(s_savePath.c_str()))
|
|
{
|
|
_ERROR("HandleSaveGlobalData: couldn't create save file (%s)", s_savePath.c_str());
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// init header
|
|
s_fileHeader.signature = Header::kSignature;
|
|
s_fileHeader.formatVersion = Header::kVersion;
|
|
s_fileHeader.skseVersion = PACKED_SKSE_VERSION;
|
|
s_fileHeader.runtimeVersion = RUNTIME_VERSION;
|
|
s_fileHeader.numPlugins = 0;
|
|
|
|
s_currentFile.Skip(sizeof(s_fileHeader));
|
|
|
|
// iterate through plugins
|
|
for(UInt32 i = 0; i < s_pluginCallbacks.size(); i++)
|
|
{
|
|
PluginCallbacks * info = &s_pluginCallbacks[i];
|
|
|
|
if(info->save && info->hadUID)
|
|
{
|
|
// set up header info
|
|
s_currentPlugin = i;
|
|
|
|
s_pluginHeader.signature = info->uid;
|
|
s_pluginHeader.numChunks = 0;
|
|
s_pluginHeader.length = 0;
|
|
|
|
s_chunkOpen = false;
|
|
|
|
// call the plugin
|
|
try
|
|
{
|
|
info->save(&g_SKSESerializationInterface);
|
|
}
|
|
catch( ... )
|
|
{
|
|
_ERROR("HandleSaveGlobalData: exception occurred saving %08X at %016I64X data may be corrupt.", s_pluginHeader.signature, s_currentFile.GetOffset());
|
|
}
|
|
|
|
// flush the remaining chunk data
|
|
FlushWriteChunk();
|
|
|
|
if(s_pluginHeader.numChunks)
|
|
{
|
|
UInt64 curOffset = s_currentFile.GetOffset();
|
|
|
|
s_currentFile.SetOffset(s_pluginHeaderOffset);
|
|
s_currentFile.WriteBuf(&s_pluginHeader, sizeof(s_pluginHeader));
|
|
|
|
s_currentFile.SetOffset(curOffset);
|
|
|
|
s_fileHeader.numPlugins++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// write header
|
|
s_currentFile.SetOffset(0);
|
|
s_currentFile.WriteBuf(&s_fileHeader, sizeof(s_fileHeader));
|
|
}
|
|
catch(...)
|
|
{
|
|
_ERROR("HandleSaveGame: exception during save");
|
|
}
|
|
|
|
s_currentFile.Close();
|
|
}
|
|
|
|
void HandleLoadGlobalData(void)
|
|
{
|
|
_MESSAGE("loading co-save");
|
|
|
|
if(!s_currentFile.Open(s_savePath.c_str()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Header header;
|
|
|
|
s_currentFile.ReadBuf(&header, sizeof(header));
|
|
|
|
if(header.signature != Header::kSignature)
|
|
{
|
|
_ERROR("HandleLoadGame: invalid file signature (found %08X expected %08X)", header.signature, Header::kSignature);
|
|
goto done;
|
|
}
|
|
|
|
if(header.formatVersion <= Header::kVersion_Invalid)
|
|
{
|
|
_ERROR("HandleLoadGame: version invalid (%08X)", header.formatVersion);
|
|
goto done;
|
|
}
|
|
|
|
if(header.formatVersion > Header::kVersion)
|
|
{
|
|
_ERROR("HandleLoadGame: version too new (found %08X current %08X)", header.formatVersion, Header::kVersion);
|
|
goto done;
|
|
}
|
|
|
|
// reset flags
|
|
for(PluginCallbackList::iterator iter = s_pluginCallbacks.begin(); iter != s_pluginCallbacks.end(); ++iter)
|
|
iter->hadData = false;
|
|
|
|
// iterate through plugin data chunks
|
|
while(s_currentFile.GetRemain() >= sizeof(PluginHeader))
|
|
{
|
|
s_currentFile.ReadBuf(&s_pluginHeader, sizeof(s_pluginHeader));
|
|
|
|
UInt64 pluginChunkStart = s_currentFile.GetOffset();
|
|
|
|
UInt32 pluginIdx = kPluginHandle_Invalid;
|
|
|
|
for(PluginCallbackList::iterator iter = s_pluginCallbacks.begin(); iter != s_pluginCallbacks.end(); ++iter)
|
|
if(iter->hadUID && (iter->uid == s_pluginHeader.signature))
|
|
pluginIdx = iter - s_pluginCallbacks.begin();
|
|
|
|
try
|
|
{
|
|
if(pluginIdx != kPluginHandle_Invalid)
|
|
{
|
|
PluginCallbacks * info = &s_pluginCallbacks[pluginIdx];
|
|
|
|
info->hadData = true;
|
|
|
|
if(info->load)
|
|
{
|
|
s_chunkOpen = false;
|
|
info->load(&g_SKSESerializationInterface);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_WARNING("HandleLoadGame: plugin with signature %08X not loaded", s_pluginHeader.signature);
|
|
}
|
|
}
|
|
catch( ... )
|
|
{
|
|
_ERROR("HandleLoadGame: exception occurred loading %08X", s_pluginHeader.signature);
|
|
}
|
|
|
|
// if plugin failed to read all its data or threw exception, jump to the next chunk
|
|
UInt64 expectedOffset = pluginChunkStart + s_pluginHeader.length;
|
|
if(s_currentFile.GetOffset() != expectedOffset)
|
|
{
|
|
_WARNING("HandleLoadGame: plugin did not read all of its data (at %016I64X expected %016I64X)", s_currentFile.GetOffset(), expectedOffset);
|
|
s_currentFile.SetOffset(expectedOffset);
|
|
}
|
|
}
|
|
|
|
// call load on plugins that had no data
|
|
for(PluginCallbackList::iterator iter = s_pluginCallbacks.begin(); iter != s_pluginCallbacks.end(); ++iter) {
|
|
if(!iter->hadData && iter->load) {
|
|
iter->load(&g_SKSESerializationInterface);
|
|
}
|
|
}
|
|
}
|
|
catch(...)
|
|
{
|
|
_ERROR("HandleLoadGame: exception during load");
|
|
|
|
// ### this could be handled better, individually catch around each plugin so one plugin can't mess things up for everyone else
|
|
}
|
|
|
|
done:
|
|
s_currentFile.Close();
|
|
}
|
|
|
|
void HandleDeleteSave(std::string saveName)
|
|
{
|
|
std::string savePath = MakeSavePath(saveName, NULL);
|
|
std::string coSavePath = savePath;
|
|
savePath += ".ess";
|
|
coSavePath += ".skse";
|
|
|
|
// Old save file really gone?
|
|
IFileStream saveFile;
|
|
if (!saveFile.Open(savePath.c_str()))
|
|
{
|
|
_MESSAGE("deleting co-save %s", coSavePath.c_str());
|
|
DeleteFile(coSavePath.c_str());
|
|
}
|
|
else
|
|
{
|
|
_MESSAGE("skipped delete of co-save %s", coSavePath.c_str());
|
|
}
|
|
}
|
|
|
|
void HandleDeletedForm(UInt64 handle)
|
|
{
|
|
for(UInt32 i = 0; i < s_pluginCallbacks.size(); i++)
|
|
if(s_pluginCallbacks[i].formDelete)
|
|
s_pluginCallbacks[i].formDelete(handle);
|
|
}
|
|
|
|
template <>
|
|
bool WriteData<BSFixedString>(SKSESerializationInterface * intfc, const BSFixedString * str)
|
|
{
|
|
return WriteData<const char>(intfc, str->data);
|
|
}
|
|
|
|
template <>
|
|
bool ReadData<BSFixedString>(SKSESerializationInterface * intfc, BSFixedString * str)
|
|
{
|
|
char buf[257] = { 0 };
|
|
UInt16 len = 0;
|
|
|
|
if (! intfc->ReadRecordData(&len, sizeof(len)))
|
|
return false;
|
|
|
|
if (len > 256)
|
|
return false;
|
|
|
|
if (! intfc->ReadRecordData(buf, len))
|
|
return false;
|
|
|
|
*str = BSFixedString(buf);
|
|
return true;
|
|
}
|
|
|
|
template <>
|
|
bool WriteData<std::string>(SKSESerializationInterface * intfc, const std::string * str)
|
|
{
|
|
UInt16 len = str->length();
|
|
if (len > 256)
|
|
return false;
|
|
|
|
if (! intfc->WriteRecordData(&len, sizeof(len)))
|
|
return false;
|
|
if (! intfc->WriteRecordData(str->data(), len))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
template <>
|
|
bool ReadData<std::string>(SKSESerializationInterface * intfc, std::string * str)
|
|
{
|
|
char buf[257] = { 0 };
|
|
UInt16 len = 0;
|
|
|
|
if (! intfc->ReadRecordData(&len, sizeof(len)))
|
|
return false;
|
|
|
|
if (len > 256)
|
|
return false;
|
|
|
|
if (! intfc->ReadRecordData(buf, len))
|
|
return false;
|
|
|
|
*str = std::string(buf);
|
|
return true;
|
|
}
|
|
|
|
template <>
|
|
bool WriteData<const char>(SKSESerializationInterface * intfc, const char* str)
|
|
{
|
|
UInt16 len = strlen(str);
|
|
if (len > 256)
|
|
return false;
|
|
|
|
if (! intfc->WriteRecordData(&len, sizeof(len)))
|
|
return false;
|
|
if (! intfc->WriteRecordData(str, len))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
template <>
|
|
bool WriteData<GFxValue>(SKSESerializationInterface* intfc, const GFxValue* val)
|
|
{
|
|
UInt32 type = val->GetType();
|
|
if (! WriteData(intfc, &type))
|
|
return false;
|
|
|
|
switch (type)
|
|
{
|
|
case GFxValue::kType_Bool:
|
|
{
|
|
bool t = val->GetBool();
|
|
return WriteData(intfc, &t);
|
|
}
|
|
case GFxValue::kType_Number:
|
|
{
|
|
double t = val->GetNumber();
|
|
return WriteData(intfc, &t);
|
|
}
|
|
case GFxValue::kType_String:
|
|
{
|
|
const char* t = val->GetString();
|
|
return WriteData(intfc, &t);
|
|
}
|
|
default:
|
|
// Unsupported
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
template <>
|
|
bool ReadData<GFxValue>(SKSESerializationInterface* intfc, GFxValue* val)
|
|
{
|
|
UInt32 type;
|
|
if (! ReadData(intfc, &type))
|
|
return false;
|
|
|
|
switch (type)
|
|
{
|
|
case GFxValue::kType_Bool:
|
|
{
|
|
bool t;
|
|
if (! ReadData(intfc, &t))
|
|
return false;
|
|
val->SetBool(t);
|
|
return true;
|
|
}
|
|
case GFxValue::kType_Number:
|
|
{
|
|
double t;
|
|
if (! ReadData(intfc, &t))
|
|
return false;
|
|
val->SetNumber(t);
|
|
return true;
|
|
}
|
|
case GFxValue::kType_String:
|
|
{
|
|
// As usual, using string cache to manage strings
|
|
BSFixedString t;
|
|
if (! ReadData(intfc, &t))
|
|
return false;
|
|
val->SetString(t.data);
|
|
return true;
|
|
}
|
|
default:
|
|
// Unsupported
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SKSESerializationInterface g_SKSESerializationInterface =
|
|
{
|
|
SKSESerializationInterface::kVersion,
|
|
|
|
Serialization::SetUniqueID,
|
|
|
|
Serialization::SetRevertCallback,
|
|
Serialization::SetSaveCallback,
|
|
Serialization::SetLoadCallback,
|
|
Serialization::SetFormDeleteCallback,
|
|
|
|
Serialization::WriteRecord,
|
|
Serialization::OpenRecord,
|
|
Serialization::WriteRecordData,
|
|
|
|
Serialization::GetNextRecordInfo,
|
|
Serialization::ReadRecordData,
|
|
Serialization::ResolveHandle,
|
|
Serialization::ResolveFormId
|
|
};
|