Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 17355) +++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 17356) @@ -1,341 +1,342 @@ const g_EngineInfo = Engine.GetEngineInfo(); const g_CivData = loadCivData(); const g_mapSizes = initMapSizes(); /** * All replays found in the directory. */ var g_Replays = []; /** * List of replays after applying the display filter. */ var g_ReplaysFiltered = []; /** * Array of unique usernames of all replays. Used for autocompleting usernames. */ var g_Playernames = []; /** * Sorted list of unique maptitles. Used by mapfilter. */ var g_MapNames = []; /** * Directory name of the currently selected replay. Used to restore the selection after changing filters. */ var g_selectedReplayDirectory = ""; /** * Initializes globals, loads replays and displays the list. */ function init() { if (!g_Settings) { Engine.SwitchGuiPage("page_pregame.xml"); return; } // By default, sort replays by date in descending order Engine.GetGUIObjectByName("replaySelection").selected_column_order = -1; loadReplays(); displayReplayList(); } /** * Store the list of replays loaded in C++ in g_Replays. * Check timestamp and compatibility and extract g_Playernames, g_MapNames */ function loadReplays() { g_Replays = Engine.GetReplays(); g_Playernames = []; for (let replay of g_Replays) { // Use time saved in file, otherwise file mod date - replay.timestamp = replay.attribs.timestamp ? +replay.attribs.timestamp : +replay.filemod_timestamp; + replay.timestamp = replay.attribs.timestamp ? +replay.attribs.timestamp : +replay.filemod_timestamp-replay.duration; // Check replay for compability replay.isCompatible = isReplayCompatible(replay); sanitizeGameAttributes(replay.attribs); // Extract map names if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "") g_MapNames.push(replay.attribs.settings.Name); // Extract playernames for (let playerData of replay.attribs.settings.PlayerData) { if (!playerData || playerData.AI) continue; // Remove rating from nick let playername = playerData.Name; let ratingStart = playername.indexOf(" ("); if (ratingStart != -1) playername = playername.substr(0, ratingStart); if (g_Playernames.indexOf(playername) == -1) g_Playernames.push(playername); } } g_MapNames.sort(); // Reload filters (since they depend on g_Replays and its derivatives) initFilters(); } /** * We may encounter malformed replays. */ function sanitizeGameAttributes(attribs) { if (!attribs.settings) attribs.settings = {}; if (!attribs.settings.Size) attribs.settings.Size = -1; if (!attribs.settings.Name) attribs.settings.Name = ""; if (!attribs.settings.PlayerData) attribs.settings.PlayerData = []; if (!attribs.settings.PopulationCap) attribs.settings.PopulationCap = 300; if (!attribs.settings.mapType) attribs.settings.mapType = "skirmish"; if (!attribs.settings.GameType) attribs.settings.GameType = "conquest"; // Remove gaia if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null) attribs.settings.PlayerData.shift(); attribs.settings.PlayerData.forEach((pData, index) => { if (!pData.Name) pData.Name = ""; }); } /** * Filter g_Replays, fill the GUI list with that data and show the description of the current replay. */ function displayReplayList() { // Remember previously selected replay var replaySelection = Engine.GetGUIObjectByName("replaySelection"); if (replaySelection.selected != -1) g_selectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory; filterReplays(); // Create GUI list data var list = g_ReplaysFiltered.map(replay => { let works = replay.isCompatible; return { "directories": replay.directory, "months": greyout(getReplayDateTime(replay), works), "popCaps": greyout(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works), "mapNames": greyout(getReplayMapName(replay), works), "mapSizes": greyout(translateMapSize(replay.attribs.settings.Size), works), "durations": greyout(getReplayDuration(replay), works), "playerNames": greyout(getReplayPlayernames(replay), works) }; }); // Extract arrays if (list.length) list = prepareForDropdown(list); // Push to GUI replaySelection.selected = -1; replaySelection.list_name = list.months || []; replaySelection.list_players = list.playerNames || []; replaySelection.list_mapName = list.mapNames || []; replaySelection.list_mapSize = list.mapSizes || []; replaySelection.list_popCapacity = list.popCaps || []; replaySelection.list_duration = list.durations || []; // Change these last, otherwise crash replaySelection.list = list.directories || []; replaySelection.list_data = list.directories || []; // Restore selection replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_selectedReplayDirectory); displayReplayDetails(); } /** * Shows preview image, description and player text in the right panel. */ function displayReplayDetails() { var selected = Engine.GetGUIObjectByName("replaySelection").selected; var replaySelected = selected > -1; Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected; Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected; Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected; Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected; - Engine.GetGUIObjectByName("summaryButton").enabled = replaySelected; + Engine.GetGUIObjectByName("summaryButton").hidden = true; if (!replaySelected) return; var replay = g_ReplaysFiltered[selected]; var mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map); // Update GUI Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name); Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size); Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.settings.mapType); Engine.GetGUIObjectByName("sgVictory").caption = translateVictoryCondition(replay.attribs.settings.GameType); Engine.GetGUIObjectByName("sgNbPlayers").caption = replay.attribs.settings.PlayerData.length; Engine.GetGUIObjectByName("sgPlayersNames").caption = getReplayTeamText(replay); Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description; Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapData.preview; + Engine.GetGUIObjectByName("summaryButton").hidden = !Engine.HasReplayMetadata(replay.directory); } /** * Adds grey font if replay is not compatible. */ function greyout(text, isCompatible) { return isCompatible ? text : '[color="96 96 96"]' + text + '[/color]'; } /** * Returns a human-readable version of the replay date. */ function getReplayDateTime(replay) { return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm")) } /** * Returns a human-readable list of the playernames of that replay. * * @returns {string} */ function getReplayPlayernames(replay) { // TODO: colorize playernames like in the lobby. return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", "); } /** * Returns the name of the map of the given replay. * * @returns {string} */ function getReplayMapName(replay) { return translate(replay.attribs.settings.Name); } /** * Returns the month of the given replay in the format "yyyy-MM". * * @returns {string} */ function getReplayMonth(replay) { return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM")); } /** * Returns a human-readable version of the time when the replay started. * * @returns {string} */ function getReplayDuration(replay) { return timeToString(replay.duration * 1000); } /** * True if we can start the given replay with the currently loaded mods. */ function isReplayCompatible(replay) { return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs, g_EngineInfo); } /** * True if we can start the given replay with the currently loaded mods. */ function replayHasSameEngineVersion(replay) { return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version; } /** * Returns a description of the player assignments. * Including civs, teams, AI settings and player colors. * * If the spoiler-checkbox is checked, it also shows defeated players. * * @returns {string} */ function getReplayTeamText(replay) { // Load replay metadata const metadata = Engine.GetReplayMetadata(replay.directory); const spoiler = Engine.GetGUIObjectByName("showSpoiler").checked; var playerDescriptions = {}; var playerIdx = 0; for (let playerData of replay.attribs.settings.PlayerData) { // Get player info ++playerIdx; let teamIdx = playerData.Team; let playerColor = playerData.Color ? playerData.Color : g_Settings.PlayerDefaults[playerIdx].Color; let showDefeated = spoiler && metadata && metadata.playerStates && metadata.playerStates[playerIdx].state == "defeated"; let isAI = playerData.AI; // Create human-readable player description let playerDetails = { "playerName": '[color="' + rgbToGuiColor(playerColor) + '"]' + escapeText(playerData.Name) + "[/color]", "civ": translate(g_CivData[playerData.Civ].Name), "AIname": isAI ? translateAIName(playerData.AI) : "", "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : "" }; if (!isAI && !showDefeated) playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s)"), playerDetails); else if (!isAI && showDefeated) playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, defeated)"), playerDetails); else if (isAI && !showDefeated) playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)"), playerDetails); else playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, defeated)"), playerDetails); // Sort player descriptions by team if (!playerDescriptions[teamIdx]) playerDescriptions[teamIdx] = []; playerDescriptions[teamIdx].push(playerDetails); } var teams = Object.keys(playerDescriptions); // If there are no teams, merge all playersDescriptions if (teams.length == 1) return playerDescriptions[teams[0]].join("\n") + "\n"; // If there are teams, merge "Team N:" + playerDescriptions return teams.map(team => { let teamCaption = (team == -1) ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 }); return '[font="sans-bold-14"]' + teamCaption + "[/font]:\n" + playerDescriptions[team].join("\n"); }).join("\n"); } Index: ps/trunk/source/ps/VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/VisualReplay.cpp (revision 17355) +++ ps/trunk/source/ps/VisualReplay.cpp (revision 17356) @@ -1,321 +1,325 @@ /* Copyright (C) 2015 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 . */ #include "precompiled.h" #include "VisualReplay.h" #include "graphics/GameView.h" #include "gui/GUIManager.h" #include "lib/allocators/shared_ptr.h" #include "lib/utf8.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "scriptinterface/ScriptInterface.h" /** * Filter too short replays (value in seconds). */ const u8 minimumReplayDuration = 3; /** * Allows quick debugging of potential platform-dependent file-reading bugs. */ const bool debugParser = false; OsPath VisualReplay::GetDirectoryName() { return OsPath(psLogDir() / L"sim_log"); } -void VisualReplay::StartVisualReplay(CStrW directory) +void VisualReplay::StartVisualReplay(const CStrW& directory) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt"; if (!FileExists(replayFile)) return; g_Game = new CGame(false, false); g_Game->StartVisualReplay(replayFile.string8()); } JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface) { TIMER(L"GetReplays"); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); u32 i = 0; DirectoryNames directories; JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0)); if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK) for (OsPath& directory : directories) { JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory)); if (!replayData.isNull()) JS_SetElement(cx, replays, i++, replayData); } return JS::ObjectValue(*replays); } /** * Move the cursor backwards until a newline was read or the beginning of the file was found. * Either way the cursor points to the beginning of a newline. * * @return The current cursor position or -1 on error. */ inline int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64& fileSize) { int currentPos; char character; for (int characters = 0; characters < 10000; ++characters) { currentPos = (int) replayStream->tellg(); // Stop when reached the beginning of the file if (currentPos == 0) return currentPos; if (!replayStream->good()) { LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.c_str()); return -1; } // Stop when reached newline replayStream->get(character); if (character == '\n') return currentPos; // Otherwise go back one character. // Notice: -1 will set the cursor back to the most recently read character. replayStream->seekg(-2, std::ios_base::cur); } LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.c_str()); return -1; } /** * Compute game duration. Assume constant turn length. * Find the last line that starts with "turn" by reading the file backwards. * * @return seconds or -1 on error */ inline int getReplayDuration(std::istream *replayStream, const CStr& fileName, const u64& fileSize) { CStr type; // Move one character before the file-end replayStream->seekg(-2, std::ios_base::end); // Infinite loop protection, should never occur. // There should be about 5 lines to read until a turn is found. for (int linesRead = 1; linesRead < 1000; ++linesRead) { int currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize); // Read error or reached file beginning. No turns exist. if (currentPosition < 1) return -1; if (debugParser) + // TODO: throws a compiler warning on some systems debug_printf("At position %i of %lu after %i lines reads.\n", currentPosition, fileSize, linesRead); if (!replayStream->good()) { LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.c_str()); return -1; } // Found last turn, compute duration. if ((u64) currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn") { u32 turn = 0, turnLength = 0; *replayStream >> turn >> turnLength; return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0 } // Otherwise move cursor back to the character before the last newline replayStream->seekg(currentPosition - 2, std::ios_base::beg); } LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str()); return -1; } JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory) { // The directory argument must not be constant, otherwise concatenating will fail const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; if (debugParser) debug_printf("Opening %s\n", replayFile.string8().c_str()); if (!FileExists(replayFile)) return JSVAL_NULL; // Get file size and modification date CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); const u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath) const u64 fileSize = (u64)fileInfo.Size(); if (fileSize == 0) return JSVAL_NULL; // Open file const CStr fileName = replayFile.string8(); std::ifstream* replayStream = new std::ifstream(fileName.c_str()); // File must begin with "start" CStr type; if (!(*replayStream >> type).good() || type != "start") { LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } // Parse header / first line CStr header; std::getline(*replayStream, header); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue attribs(cx); if (!scriptInterface.ParseJSON(header, &attribs)) { LOGERROR("Couldn't parse replay header of %s", fileName.c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } // Ensure "turn" after header if (!(*replayStream >> type).good() || type != "turn") { SAFE_DELETE(replayStream); return JSVAL_NULL; // there are no turns at all } // Don't process files of rejoined clients u32 turn = 1; *replayStream >> turn; if (turn != 0) { SAFE_DELETE(replayStream); return JSVAL_NULL; } int duration = getReplayDuration(replayStream, fileName, fileSize); SAFE_DELETE(replayStream); // Ensure minimum duration if (duration < minimumReplayDuration) return JSVAL_NULL; // Return the actual data JS::RootedValue replayData(cx); scriptInterface.Eval("({})", &replayData); scriptInterface.SetProperty(replayData, "file", replayFile); scriptInterface.SetProperty(replayData, "directory", directory); scriptInterface.SetProperty(replayData, "filemod_timestamp", std::to_string(fileTime)); scriptInterface.SetProperty(replayData, "attribs", attribs); scriptInterface.SetProperty(replayData, "duration", duration); return replayData; } bool VisualReplay::DeleteReplay(const CStrW& replayDirectory) { if (replayDirectory.empty()) return false; const OsPath directory = GetDirectoryName() / replayDirectory; return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; } JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) { // Create empty JS object JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue attribs(cx); pCxPrivate->pScriptInterface->Eval("({})", &attribs); // Return empty object if file doesn't exist const OsPath replayFile = GetDirectoryName() / directoryName / L"commands.txt"; if (!FileExists(replayFile)) return attribs; // Open file std::istream* replayStream = new std::ifstream(replayFile.string8().c_str()); CStr type, line; ENSURE((*replayStream >> type).good() && type == "start"); // Read and return first line std::getline(*replayStream, line); pCxPrivate->pScriptInterface->ParseJSON(line, &attribs); SAFE_DELETE(replayStream);; return attribs; } // TODO: enhancement: how to save the data if the process is killed? (case SDL_QUIT in main.cpp) void VisualReplay::SaveReplayMetadata(const CStrW& data) { // TODO: enhancement: use JS::HandleValue similar to SaveGame if (!g_Game) return; // Get the directory of the currently active replay const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json"; CreateDirectories(fileName.Parent(), 0700); - std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc); + std::ofstream stream (fileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc); stream << utf8_from_wstring(data); stream.close(); } +bool VisualReplay::HasReplayMetadata(const CStrW& directoryName) +{ + return FileExists(GetDirectoryName() / directoryName / L"metadata.json"); +} + JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) { - const OsPath filePath = GetDirectoryName() / directoryName / L"metadata.json"; + if (!HasReplayMetadata(directoryName)) + return JSVAL_NULL; JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue metadata(cx); - if (!FileExists(filePath)) - return JSVAL_NULL; - - std::ifstream* stream = new std::ifstream(OsString(filePath).c_str()); + std::ifstream* stream = new std::ifstream(OsPath(GetDirectoryName() / directoryName / L"metadata.json").string8()); ENSURE(stream->good()); CStr line; std::getline(*stream, line); stream->close(); SAFE_DELETE(stream); pCxPrivate->pScriptInterface->ParseJSON(line, &metadata); return metadata; } Index: ps/trunk/source/ps/VisualReplay.h =================================================================== --- ps/trunk/source/ps/VisualReplay.h (revision 17355) +++ ps/trunk/source/ps/VisualReplay.h (revision 17356) @@ -1,83 +1,88 @@ /* Copyright (C) 2015 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 . */ #ifndef INCLUDED_REPlAY #define INCLUDED_REPlAY #include "scriptinterface/ScriptInterface.h" class CSimulation2; class CGUIManager; /** * Contains functions for visually replaying past games. */ namespace VisualReplay { /** * Returns the path to the sim-log directory (that contains the directories with the replay files. * * @param scriptInterface the ScriptInterface in which to create the return data. * @return OsPath the absolute file path */ OsPath GetDirectoryName(); /** * Replays the commands.txt file in the given subdirectory visually. */ -void StartVisualReplay(CStrW directory); +void StartVisualReplay(const CStrW& directory); /** * Get a list of replays to display in the GUI. * * @param scriptInterface the ScriptInterface in which to create the return data. * @return array of objects containing replay data */ JS::Value GetReplays(ScriptInterface& scriptInterface); /** * Parses a commands.txt file and extracts metadata. * Works similarly to CGame::LoadReplayData(). */ JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory); /** * Permanently deletes the visual replay (including the parent directory) * * @param replayFile path to commands.txt, whose parent directory will be deleted * @return true if deletion was successful, false on error */ bool DeleteReplay(const CStrW& replayFile); /** * Returns the parsed header of the replay file (commands.txt). */ JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); /** + * Returns whether or not the metadata / summary screen data has been saved properly when the game ended. + */ +bool HasReplayMetadata(const CStrW& directoryName); + +/** * Returns the metadata of a replay. */ JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); /** * Saves the metadata from the session to metadata.json */ void SaveReplayMetadata(const CStrW& data); } #endif Index: ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 17355) +++ ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 17356) @@ -1,61 +1,67 @@ /* Copyright (C) 2015 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 . */ #include "precompiled.h" #include "ps/VisualReplay.h" #include "ps/scripting/JSInterface_VisualReplay.h" void JSI_VisualReplay::StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW directory) { VisualReplay::StartVisualReplay(directory); } bool JSI_VisualReplay::DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW replayFile) { return VisualReplay::DeleteReplay(replayFile); } JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate) { return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface)); } JS::Value JSI_VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName) { return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName); } +bool JSI_VisualReplay::HasReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW directoryName) +{ + return VisualReplay::HasReplayMetadata(directoryName); +} + JS::Value JSI_VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName) { return VisualReplay::GetReplayMetadata(pCxPrivate, directoryName); } void JSI_VisualReplay::SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW data) { VisualReplay::SaveReplayMetadata(data); } void JSI_VisualReplay::RegisterScriptFunctions(ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GetReplays"); scriptInterface.RegisterFunction("DeleteReplay"); scriptInterface.RegisterFunction("StartVisualReplay"); scriptInterface.RegisterFunction("GetReplayAttributes"); scriptInterface.RegisterFunction("GetReplayMetadata"); + scriptInterface.RegisterFunction("HasReplayMetadata"); scriptInterface.RegisterFunction("SaveReplayMetadata"); } Index: ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h (revision 17355) +++ ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h (revision 17356) @@ -1,35 +1,36 @@ /* Copyright (C) 2015 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 . */ #ifndef INCLUDED_JSI_VISUALREPLAY #define INCLUDED_JSI_VISUALREPLAY #include "ps/VisualReplay.h" #include "scriptinterface/ScriptInterface.h" namespace JSI_VisualReplay { - void StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW directory); - bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW replayFile); + void StartVisualReplay(ScriptInterface::CxPrivate* pCxPrivate, CStrW directory); + bool DeleteReplay(ScriptInterface::CxPrivate* pCxPrivate, CStrW replayFile); JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate); JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName); + bool HasReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName); JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName); - void SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW data); + void SaveReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW data); void RegisterScriptFunctions(ScriptInterface& scriptInterface); } #endif