1
0
forked from 0ad/0ad

# Allow saving and loading single-player games.

Support writing zip files based on in-memory data.
Fix menu sliding animations to be framerate independent.

This was SVN commit r10454.
This commit is contained in:
Ykkrosh 2011-10-30 00:07:28 +00:00
parent 21c6141cdc
commit 7064565ff6
27 changed files with 601 additions and 48 deletions

View File

@ -97,7 +97,7 @@
</object>
</object>
<object type="button" style="StoneButton" size="50%+16 100%-60 50%+144 100%-32">
<object type="button" style="StoneButton" size="50%+16 100%-60 50%+144 100%-32">
Cancel
<action on="Press"><![CDATA[cancelSetup();]]></action>
</object>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>common/setup.xml</include>
<include>common/styles.xml</include>
<include>common/sprite1.xml</include>
<include>common/common_sprites.xml</include>
<include>common/common_styles.xml</include>
<include>savedgames/load.xml</include>
</page>

View File

@ -83,13 +83,18 @@ function formatUserReportStatus(status)
return "unknown";
}
var lastTickTime = new Date;
function onTick()
{
var now = new Date;
var tickLength = new Date - lastTickTime;
lastTickTime = now;
// Animate backgrounds
scrollBackgrounds();
// Animate submenu
updateMenuPosition();
updateMenuPosition(tickLength);
// Update music state
global.music.updateTimer();
@ -124,24 +129,23 @@ function onTick()
//}
// Slide menu
function updateMenuPosition()
function updateMenuPosition(dt)
{
var submenu = getGUIObjectByName("submenu");
if (submenu.hidden == false)
{
// The offset is the increment or number of units/pixels to move
// the menu. An offset of one is always accurate, but it is too
// slow. The offset must divide into the travel distance evenly
// in order for the menu to end up at the right spot. The travel
// distance is the max-initial. The travel distance in this
// example is 300-60 = 240. We choose an offset of 5 because it
// divides into 240 evenly and provided the speed we wanted.
const OFFSET = 10;
// Number of pixels per millisecond to move
const SPEED = 1.2;
if (submenu.size.left < getGUIObjectByName("mainMenu").size.right)
var maxOffset = getGUIObjectByName("mainMenu").size.right - submenu.size.left;
if (maxOffset > 0)
{
submenu.size = (submenu.size.left + OFFSET) + " " + submenu.size.top + " " + (submenu.size.right + OFFSET) + " " + submenu.size.bottom;
var offset = Math.min(SPEED * dt, maxOffset);
var size = submenu.size;
size.left += offset;
size.right += offset;
submenu.size = size;
}
}
}

View File

@ -189,6 +189,21 @@ Status: $status.
]]>
</action>
</object>
<object name="subMenuLoadButton"
type="button"
style="StoneButtonFancy"
size="0 64 100% 92"
tooltip_style="pgToolTip"
tooltip="Click here to load a saved game."
>
Load Game
<action on="Press">
closeMenu();
Engine.PushGuiPage("page_loadgame.xml", { type: "offline" });
</action>
</object>
</object>
<!-- submenuMultiplayer -->
@ -344,7 +359,7 @@ Status: $status.
Single Player
<action on="Press">
closeMenu();
openMenu("submenuSinglePlayer", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 2);
openMenu("submenuSinglePlayer", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 3);
</action>
</object>

View File

@ -0,0 +1,47 @@
function sortDecreasingDate(a, b)
{
return b.metadata.time - a.metadata.time;
}
function twoDigits(n)
{
return n < 10 ? "0" + n : n;
}
function generateLabel(metadata)
{
var t = new Date(metadata.time*1000);
// TODO: timezones
var date = t.getUTCFullYear()+"-"+twoDigits(1+t.getUTCMonth())+"-"+twoDigits(t.getUTCDate());
var time = twoDigits(t.getUTCHours())+":"+twoDigits(t.getUTCMinutes())+":"+twoDigits(t.getUTCSeconds());
return "["+date+" "+time+"] "+metadata.initAttributes.map;
}
function init()
{
var savedGames = Engine.GetSavedGames();
savedGames.sort(sortDecreasingDate);
var gameListIDs = [ game.id for each (game in savedGames) ];
var gameListLabels = [ generateLabel(game.metadata) for each (game in savedGames) ];
var gameSelection = getGUIObjectByName("gameSelection");
gameSelection.list = gameListLabels;
gameSelection.list_data = gameListIDs;
gameSelection.selected = 0;
}
function loadGame()
{
var gameSelection = getGUIObjectByName("gameSelection");
var gameID = gameSelection.list_data[gameSelection.selected];
var metadata = Engine.StartSavedGame(gameID);
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": metadata.initAttributes,
"isNetworked" : false,
"playerAssignments": metadata.gui.playerAssignments
});
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script file="gui/savedgames/load.js"/>
<!-- Add a translucent black background to fade out the menu page -->
<object type="image" z="0" sprite="BackgroundTranslucent"/>
<object type="image" style="StoneDialog" size="50%-190 50%-200 50%+190 50%+200">
<object type="text" style="TitleText" size="50%-128 0%-16 50%+128 16">
Load Game
</object>
<object name="gameSelection"
style="StoneList"
type="list"
size="24 24 100%-24 100%-100">
</object>
<object type="button" size="50%-144 100%-60 50%-16 100%-32" style="StoneButton">
Load Game
<action on="Press">loadGame();</action>
</object>
<object type="button" style="StoneButton" size="50%+16 100%-60 50%+144 100%-32">
Cancel
<action on="Press">Engine.PopGuiPage();</action>
</object>
</object>
</objects>

View File

@ -9,7 +9,7 @@ const RESUME = "Resume";
const MARGIN = 4;
// Includes the main menu button
const NUM_BUTTONS = 5;
const NUM_BUTTONS = 6;
// Regular menu buttons
const BUTTON_HEIGHT = 32;
@ -26,14 +26,8 @@ const MENU_TOP = MENU_BOTTOM - END_MENU_POSITION;
// Menu starting position: overall
const INITIAL_MENU_POSITION = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM;
// The offset is the increment or number of units/pixels to move
// the menu. An offset of one is always accurate, but it is too
// slow. The offset must divide into the travel distance evenly
// in order for the menu to end up at the right spot. The travel
// distance is the max-initial. The travel distance in this
// example is 164-0 = 164. We choose an offset of 10.25 because it
// divides into 164 evenly and provided the speed we wanted.
const OFFSET = 20.5;
// Number of pixels per millisecond to move
const MENU_SPEED = 1.2;
var isMenuOpen = false;
var menu;
@ -51,20 +45,30 @@ function initMenuPosition()
// =============================================================================
//
// Slide menu
function updateMenuPosition()
function updateMenuPosition(dt)
{
if (isMenuOpen)
{
if (menu.size.bottom < END_MENU_POSITION)
var maxOffset = END_MENU_POSITION - menu.size.bottom;
if (maxOffset > 0)
{
menu.size = "100%-164 " + (menu.size.top + OFFSET) + " 100% " + (menu.size.bottom + OFFSET);
var offset = Math.min(MENU_SPEED * dt, maxOffset);
var size = menu.size;
size.top += offset;
size.bottom += offset;
menu.size = size;
}
}
else
{
if (menu.size.top > MENU_TOP)
var maxOffset = menu.size.top - MENU_TOP;
if (maxOffset > 0)
{
menu.size = "100%-164 " + (menu.size.top - OFFSET) + " 100% " + (menu.size.bottom - OFFSET);
var offset = Math.min(MENU_SPEED * dt, maxOffset);
var size = menu.size;
size.top -= offset;
size.bottom -= offset;
menu.size = size;
}
}
}

View File

@ -159,9 +159,22 @@ function getHotloadData()
return { selection: g_Selection.selected };
}
// Return some data that will be stored in saved game files
function getSavedGameData()
{
var data = {};
data.playerAssignments = g_PlayerAssignments;
// TODO: control groups, etc
return data;
}
var lastTickTime = new Date;
function onTick()
{
var now = new Date;
var tickLength = new Date - lastTickTime;
lastTickTime = now;
checkPlayerState();
while (true)
@ -187,7 +200,7 @@ function onTick()
updateTimers();
// Animate menu
updateMenuPosition();
updateMenuPosition(tickLength);
// Update music state
global.music.updateTimer();

View File

@ -371,7 +371,6 @@
<object name="menu"
style="StonePanelThinBorder"
type="image"
size="100%-164 -160 100% 36"
hidden="false"
>
<object size="4 36 100%-4 50%+20">
@ -419,6 +418,21 @@
<object name="pauseButtonText" type="text" style="CenteredButtonText" ghost="true">Pause</object>
<action on="Press">togglePause();</action>
</object>
<!-- Save game button -->
<object type="button"
name="saveGameButton"
style="StoneButtonFancy"
size="0 128 100% 154"
tooltip_style="sessionToolTip"
>
Save Game
<action on="Press">
Engine.SaveGame();
closeMenu();
</action>
</object>
</object>
</object>

View File

@ -178,6 +178,12 @@ Status CGUIManager::ReloadChangedFiles(const VfsPath& path)
return INFO::OK;
}
CScriptVal CGUIManager::GetSavedGameData()
{
CScriptVal data;
m_ScriptInterface.CallFunction(OBJECT_TO_JSVAL(top()->GetScriptObject()), "getSavedGameData", data);
return data;
}
InReaction CGUIManager::HandleEvent(const SDL_Event_* ev)
{

View File

@ -134,6 +134,11 @@ public:
*/
void UpdateResolution();
/**
* Calls the current page's script function getSavedGameData() and returns the result.
*/
CScriptVal GetSavedGameData();
private:
struct SGUIPage
{

View File

@ -38,6 +38,7 @@
#include "ps/Overlay.h"
#include "ps/ProfileViewer.h"
#include "ps/Pyrogenesis.h"
#include "ps/SavedGame.h"
#include "ps/UserReport.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Config.h"
@ -189,7 +190,52 @@ void StartGame(void* cbdata, CScriptVal attribs, int playerID)
sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), attribs.get()));
g_Game->SetPlayerID(playerID);
g_Game->StartGame(gameAttribs);
g_Game->StartGame(gameAttribs, "");
}
CScriptVal StartSavedGame(void* cbdata, std::wstring name)
{
CGUIManager* guiManager = static_cast<CGUIManager*> (cbdata);
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
// Load the saved game data from disk
CScriptValRooted metadata;
std::string savedState;
Status err = SavedGames::Load(name, guiManager->GetScriptInterface(), metadata, savedState);
WARN_IF_ERR(err);
if (err < 0)
return CScriptVal();
g_Game = new CGame();
// Convert from GUI script context to sim script context
CSimulation2* sim = g_Game->GetSimulation2();
CScriptValRooted gameMetadata (sim->GetScriptInterface().GetContext(),
sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), metadata.get()));
CScriptValRooted gameInitAttributes;
sim->GetScriptInterface().GetProperty(gameMetadata.get(), "initAttributes", gameInitAttributes);
int playerID;
sim->GetScriptInterface().GetProperty(gameMetadata.get(), "player", playerID);
// Start the game
g_Game->SetPlayerID(playerID);
g_Game->StartGame(gameInitAttributes, savedState);
return metadata.get();
}
void SaveGame(void* cbdata)
{
CGUIManager* guiManager = static_cast<CGUIManager*> (cbdata);
if (SavedGames::Save(L"quicksave", *g_Game->GetSimulation2(), guiManager, g_Game->GetPlayerID()) < 0)
LOGERROR(L"Failed to save game");
}
void SetNetworkGameAttributes(void* cbdata, CScriptVal attribs)
@ -291,6 +337,13 @@ std::vector<CScriptValRooted> GetAIs(void* cbdata)
return ICmpAIManager::GetAIs(guiManager->GetScriptInterface());
}
std::vector<CScriptValRooted> GetSavedGames(void* cbdata)
{
CGUIManager* guiManager = static_cast<CGUIManager*> (cbdata);
return SavedGames::GetSavedGames(guiManager->GetScriptInterface());
}
void OpenURL(void* UNUSED(cbdata), std::string url)
{
sys_open_url(url);
@ -498,6 +551,13 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction<void, std::wstring, &SendNetworkChat>("SendNetworkChat");
scriptInterface.RegisterFunction<std::vector<CScriptValRooted>, &GetAIs>("GetAIs");
// Saved games
scriptInterface.RegisterFunction<CScriptVal, std::wstring, &StartSavedGame>("StartSavedGame");
scriptInterface.RegisterFunction<std::vector<CScriptValRooted>, &GetSavedGames>("GetSavedGames");
scriptInterface.RegisterFunction<void, &SaveGame>("SaveGame");
scriptInterface.RegisterFunction<void, &QuickSave>("QuickSave");
scriptInterface.RegisterFunction<void, &QuickLoad>("QuickLoad");
// Misc functions
scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor");
scriptInterface.RegisterFunction<int, &GetPlayerID>("GetPlayerID");
@ -530,6 +590,4 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction<void, &DumpSimState>("DumpSimState");
scriptInterface.RegisterFunction<void, unsigned int, &EnableTimeWarpRecording>("EnableTimeWarpRecording");
scriptInterface.RegisterFunction<void, &RewindTimeWarp>("RewindTimeWarp");
scriptInterface.RegisterFunction<void, &QuickSave>("QuickSave");
scriptInterface.RegisterFunction<void, &QuickLoad>("QuickLoad");
}

View File

@ -92,6 +92,16 @@ struct IArchiveWriter
* @param pathnameInArchive the name to store in the archive
**/
virtual Status AddFile(const OsPath& pathname, const Path& pathameInArchive) = 0;
/**
* add a file to the archive, when it is already in memory and not on disk.
*
* @param data the uncompressed file contents to add
* @param size the length of data
* @param mtime the last-modified-time to be stored in the archive
* @param pathnameInArchive the name to store in the archive
**/
virtual Status AddMemory(const u8* data, size_t size, time_t mtime, const OsPath& pathnameInArchive) = 0;
};
typedef shared_ptr<IArchiveWriter> PIArchiveWriter;

View File

@ -607,6 +607,24 @@ public:
{
FileInfo fileInfo;
RETURN_STATUS_IF_ERR(GetFileInfo(pathname, &fileInfo));
PFile file(new File);
RETURN_STATUS_IF_ERR(file->Open(pathname, O_RDONLY));
return AddFileOrMemory(fileInfo, pathnameInArchive, file, NULL);
}
Status AddMemory(const u8* data, size_t size, time_t mtime, const OsPath& pathnameInArchive)
{
FileInfo fileInfo(pathnameInArchive, size, mtime);
return AddFileOrMemory(fileInfo, pathnameInArchive, PFile(), data);
}
Status AddFileOrMemory(const FileInfo& fileInfo, const OsPath& pathnameInArchive, const PFile& file, const u8* data)
{
ENSURE((file && !data) || (data && !file));
const off_t usize = fileInfo.Size();
// skip 0-length files.
// rationale: zip.cpp needs to determine whether a CDFH entry is
@ -619,9 +637,6 @@ public:
if(!usize)
return INFO::SKIPPED;
PFile file(new File);
RETURN_STATUS_IF_ERR(file->Open(pathname, O_RDONLY));
const size_t pathnameLength = pathnameInArchive.string().length();
// choose method and the corresponding codec
@ -648,9 +663,16 @@ public:
u8* cdata = (u8*)buf.get() + sizeof(LFH) + pathnameLength;
Stream stream(codec);
stream.SetOutputBuffer(cdata, csizeMax);
io::Operation op(*file.get(), 0, usize);
StreamFeeder streamFeeder(stream);
RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder));
if(file)
{
io::Operation op(*file.get(), 0, usize);
RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder));
}
else
{
RETURN_STATUS_IF_ERR(streamFeeder(data, usize));
}
RETURN_STATUS_IF_ERR(stream.Finish());
csize = stream.OutSize();
checksum = stream.Checksum();

View File

@ -38,7 +38,7 @@ WriteBuffer::WriteBuffer()
}
void WriteBuffer::Append(const void* data, size_t size)
void WriteBuffer::EnsureSufficientCapacity(size_t size)
{
if(m_size + size > m_capacity)
{
@ -48,12 +48,25 @@ void WriteBuffer::Append(const void* data, size_t size)
memcpy(newData.get(), m_data.get(), m_size);
m_data = newData;
}
}
void WriteBuffer::Append(const void* data, size_t size)
{
EnsureSufficientCapacity(size);
memcpy(m_data.get() + m_size, data, size);
m_size += size;
}
void WriteBuffer::Reserve(size_t size)
{
EnsureSufficientCapacity(size);
memset(m_data.get() + m_size, 0, size);
m_size += size;
}
void WriteBuffer::Overwrite(const void* data, size_t size, size_t offset)
{
ENSURE(offset+size < m_size);

View File

@ -31,6 +31,7 @@ public:
WriteBuffer();
void Append(const void* data, size_t size);
void Reserve(size_t size);
void Overwrite(const void* data, size_t size, size_t offset);
shared_ptr<u8> Data() const
@ -44,6 +45,8 @@ public:
}
private:
void EnsureSufficientCapacity(size_t size);
size_t m_capacity; // must come first (init order)
shared_ptr<u8> m_data;

View File

@ -205,6 +205,15 @@ public:
return INFO::OK;
}
virtual Status GetDirectoryRealPath(const VfsPath& pathname, OsPath& realPathname)
{
ScopedLock s;
VfsDirectory* directory;
WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, NULL));
realPathname = directory->AssociatedDirectory()->Path();
return INFO::OK;
}
virtual Status GetVirtualPath(const OsPath& realPathname, VfsPath& pathname)
{
ScopedLock s;

View File

@ -155,6 +155,13 @@ struct IVFS
**/
virtual Status GetRealPath(const VfsPath& pathname, OsPath& realPathname) = 0;
/**
* retrieve the real (POSIX) pathname underlying a VFS directory.
*
* this is useful for passing paths to external libraries.
**/
virtual Status GetDirectoryRealPath(const VfsPath& pathname, OsPath& realPathname) = 0;
/**
* retrieve the VFS pathname that corresponds to a real file.
*

View File

@ -471,7 +471,7 @@ bool CNetClient::OnGameStart(void* context, CFsmEvent* event)
*client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger());
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(client->m_GameAttributes);
client->m_Game->StartGame(client->m_GameAttributes, "");
CScriptValRooted msg;
client->GetScriptInterface().Eval("({'type':'start'})", msg);

View File

@ -29,6 +29,7 @@
#include "ps/Profile.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/SavedGame.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"

View File

@ -112,8 +112,10 @@ void CGame::SetTurnManager(CNetTurnManager* turnManager)
* Makes calls to initialize the game view, world, and simulation objects.
* Calls are made to facilitate progress reporting of the initialization.
**/
void CGame::RegisterInit(const CScriptValRooted& attribs)
void CGame::RegisterInit(const CScriptValRooted& attribs, const std::string& savedState)
{
m_InitialSavedState = savedState;
m_Simulation2->SetInitAttributes(attribs);
std::string mapType;
@ -151,9 +153,27 @@ void CGame::RegisterInit(const CScriptValRooted& attribs)
m_World->RegisterInitRMS(scriptFile, settings, m_PlayerID);
}
if (!m_InitialSavedState.empty())
RegMemFun(this, &CGame::LoadInitialState, L"Loading game", 1000);
LDR_EndRegistering();
}
int CGame::LoadInitialState()
{
ENSURE(!m_InitialSavedState.empty());
std::string state;
m_InitialSavedState.swap(state); // deletes the original to save a bit of memory
std::stringstream stream(state);
bool ok = m_Simulation2->DeserializeState(stream);
ENSURE(ok);
return 0;
}
/**
* Game initialization has been completed. Set game started flag and start the session.
*
@ -209,11 +229,11 @@ void CGame::SetPlayerID(int playerID)
m_TurnManager->SetPlayerID(m_PlayerID);
}
void CGame::StartGame(const CScriptValRooted& attribs)
void CGame::StartGame(const CScriptValRooted& attribs, const std::string& savedState)
{
m_ReplayLogger->StartGame(attribs);
RegisterInit(attribs);
RegisterInit(attribs, savedState);
}
// TODO: doInterpolate is optional because Atlas interpolates explicitly,

View File

@ -73,7 +73,7 @@ public:
**/
bool m_Paused;
void StartGame(const CScriptValRooted& attribs);
void StartGame(const CScriptValRooted& attribs, const std::string& savedState);
PSRETURN ReallyStartGame();
/*
@ -154,10 +154,13 @@ public:
{ return *m_ReplayLogger; }
private:
void RegisterInit(const CScriptValRooted& attribs);
void RegisterInit(const CScriptValRooted& attribs, const std::string& savedState);
IReplayLogger* m_ReplayLogger;
std::vector<CColor> m_PlayerColours;
int LoadInitialState();
std::string m_InitialSavedState; // valid between RegisterInit and LoadInitialState
};
extern CGame *g_Game;

View File

@ -430,6 +430,7 @@ static void InitVfs(const CmdLineArgs& args)
g_VFS = CreateVfs(cacheSize);
g_VFS->Mount(L"screenshots/", paths.Data()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.Data()/"saves"/"");
const OsPath readonlyConfig = paths.RData()/"config"/"";
g_VFS->Mount(L"config/", readonlyConfig);
if(readonlyConfig != paths.Config())
@ -1145,7 +1146,7 @@ bool Autostart(const CmdLineArgs& args)
else
{
g_Game->SetPlayerID(1);
g_Game->StartGame(attrs);
g_Game->StartGame(attrs, "");
LDR_NonprogressiveLoad();

View File

@ -155,7 +155,7 @@ void CReplayPlayer::Replay()
std::getline(*m_Stream, line);
CScriptValRooted attribs = game.GetSimulation2()->GetScriptInterface().ParseJSON(line);
game.StartGame(attribs);
game.StartGame(attribs, "");
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();

216
source/ps/SavedGame.cpp Normal file
View File

@ -0,0 +1,216 @@
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "SavedGame.h"
#include "gui/GUIManager.h"
#include "lib/allocators/shared_ptr.h"
#include "lib/file/archive/archive_zip.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
static const int SAVED_GAME_VERSION_MAJOR = 1; // increment on incompatible changes to the format
static const int SAVED_GAME_VERSION_MINOR = 0; // increment on compatible changes to the format
// TODO: we ought to check version numbers when loading files
Status SavedGames::Save(const std::wstring& prefix, CSimulation2& simulation, CGUIManager* gui, int playerID)
{
// Determine the filename to save under
const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d");
const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave");
VfsPath filename;
// Don't make this a static global like NextNumberedFilename expects, because
// that wouldn't work when 'prefix' changes, and because it's not thread-safe
size_t nextSaveNumber = 0;
vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename);
// ArchiveWriter_Zip can only write to OsPaths, not VfsPaths,
// but we'd like to handle saved games via VFS.
// To avoid potential confusion from writing with non-VFS then
// reading the same file with VFS, we'll just write to a temporary
// non-VFS path and then load and save again via VFS,
// which is kind of a hack.
OsPath tempSaveFileRealPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath));
tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave";
time_t now = time(NULL);
// Construct the serialized state to be saved
std::stringstream simStateStream;
if (!simulation.SerializeState(simStateStream))
WARN_RETURN(ERR::FAIL);
CScriptValRooted metadata;
simulation.GetScriptInterface().Eval("({})", metadata);
simulation.GetScriptInterface().SetProperty(metadata.get(), "version_major", SAVED_GAME_VERSION_MAJOR);
simulation.GetScriptInterface().SetProperty(metadata.get(), "version_minor", SAVED_GAME_VERSION_MINOR);
simulation.GetScriptInterface().SetProperty(metadata.get(), "time", (double)now);
simulation.GetScriptInterface().SetProperty(metadata.get(), "player", playerID);
simulation.GetScriptInterface().SetProperty(metadata.get(), "initAttributes", simulation.GetInitAttributes());
if (gui)
simulation.GetScriptInterface().SetProperty(metadata.get(), "gui", gui->GetSavedGameData());
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);
}
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"));
archiveWriter.reset(); // close the file
WriteBuffer buffer;
FileInfo tempSaveFile;
WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile));
buffer.Reserve(tempSaveFile.Size());
WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size()));
WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size()));
OsPath realPath;
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
LOGMESSAGERENDER(L"Saved game to %ls\n", realPath.string().c_str());
return INFO::OK;
}
class CGameLoader
{
NONCOPYABLE(CGameLoader);
public:
CGameLoader(ScriptInterface& scriptInterface, CScriptValRooted* metadata, std::string* savedState) :
m_ScriptInterface(scriptInterface), m_Metadata(metadata), m_SavedState(savedState)
{
}
static void ReadEntryCallback(const VfsPath& pathname, const FileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData)
{
((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile);
}
void ReadEntry(const VfsPath& pathname, const FileInfo& fileInfo, PIArchiveFile archiveFile)
{
if (pathname == L"metadata.json" && m_Metadata)
{
std::string buffer;
buffer.resize(fileInfo.Size());
WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size()));
*m_Metadata = m_ScriptInterface.ParseJSON(buffer);
}
else if (pathname == L"simulation.dat" && m_SavedState)
{
m_SavedState->resize(fileInfo.Size());
WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size()));
}
}
ScriptInterface& m_ScriptInterface;
CScriptValRooted* m_Metadata;
std::string* m_SavedState;
};
Status SavedGames::Load(const std::wstring& name, ScriptInterface& scriptInterface, CScriptValRooted& metadata, std::string& savedState)
{
// Determine the filename to load
const VfsPath basename(L"saves/" + name);
const VfsPath filename = basename.ChangeExtension(L".0adsave");
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);
}
CGameLoader loader(scriptInterface, &metadata, &savedState);
WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader));
return INFO::OK;
}
std::vector<CScriptValRooted> SavedGames::GetSavedGames(ScriptInterface& scriptInterface)
{
TIMER(L"GetSavedGames");
std::vector<CScriptValRooted> games;
Status err;
VfsPaths pathnames;
err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames);
WARN_IF_ERR(err);
for (size_t i = 0; i < pathnames.size(); ++i)
{
OsPath realPath;
err = g_VFS->GetRealPath(pathnames[i], realPath);
if (err < 0)
{
DEBUG_WARN_ERR(err);
continue; // skip this file
}
PIArchiveReader archiveReader;
try
{
archiveReader = CreateArchiveReader_Zip(realPath);
}
catch (Status err)
{
DEBUG_WARN_ERR(err);
continue; // skip this file
}
CScriptValRooted metadata;
CGameLoader loader(scriptInterface, &metadata, NULL);
err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader);
if (err < 0)
{
DEBUG_WARN_ERR(err);
continue; // skip this file
}
CScriptValRooted game;
scriptInterface.Eval("({})", game);
scriptInterface.SetProperty(game.get(), "id", pathnames[i].Basename());
scriptInterface.SetProperty(game.get(), "metadata", metadata);
games.push_back(game);
}
return games;
}

37
source/ps/SavedGame.h Normal file
View File

@ -0,0 +1,37 @@
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_SAVEDGAME
#define INCLUDED_SAVEDGAME
class CSimulation2;
class ScriptInterface;
class CScriptValRooted;
class CGUIManager;
namespace SavedGames
{
Status Save(const std::wstring& prefix, CSimulation2& simulation, CGUIManager* gui, int playerIDo);
Status Load(const std::wstring& name, ScriptInterface& scriptInterface, CScriptValRooted& metadata, std::string& savedState);
std::vector<CScriptValRooted> GetSavedGames(ScriptInterface& scriptInterface);
}
#endif // INCLUDED_SAVEDGAME

View File

@ -58,7 +58,7 @@ namespace
void StartGame(const CScriptValRooted& attrs)
{
g_Game->StartGame(attrs);
g_Game->StartGame(attrs, "");
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();