# Partial support for saved games with AI.

Support cancelling loads while inside a loader callback.
Fix use of ArchiveReader/Writer since their API changed.
Improve error-detection in deserializer to avoid crashes.
Report deserializer errors to users.
Expand load-error message box to fit message about invalid saved games.

This was SVN commit r10787.
This commit is contained in:
Ykkrosh 2011-12-22 14:04:32 +00:00
parent 5f0d5e4137
commit 6399ec0cd2
14 changed files with 202 additions and 117 deletions

View file

@ -16,7 +16,7 @@ function cancelOnError(msg)
if (msg)
{
Engine.PushGuiPage("page_msgbox.xml", {
width: 400,
width: 500,
height: 200,
message: '[font="serif-bold-18"]' + msg + '[/font]',
title: "Loading Aborted",

View file

@ -3,16 +3,41 @@ function BaseAI(settings)
if (!settings)
return;
// Make some properties non-enumerable, so they won't be serialised
Object.defineProperty(this, "_player", {value: settings.player, enumerable: false});
Object.defineProperty(this, "_templates", {value: settings.templates, enumerable: false});
Object.defineProperty(this, "_derivedTemplates", {value: {}, enumerable: false});
// Copies of static engine data (not serialized)
this._player = settings.player;
this._templates = settings.templates;
this._derivedTemplates = {};
// Representation of the current world state (requires serialization)
this._rawEntities = null;
this._ownEntities = {};
this._entityMetadata = {};
}
// Return a simple object (using no classes etc) that will be serialized
// into saved games
BaseAI.prototype.Serialize = function()
{
return {
_rawEntities: this._rawEntities,
_ownEntities: this._ownEntities,
_entityMetadata: this._entityMetadata,
};
// TODO: ought to get the AI script subclass to serialize its own state
};
// Called after the constructor when loading a saved game, with 'data' being
// whatever Serialize() returned
BaseAI.prototype.Deserialize = function(data)
{
this._rawEntities = data._rawEntities;
this._ownEntities = data._ownEntities;
this._entityMetadata = data._entityMetadata;
// TODO: ought to get the AI script subclass to deserialize its own state
};
// Components that will be disabled in foundation entity templates.
// (This is a bit yucky and fragile since it's the inverse of
// CCmpTemplateManager::CopyFoundationSubset and only includes components
@ -160,7 +185,8 @@ BaseAI.prototype.ApplyEntitiesDelta = function(state)
};
BaseAI.prototype.OnUpdate = function()
{ // AIs override this function
{
// AIs override this function
};
BaseAI.prototype.chat = function(message)

View file

@ -2,6 +2,8 @@ function InitGame(settings)
{
// This will be called after the map settings have been loaded,
// before the simulation has started.
// This is only called at the start of a new game, not when loading
// a saved game.
// No settings when loading a map in Atlas, so do nothing
if (!settings)

View file

@ -36,6 +36,7 @@
#include "ps/Profile.h"
#include "ps/Replay.h"
#include "ps/World.h"
#include "ps/GameSetup/GameSetup.h"
#include "renderer/Renderer.h"
#include "scripting/ScriptingHost.h"
#include "scriptinterface/ScriptInterface.h"
@ -63,7 +64,8 @@ CGame::CGame(bool disableGraphics):
m_GameStarted(false),
m_Paused(false),
m_SimRate(1.0f),
m_PlayerID(-1)
m_PlayerID(-1),
m_IsSavedGame(false)
{
m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface());
// TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps?
@ -115,6 +117,7 @@ void CGame::SetTurnManager(CNetTurnManager* turnManager)
void CGame::RegisterInit(const CScriptValRooted& attribs, const std::string& savedState)
{
m_InitialSavedState = savedState;
m_IsSavedGame = !savedState.empty();
m_Simulation2->SetInitAttributes(attribs);
@ -153,7 +156,7 @@ void CGame::RegisterInit(const CScriptValRooted& attribs, const std::string& sav
m_World->RegisterInitRMS(scriptFile, settings, m_PlayerID);
}
if (!m_InitialSavedState.empty())
if (m_IsSavedGame)
RegMemFun(this, &CGame::LoadInitialState, L"Loading game", 1000);
LDR_EndRegistering();
@ -161,6 +164,7 @@ void CGame::RegisterInit(const CScriptValRooted& attribs, const std::string& sav
int CGame::LoadInitialState()
{
ENSURE(m_IsSavedGame);
ENSURE(!m_InitialSavedState.empty());
std::string state;
@ -169,7 +173,11 @@ int CGame::LoadInitialState()
std::stringstream stream(state);
bool ok = m_Simulation2->DeserializeState(stream);
ENSURE(ok);
if (!ok)
{
CancelLoad(L"Failed to load saved game state. It might have been\nsaved with an incompatible version of the game.");
return 0;
}
return 0;
}
@ -181,9 +189,13 @@ int CGame::LoadInitialState()
**/
PSRETURN CGame::ReallyStartGame()
{
CScriptVal settings;
m_Simulation2->GetScriptInterface().GetProperty(m_Simulation2->GetInitAttributes().get(), "settings", settings);
m_Simulation2->InitGame(settings);
// Call the script function InitGame only for new games, not saved games
if (!m_IsSavedGame)
{
CScriptVal settings;
m_Simulation2->GetScriptInterface().GetProperty(m_Simulation2->GetInitAttributes().get(), "settings", settings);
m_Simulation2->InitGame(settings);
}
// Call the reallyStartGame GUI function, but only if it exists
if (g_GUI && g_GUI->HasPages())

View file

@ -161,6 +161,7 @@ private:
int LoadInitialState();
std::string m_InitialSavedState; // valid between RegisterInit and LoadInitialState
bool m_IsSavedGame; // true if loading a saved game; false for a new game
};
extern CGame *g_Game;

View file

@ -202,7 +202,7 @@ Status LDR_ProgressiveLoad(double time_budget, wchar_t* description, size_t max_
{
state = LOADING;
ret = ERR::TIMED_OUT; // make caller think we did something
ret = ERR::TIMED_OUT; // make caller think we did something
// progress already set to 0.0; that'll be passed back.
goto done;
}
@ -219,7 +219,7 @@ Status LDR_ProgressiveLoad(double time_budget, wchar_t* description, size_t max_
const double estimated_duration = lr.estimated_duration_ms*1e-3;
if(!HaveTimeForNextTask(time_left, time_budget, lr.estimated_duration_ms))
{
ret = ERR::TIMED_OUT;
ret = ERR::TIMED_OUT;
goto done;
}
@ -258,7 +258,7 @@ Status LDR_ProgressiveLoad(double time_budget, wchar_t* description, size_t max_
// .. function interrupted itself, i.e. timed out; abort.
if(timed_out)
{
ret = ERR::TIMED_OUT;
ret = ERR::TIMED_OUT;
goto done;
}
// .. failed; abort. loading will continue when we're called in
@ -270,6 +270,13 @@ Status LDR_ProgressiveLoad(double time_budget, wchar_t* description, size_t max_
ret = (Status)status;
goto done;
}
// .. function called LDR_Cancel; abort. return OK since this is an
// intentional cancellation, not an error.
else if(state != LOADING)
{
ret = INFO::OK;
goto done;
}
// .. succeeded; continue and process next queued task.
}

View file

@ -75,15 +75,9 @@ Status SavedGames::Save(const std::wstring& prefix, CSimulation2& simulation, CG
std::string metadataString = simulation.GetScriptInterface().StringifyJSON(metadata.get(), true);
// Write the saved game as zip file containing the various components
PIArchiveWriter archiveWriter;
try
{
archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false);
}
catch (Status err)
{
WARN_RETURN(err);
}
PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false);
if (!archiveWriter)
WARN_RETURN(ERR::FAIL);
WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json"));
WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat"));
@ -147,15 +141,9 @@ Status SavedGames::Load(const std::wstring& name, ScriptInterface& scriptInterfa
OsPath realPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
PIArchiveReader archiveReader;
try
{
archiveReader = CreateArchiveReader_Zip(realPath);
}
catch (Status err)
{
WARN_RETURN(err);
}
PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
if (!archiveReader)
WARN_RETURN(ERR::FAIL);
CGameLoader loader(scriptInterface, &metadata, &savedState);
WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader));
@ -185,14 +173,11 @@ std::vector<CScriptValRooted> SavedGames::GetSavedGames(ScriptInterface& scriptI
continue; // skip this file
}
PIArchiveReader archiveReader;
try
PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
if (!archiveReader)
{
archiveReader = CreateArchiveReader_Zip(realPath);
}
catch (Status err)
{
DEBUG_WARN_ERR(err);
// Triggered by e.g. the file being open in another program
LOGWARNING(L"Failed to read saved game '%ls'", realPath.string().c_str());
continue; // skip this file
}

View file

@ -638,8 +638,7 @@ ScriptInterface& CSimulation2::GetScriptInterface() const
void CSimulation2::InitGame(const CScriptVal& data)
{
CScriptVal ret; // ignored
GetScriptInterface().CallFunction(GetScriptInterface().GetGlobalObject(), "InitGame", data, ret);
GetScriptInterface().CallFunctionVoid(GetScriptInterface().GetGlobalObject(), "InitGame", data);
}
void CSimulation2::Update(int turnLength)

View file

@ -210,6 +210,7 @@ private:
CScriptVal settings;
m_ScriptInterface.Eval(L"({})", settings);
m_ScriptInterface.SetProperty(settings.get(), "player", m_Player, false);
ENSURE(m_Worker.m_HasLoadedEntityTemplates);
m_ScriptInterface.SetProperty(settings.get(), "templates", m_Worker.m_EntityTemplates, false);
obj = m_ScriptInterface.CallConstructor(ctor.get(), settings.get());
@ -218,6 +219,7 @@ private:
{
// For deserialization, we want to create the object with the correct prototype
// but don't want to actually run the constructor again
// XXX: actually we don't currently use this path for deserialization - maybe delete it?
obj = m_ScriptInterface.NewObjectFromConstructor(ctor.get());
}
@ -258,7 +260,8 @@ public:
m_ScriptRuntime(ScriptInterface::CreateRuntime()),
m_ScriptInterface("Engine", "AI", m_ScriptRuntime),
m_TurnNum(0),
m_CommandsComputed(true)
m_CommandsComputed(true),
m_HasLoadedEntityTemplates(false)
{
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
@ -337,6 +340,8 @@ public:
void LoadEntityTemplates(const std::vector<std::pair<std::string, const CParamNode*> >& templates)
{
m_HasLoadedEntityTemplates = true;
m_ScriptInterface.Eval("({})", m_EntityTemplates);
for (size_t i = 0; i < templates.size(); ++i)
@ -369,13 +374,18 @@ public:
void SerializeState(ISerializer& serializer)
{
std::stringstream rngStream;
rngStream << m_RNG;
serializer.StringASCII("rng", rngStream.str(), 0, 32);
serializer.NumberU32_Unbounded("turn", m_TurnNum);
serializer.NumberU32_Unbounded("num ais", (u32)m_Players.size());
for (size_t i = 0; i < m_Players.size(); ++i)
{
serializer.String("name", m_Players[i]->m_AIName, 0, 256);
serializer.String("name", m_Players[i]->m_AIName, 1, 256);
serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player);
serializer.ScriptVal("data", m_Players[i]->m_Obj);
serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size());
for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j)
@ -383,6 +393,11 @@ public:
CScriptVal val = m_ScriptInterface.ReadStructuredClone(m_Players[i]->m_Commands[j]);
serializer.ScriptVal("command", val);
}
CScriptVal scriptData;
if (!m_ScriptInterface.CallFunction(m_Players[i]->m_Obj.get(), "Serialize", scriptData))
LOGERROR(L"AI script Serialize call failed");
serializer.ScriptVal("data", scriptData);
}
}
@ -395,6 +410,14 @@ public:
m_PlayerMetadata.clear();
m_Players.clear();
std::string rngString;
std::stringstream rngStream;
deserializer.StringASCII("rng", rngString, 0, 32);
rngStream << rngString;
rngStream >> m_RNG;
deserializer.NumberU32_Unbounded("turn", m_TurnNum);
uint32_t numAis;
deserializer.NumberU32_Unbounded("num ais", numAis);
@ -402,15 +425,11 @@ public:
{
std::wstring name;
player_id_t player;
deserializer.String("name", name, 0, 256);
deserializer.String("name", name, 1, 256);
deserializer.NumberI32_Unbounded("player", player);
if (!AddPlayer(name, player, false))
if (!AddPlayer(name, player, true))
throw PSERROR_Deserialize_ScriptError();
// Use ScriptObjectAppend so we don't lose the carefully-constructed
// prototype/parent of this object
deserializer.ScriptObjectAppend("data", m_Players.back()->m_Obj.getRef());
uint32_t numCommands;
deserializer.NumberU32_Unbounded("num commands", numCommands);
m_Players.back()->m_Commands.reserve(numCommands);
@ -420,6 +439,11 @@ public:
deserializer.ScriptVal("command", val);
m_Players.back()->m_Commands.push_back(m_ScriptInterface.WriteStructuredClone(val.get()));
}
CScriptVal scriptData;
deserializer.ScriptVal("data", scriptData);
if (!m_ScriptInterface.CallFunctionVoid(m_Players.back()->m_Obj.get(), "Deserialize", scriptData))
LOGERROR(L"AI script Deserialize call failed");
}
}
@ -473,9 +497,11 @@ private:
shared_ptr<ScriptRuntime> m_ScriptRuntime;
ScriptInterface m_ScriptInterface;
boost::rand48 m_RNG;
size_t m_TurnNum;
u32 m_TurnNum;
CScriptValRooted m_EntityTemplates;
bool m_HasLoadedEntityTemplates;
std::map<VfsPath, CScriptValRooted> m_PlayerMetadata;
std::vector<shared_ptr<CAIPlayer> > m_Players; // use shared_ptr just to avoid copying
@ -523,17 +549,16 @@ public:
// directly. So we'll just grab the ISerializer's stream and write to it
// with an independent serializer.
// TODO: make the serialization/deserialization actually work, and not really slowly
// m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug());
UNUSED2(serialize);
m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug());
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
// m_Worker.Deserialize(deserialize.GetStream());
UNUSED2(deserialize);
ForceLoadEntityTemplates();
m_Worker.Deserialize(deserialize.GetStream());
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))

View file

@ -163,7 +163,9 @@ void IDeserializer::StringASCII(const char* name, std::string& out, uint32_t min
{
uint32_t len;
NumberU32("string length", len, minlength, maxlength);
out.resize(len); // TODO: should check len <= bytes remaining in stream
RequireBytesInStream(len);
out.resize(len);
Get(name, (u8*)out.data(), len);
for (size_t i = 0; i < out.length(); ++i)
@ -176,7 +178,9 @@ void IDeserializer::String(const char* name, std::wstring& out, uint32_t minleng
std::string str;
uint32_t len;
NumberU32_Unbounded("string length", len);
str.resize(len); // TODO: should check len <= bytes remaining in stream
RequireBytesInStream(len);
str.resize(len);
Get(name, (u8*)str.data(), len);
Status err;

View file

@ -78,6 +78,14 @@ public:
*/
virtual std::istream& GetStream() = 0;
/**
* Throws an exception if the stream cannot provide the required number of
* bytes. (This should be used when allocating memory based on data in the
* stream, e.g. reading strings, to avoid dangerously large allocations
* when the data is invalid.)
*/
virtual void RequireBytesInStream(size_t numBytes) = 0;
protected:
virtual void Get(const char* name, u8* data, size_t len) = 0;
};

View file

@ -67,6 +67,12 @@ std::istream& CStdDeserializer::GetStream()
return m_Stream;
}
void CStdDeserializer::RequireBytesInStream(size_t numBytes)
{
if (numBytes >= m_Stream.rdbuf()->in_avail())
throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream");
}
void CStdDeserializer::AddScriptBackref(JSObject* obj)
{
std::pair<std::map<u32, JSObject*>::iterator, bool> it = m_ScriptBackrefs.insert(std::make_pair((u32)m_ScriptBackrefs.size()+1, obj));
@ -197,7 +203,8 @@ void CStdDeserializer::ReadStringUTF16(const char* name, utf16string& str)
{
uint32_t len;
NumberU32_Unbounded("string length", len);
str.resize(len); // TODO: should check len*2 <= bytes remaining in stream, before resizing
RequireBytesInStream(len*2);
str.resize(len);
Get(name, (u8*)str.data(), len*2);
}

View file

@ -38,7 +38,8 @@ public:
virtual void ScriptString(const char* name, JSString*& out);
virtual std::istream& GetStream();
virtual void RequireBytesInStream(size_t numBytes);
protected:
virtual void Get(const char* name, u8* data, size_t len);

View file

@ -261,69 +261,77 @@ bool CComponentManager::SerializeState(std::ostream& stream)
bool CComponentManager::DeserializeState(std::istream& stream)
{
CStdDeserializer deserializer(m_ScriptInterface, stream);
ResetState();
std::string rng;
deserializer.StringASCII("rng", rng, 0, 32);
DeserializeRNG(rng, m_RNG);
deserializer.NumberU32_Unbounded("next entity id", m_NextEntityId); // TODO: use sensible bounds
uint32_t numComponentTypes;
deserializer.NumberU32_Unbounded("num component types", numComponentTypes);
ICmpTemplateManager* templateManager = NULL;
CParamNode noParam;
for (size_t i = 0; i < numComponentTypes; ++i)
try
{
std::string ctname;
deserializer.StringASCII("name", ctname, 0, 255);
ComponentTypeId ctid = LookupCID(ctname);
if (ctid == CID__Invalid)
CStdDeserializer deserializer(m_ScriptInterface, stream);
ResetState();
std::string rng;
deserializer.StringASCII("rng", rng, 0, 32);
DeserializeRNG(rng, m_RNG);
deserializer.NumberU32_Unbounded("next entity id", m_NextEntityId); // TODO: use sensible bounds
uint32_t numComponentTypes;
deserializer.NumberU32_Unbounded("num component types", numComponentTypes);
ICmpTemplateManager* templateManager = NULL;
CParamNode noParam;
for (size_t i = 0; i < numComponentTypes; ++i)
{
LOGERROR(L"Deserialization saw unrecognised component type '%hs'", ctname.c_str());
std::string ctname;
deserializer.StringASCII("name", ctname, 0, 255);
ComponentTypeId ctid = LookupCID(ctname);
if (ctid == CID__Invalid)
{
LOGERROR(L"Deserialization saw unrecognised component type '%hs'", ctname.c_str());
return false;
}
uint32_t numComponents;
deserializer.NumberU32_Unbounded("num components", numComponents);
for (size_t j = 0; j < numComponents; ++j)
{
entity_id_t ent;
deserializer.NumberU32_Unbounded("entity id", ent);
IComponent* component = ConstructComponent(ent, ctid);
if (!component)
return false;
// Try to find the template for this entity
const CParamNode* entTemplate = NULL;
if (templateManager && ent != SYSTEM_ENTITY) // (system entities don't use templates)
entTemplate = templateManager->LoadLatestTemplate(ent);
// Deserialize, with the appropriate template for this component
if (entTemplate)
component->Deserialize(entTemplate->GetChild(ctname.c_str()), deserializer);
else
component->Deserialize(noParam, deserializer);
// If this was the template manager, remember it so we can use it when
// deserializing any further non-system entities
if (ent == SYSTEM_ENTITY && ctid == CID_TemplateManager)
templateManager = static_cast<ICmpTemplateManager*> (component);
}
}
if (stream.peek() != EOF)
{
LOGERROR(L"Deserialization didn't reach EOF");
return false;
}
uint32_t numComponents;
deserializer.NumberU32_Unbounded("num components", numComponents);
for (size_t j = 0; j < numComponents; ++j)
{
entity_id_t ent;
deserializer.NumberU32_Unbounded("entity id", ent);
IComponent* component = ConstructComponent(ent, ctid);
if (!component)
return false;
// Try to find the template for this entity
const CParamNode* entTemplate = NULL;
if (templateManager && ent != SYSTEM_ENTITY) // (system entities don't use templates)
entTemplate = templateManager->LoadLatestTemplate(ent);
// Deserialize, with the appropriate template for this component
if (entTemplate)
component->Deserialize(entTemplate->GetChild(ctname.c_str()), deserializer);
else
component->Deserialize(noParam, deserializer);
// If this was the template manager, remember it so we can use it when
// deserializing any further non-system entities
if (ent == SYSTEM_ENTITY && ctid == CID_TemplateManager)
templateManager = static_cast<ICmpTemplateManager*> (component);
}
return true;
}
if (stream.peek() != EOF)
catch (PSERROR_Deserialize& e)
{
LOGERROR(L"Deserialization didn't reach EOF");
LOGERROR(L"Deserialization failed: %hs", e.what());
return false;
}
// TODO: catch exceptions
return true;
}