Ticket #3261: t3261_savegame_checkpoints_wip_v0.01.patch

File t3261_savegame_checkpoints_wip_v0.01.patch, 14.4 KB (added by elexis, 8 years ago)

Writes a savegame every 5 minutes of ingame time.

  • binaries/data/config/default.cfg

    rotate.y.smoothness = 0.3  
    411411near = 2.0                        ; Near plane distance
    412412far = 4096.0                      ; Far plane distance
    413413fov = 45.0                        ; Field of view (degrees), lower is narrow, higher is wide
    414414height.smoothness = 0.5
    415415height.min = 16
     416
     417[replay.save.session]
     418enabled = false                   ; Whether to generate savepoints in running games to skip / rewind replays (caution: requires significant disk space and can cause lag spikes)
     419interval = 300                    ; save the gamestate every 5 minutes
     420
     421[replay.save.replay]
     422enabled = true                    ; Whether to generate savepoints while replaying
     423interval = 300                    ; save the gamestate every 5 minutes
  • binaries/data/mods/public/gui/session/session.js

    var g_CivData = {};  
    8282/**
    8383 * For restoring selection, order and filters when returning to the replay menu
    8484 */
    8585var g_ReplaySelectionData;
    8686
     87/**
     88 * Simulation time in milliseconds.
     89 */
     90var g_ReplayLastSave = 0;
     91
    8792var g_PlayerAssignments = {
    8893    "local": {
    8994        "name": singleplayerName(),
    9095        "player": 1
    9196    }
    function onTick()  
    582587
    583588    checkPlayerState();
    584589
    585590    handleNetMessages();
    586591
     592    saveReplayStates();
     593
    587594    updateCursorAndTooltip();
    588595
    589596    if (g_Selection.dirty)
    590597    {
    591598        g_Selection.dirty = false;
    function reportGame()  
    12841291    reportObject.lootCollected = playerStatistics.lootCollected;
    12851292    reportObject.tradeIncome = playerStatistics.tradeIncome;
    12861293
    12871294    Engine.SendGameReport(reportObject);
    12881295}
     1296
     1297/**
     1298 * Write a savegame in frequent intervals for seeking replays.
     1299 */
     1300function saveReplayStates()
     1301{
     1302    let type = g_IsReplay ? "replay" : "session";
     1303
     1304    if (!Engine.ConfigDB_GetValue("user", "replay.save." + type + ".enabled"))
     1305        return;
     1306
     1307    let now = GetSimState().timeElapsed;
     1308
     1309    // Randomize the first time the game is being saved,
     1310    // to avoid clients serializing simultaneously.
     1311    // -> Likely better to do it parallel than in series
     1312    //if (!g_IsReplay && !g_ReplayLastSave)
     1313    //  g_ReplayLastSave = Math.random() * 10;
     1314
     1315    let duration = Engine.ConfigDB_GetValue("user", "replay.save." + type + ".interval");
     1316    duration = Math.max(duration, 1) * 1000;
     1317
     1318    if (now - g_ReplayLastSave < duration)
     1319        return;
     1320
     1321    // Don't save if we already got a checkpoint.
     1322    // Do this just before saving to be compatible for concurrent replays
     1323    if (false && g_IsReplay)
     1324    {
     1325        let states = Engine.GetReplayStates();
     1326        // states is an array of { "metadata": "", "filename": "" }
     1327        // TODO: loop over states and dont save if there is a recent one
     1328    }
     1329
     1330    // TODO: access the JS Timer component from c++ directly
     1331    if (Engine.SaveReplayState(now))
     1332        g_ReplayLastSave = now;
     1333}
  • source/ps/SavedGame.cpp

    Status SavedGames::Save(const CStrW& nam  
    7272
    7373    OsPath tempSaveFileRealPath;
    7474    WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath));
    7575    tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave";
    7676
     77    WARN_RETURN_STATUS_IF_ERR(WriteSavegameTemp(description, simulation, guiMetadataClone, tempSaveFileRealPath));
     78
     79    WriteBuffer buffer;
     80    CFileInfo tempSaveFile;
     81    WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile));
     82    buffer.Reserve(tempSaveFile.Size());
     83    WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size()));
     84    WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size()));
     85
     86    OsPath realPath;
     87    WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
     88    LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8());
     89    debug_printf("Saved game to '%s'\n", realPath.string8().c_str());
     90
     91    return INFO::OK;
     92}
     93
     94Status SavedGames::WriteSavegameTemp(const CStrW& description, CSimulation2& simulation, shared_ptr<ScriptInterface::StructuredClone> guiMetadataClone, OsPath& targetFile)
     95{
     96    JSContext* cx = simulation.GetScriptInterface().GetContext();
     97    JSAutoRequest rq(cx);
     98
    7799    time_t now = time(NULL);
    78100
    79101    // Construct the serialized state to be saved
    80102
    81103    std::stringstream simStateStream;
    Status SavedGames::Save(const CStrW& nam  
    111133    simulation.GetScriptInterface().SetProperty(metadata, "description", description);
    112134
    113135    std::string metadataString = simulation.GetScriptInterface().StringifyJSON(&metadata, true);
    114136
    115137    // Write the saved game as zip file containing the various components
    116     PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false);
     138    PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(targetFile, false);
    117139    if (!archiveWriter)
    118140        WARN_RETURN(ERR::FAIL);
    119141
    120142    WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json"));
    121143    WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat"));
    122144    archiveWriter.reset(); // close the file
    123145
    124     WriteBuffer buffer;
    125     CFileInfo tempSaveFile;
    126     WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile));
    127     buffer.Reserve(tempSaveFile.Size());
    128     WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size()));
    129     WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size()));
    130 
    131     OsPath realPath;
    132     WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
    133     LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8());
    134     debug_printf("Saved game to '%s'\n", realPath.string8().c_str());
    135 
    136146    return INFO::OK;
    137147}
    138148
    139149/**
    140150 * Helper class for retrieving data from saved game archives
  • source/ps/SavedGame.h

    namespace SavedGames  
    4646 * @param gui if not NULL, store some UI-related data with the saved game
    4747 * @return INFO::OK if successfully saved, else an error Status
    4848 */
    4949Status Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, shared_ptr<ScriptInterface::StructuredClone> guiMetadataClone);
    5050
     51
     52/**
     53 * Creates a savegame file, also used by replays.
     54 */
     55Status WriteSavegameTemp(const CStrW& description, CSimulation2& simulation, shared_ptr<ScriptInterface::StructuredClone> guiMetadataClone, OsPath& output);
     56
    5157/**
    5258 * Create new saved game archive with given prefix and simulation data
    5359 *
    5460 * @param prefix Create new numbered file starting with this prefix
    5561 * @param description A user-given description of the save
  • source/ps/VisualReplay.cpp

     
    1818#include "precompiled.h"
    1919
    2020#include "VisualReplay.h"
    2121#include "graphics/GameView.h"
    2222#include "gui/GUIManager.h"
     23#include "i18n/L10n.h"
    2324#include "lib/allocators/shared_ptr.h"
    2425#include "lib/external_libraries/libsdl.h"
    2526#include "lib/utf8.h"
    2627#include "network/NetClient.h"
    2728#include "network/NetServer.h"
     
    3031#include "ps/Game.h"
    3132#include "ps/GameSetup/Paths.h"
    3233#include "ps/Mod.h"
    3334#include "ps/Pyrogenesis.h"
    3435#include "ps/Replay.h"
     36#include "ps/SavedGame.h"
    3537#include "ps/Util.h"
    3638#include "scriptinterface/ScriptInterface.h"
    3739
    3840/**
    3941 * Filter too short replays (value in seconds).
    JS::Value VisualReplay::GetReplayMetadat  
    330332    SAFE_DELETE(stream);
    331333    pCxPrivate->pScriptInterface->ParseJSON(line, &metadata);
    332334
    333335    return metadata;
    334336}
     337
     338bool VisualReplay::SaveReplayState(ScriptInterface::CxPrivate* pCxPrivate, int simTime, JS::HandleValue GUIMetadata)
     339{
     340    shared_ptr<ScriptInterface::StructuredClone> guiMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
     341
     342    OsPath output = g_Game->GetReplayLogger().GetDirectory() / L"savegames";
     343    CreateDirectories(output, 0700);
     344
     345    output = output / (std::to_string(simTime) + CStr(".0adsave"));
     346
     347    if (SavedGames::WriteSavegameTemp(L"Replay", *g_Game->GetSimulation2(), guiMetadataClone, output) != INFO::OK)
     348        return false;
     349
     350    // TODO: something like IComponent* component = componentManager->QueryInterface(SYSTEM_ENTITY, IID_Timer);
     351
     352    LOGMESSAGERENDER(g_L10n.Translate("Saved replay checkpoint to '%s'"), output.string8());
     353    debug_printf("Saved replay checkpoint to %s\n", output.string8().c_str());
     354
     355    return true;
     356}
     357
     358JS::Value VisualReplay::GetReplayStates(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directory)
     359{
     360    TIMER(L"GetReplayStates");
     361
     362    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     363    JSAutoRequest rq(cx);
     364
     365    u32 i = 0;
     366    DirectoryNames filenames;
     367    JS::RootedObject replayStates(cx, JS_NewArrayObject(cx, 0));
     368
     369    if (GetDirectoryEntries(directory, NULL, &filenames) == INFO::OK)
     370        for (OsPath& filepath : filenames)
     371        {
     372            CStr filename = filepath.Filename().string8();
     373
     374            if (filename.substr(filename.size() - 8) == ".0adsave")
     375                continue;
     376
     377            int time = std::atoi(filename.substr(0, filename.size() - 8).c_str());
     378
     379            JS::RootedValue replayStateInfo(cx);
     380            pCxPrivate->pScriptInterface->Eval("({})", &replayStateInfo);
     381            pCxPrivate->pScriptInterface->SetProperty(replayStateInfo, "file", filename);
     382            pCxPrivate->pScriptInterface->SetProperty(replayStateInfo, "time", time);
     383            JS_SetElement(cx, replayStates, i++, replayStateInfo);
     384        }
     385
     386    return JS::ObjectValue(*replayStates);
     387}
  • source/ps/VisualReplay.h

     
    1 /* Copyright (C) 2015 Wildfire Games.
     1/* Copyright (C) 2016 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    55 * it under the terms of the GNU General Public License as published by
    66 * the Free Software Foundation, either version 2 of the License, or
    JS::Value GetReplayMetadata(ScriptInterf  
    8181/**
    8282 * Saves the metadata from the session to metadata.json
    8383 */
    8484void SaveReplayMetadata(const CStrW& data);
    8585
     86/**
     87 * Saves the serialized gamestate to the replay directory to allow skipping and rewinding replays.
     88 */
     89bool SaveReplayState(ScriptInterface::CxPrivate* pCxPrivate, int simTime, JS::HandleValue GUIMetadata);
     90
     91/**
     92 * Given a replay directory, returns an array of integers identifying the gametime of every savestate.
     93 */
     94JS::Value GetReplayStates(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directory);
     95
    8696}
    8797
    8898#endif
  • source/ps/scripting/JSInterface_VisualReplay.cpp

     
    1 /* Copyright (C) 2015 Wildfire Games.
     1/* Copyright (C) 2016 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    55 * it under the terms of the GNU General Public License as published by
    66 * the Free Software Foundation, either version 2 of the License, or
    JS::Value JSI_VisualReplay::GetReplayMet  
    5353void JSI_VisualReplay::SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& data)
    5454{
    5555    VisualReplay::SaveReplayMetadata(data);
    5656}
    5757
     58// TODO: support u64
     59bool JSI_VisualReplay::SaveReplayState(ScriptInterface::CxPrivate* pCxPrivate, int simTime, JS::HandleValue GUIMetadata)
     60{
     61    return VisualReplay::SaveReplayState(pCxPrivate, simTime, GUIMetadata);
     62}
     63
     64JS::Value JSI_VisualReplay::GetReplayStates(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directory)
     65{
     66    return VisualReplay::GetReplayStates(pCxPrivate, directory);
     67}
     68
    5869void JSI_VisualReplay::RegisterScriptFunctions(ScriptInterface& scriptInterface)
    5970{
    6071    scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays");
    6172    scriptInterface.RegisterFunction<bool, CStrW, &DeleteReplay>("DeleteReplay");
    6273    scriptInterface.RegisterFunction<void, CStrW, &StartVisualReplay>("StartVisualReplay");
    6374    scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayAttributes>("GetReplayAttributes");
    6475    scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayMetadata>("GetReplayMetadata");
    6576    scriptInterface.RegisterFunction<bool, CStrW, &HasReplayMetadata>("HasReplayMetadata");
    6677    scriptInterface.RegisterFunction<void, CStrW, &SaveReplayMetadata>("SaveReplayMetadata");
     78    scriptInterface.RegisterFunction<bool, int, JS::HandleValue, &SaveReplayState>("SaveReplayState");
     79    scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayStates>("GetReplayStates");
    6780}
  • source/ps/scripting/JSInterface_VisualReplay.h

     
    1 /* Copyright (C) 2015 Wildfire Games.
     1/* Copyright (C) 2016 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    55 * it under the terms of the GNU General Public License as published by
    66 * the Free Software Foundation, either version 2 of the License, or
    namespace JSI_VisualReplay  
    2828    JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate);
    2929    JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
    3030    bool HasReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
    3131    JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
    3232    void SaveReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& data);
     33    bool SaveReplayState(ScriptInterface::CxPrivate* pCxPrivate, int, JS::HandleValue GUIMetadata);
     34    JS::Value GetReplayStates(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directory);
    3335    void RegisterScriptFunctions(ScriptInterface& scriptInterface);
    3436}
    3537
    3638#endif