Files
oboe/src/tools/replay.cpp

290 lines
8.2 KiB
C++

#include "replay.hpp"
// Input recording system
#include <ctime>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/optional.hpp>
#include <cppcodec/base64_rfc4648.hpp>
#include <locale>
#include <codecvt>
#include <string>
#include <algorithm>
#include <boost/lexical_cast.hpp>
#include "ticpp.h"
#include "gfx/render_shapes.hpp"
#include "tools/framerate_limiter.hpp"
#ifndef MSBUILD_GITREV
#include "tools/gitrev.hpp"
#endif
using base64 = cppcodec::base64_rfc4648;
bool recording = false;
bool replaying = false;
bool record_verbose = false;
bool replay_verbose = false;
bool replay_strict = false;
std::string last_action_type;
const std::string replay_warning = "Replay warning: ";
const std::string replay_error = "Replay system internal error! ";
using namespace ticpp;
Document log_document;
std::string log_file;
Element* next_action;
boost::optional<cFramerateLimiter> replay_fps_limit;
bool init_action_log(std::string command, std::string file) {
if(command == "record-unique") {
// If a filename is given, use it as a base, but insert a timestamp for uniqueness.
if (file.empty())
file = "BoE";
if (boost::ends_with(file, ".xml"))
file = file.substr(0, file.length() - 4);
// Get a time stamp
std::time_t t = time(nullptr);
auto tm = *std::localtime(&t);
std::ostringstream stream;
stream << file << std::put_time(&tm, "_%d-%m-%Y_%H-%M-%S") << ".xml";
file = stream.str();
command = "record";
}
if(command == "record") {
log_file = file;
try {
Element root_element("actions");
#ifndef MSBUILD_GITREV
root_element.SetAttribute("SHA", GIT_REVISION);
root_element.SetAttribute("Tag", GIT_TAG);
root_element.SetAttribute("Status", GIT_STATUS);
root_element.SetAttribute("Repo", GIT_REPO);
#endif
log_document.InsertEndChild(root_element);
log_document.SaveFile(log_file);
recording = true;
std::cout << "Recording this session: " << log_file << std::endl;
} catch(...) {
std::cout << "Failed to write to file " << log_file << std::endl;
}
return true;
}
else if (command == "replay") {
try {
log_document.LoadFile(file);
Element* root = log_document.FirstChildElement();
next_action = root->FirstChildElement();
// Set replay_verbose automatically if the log has verbose elements.
// This must be done before starting the replay, not when the first
// verbose element appears, because the *absence* of a verbose elements
// is meaningful in a verbose replay.
// This is an O(N) traversal, but it won't matter, because advance_time
// elements are the most common elements in verbose logs.
// Specifying replay_verbose manually and incorrectly would cause confusing
// errors.
std::vector<std::string> verbose_actions = { "advance_time" };
Element* checking_action = next_action;
do{
if(std::find(verbose_actions.begin(), verbose_actions.end(), checking_action->Value()) != verbose_actions.end()){
replay_verbose = true;
break;
}
checking_action = checking_action->NextSiblingElement(false);
}while(checking_action != nullptr);
replaying = true;
} catch(...) {
std::cout << "Failed to load file " << file << std::endl;
}
return true;
}
return false;
}
void record_action(std::string action_type, std::string inner_text, bool cdata) {
Element* root = log_document.FirstChildElement();
Element next_action(action_type);
Text action_text(inner_text);
action_text.SetCDATA(cdata);
next_action.InsertEndChild(action_text);
root->InsertEndChild(next_action);
log_document.SaveFile(log_file);
}
void record_action(std::string action_type, std::map<std::string,std::string> info) {
Element* root = log_document.FirstChildElement();
Element next_action(action_type);
for(auto& p : info){
Element next_child(p.first);
Text child_text(p.second);
next_child.InsertEndChild(child_text);
next_action.InsertEndChild(next_child);
}
root->InsertEndChild(next_action);
log_document.SaveFile(log_file);
}
void record_field_input(cKey key) {
std::map<std::string,std::string> info;
info["spec"] = bool_to_str(key.spec);
if(key.spec){
info["k"] = boost::lexical_cast<std::string>(key.k);
}else{
std::wostringstream wstr;
wstr << key.c;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
std::string c = converter.to_bytes(wstr.str());
info["c"] = c;
}
info["mod"] = boost::lexical_cast<std::string>(key.mod);
record_action("field_input", info);
}
bool has_next_action(std::string type) {
if(type.empty())
return next_action != nullptr;
else
return has_next_action() && next_action_type() == type;
}
std::string next_action_type() {
if(next_action == nullptr){
throw std::string { "Replay error! No action left to check type" };
}
return next_action->Value();
}
Element& pop_next_action(std::string expected_action_type) {
if(next_action == nullptr){
throw std::string { "Replay error! No action left to pop" };
}
if(expected_action_type != "" && next_action->Value() != expected_action_type){
std::ostringstream stream;
stream << "Replay error! Expected '" << expected_action_type << "' action next";
throw stream.str();
}
Element* to_return = next_action;
next_action = next_action->NextSiblingElement(false);
if(replay_fps_limit.has_value()) {
replay_fps_limit->frame_finished();
}
// click_control actions are not meaningful for debugging
if (to_return->Value() != "click_control"){
last_action_type = to_return->Value();
if(replay_verbose){
// Verbose replays are for internal testing, so this console output won't bother
// anyone and is very helpful:
std::cout << "Replaying action: " << last_action_type << std::endl;
}
}
return *to_return;
}
std::map<std::string,std::string> info_from_action(Element& action) {
std::map<std::string,std::string> info = {};
Element* next_child = action.FirstChildElement(false);
while(next_child){
info[next_child->Value()] = next_child->GetTextOrDefault("");
next_child = next_child->NextSiblingElement(false);
}
return info;
}
std::string encode_file(fs::path file) {
std::ifstream ifs(file.string(), std::ios::binary|std::ios::ate);
std::ifstream::pos_type pos = ifs.tellg();
std::vector<char> result(pos);
ifs.seekg(0, std::ios::beg);
ifs.read(result.data(), pos);
return base64::encode(result.data(), result.size() * sizeof(char));
}
void decode_file(std::string data, fs::path file) {
std::ofstream ofs(file.string(), std::ios::binary|std::ios::ate);
std::vector<uint8_t> bytes = base64::decode(data.c_str(), strlen(data.c_str()) * sizeof(char));
char* bytes_as_c_str = reinterpret_cast<char*>(bytes.data());
ofs.write(bytes_as_c_str, bytes.size() / sizeof(char));
}
location location_from_action(Element& action) {
return boost::lexical_cast<location>(action.GetText());
}
short short_from_action(Element& action) {
return boost::lexical_cast<short>(action.GetText());
}
cKey key_from_action(Element& action) {
auto info = info_from_action(action);
cKey key;
int enum_v;
key.spec = str_to_bool(info["spec"]);
if(key.spec){
enum_v = boost::lexical_cast<int>(info["k"]);
key.k = static_cast<eSpecKey>(enum_v);
}else{
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
std::wstring wstr = converter.from_bytes(info["c"]);
std::wistringstream wsstr(wstr);
wsstr >> key.c;
}
enum_v = boost::lexical_cast<int>(info["mod"]);
key.mod = static_cast<eKeyMod>(enum_v);
return key;
}
word_rect_t word_rect_from_action(Element& action) {
auto info = info_from_action(action);
std::string word = info["word"];
rectangle rect = boost::lexical_cast<rectangle>(info["rect"]);
word_rect_t word_rect(word, rect);
word_rect.node = boost::lexical_cast<int>(info["node"]);
bool preset = str_to_bool(info["preset"]);
word_rect.on = preset ? PRESET_WORD_ON : CUSTOM_WORD_ON;
word_rect.off = preset ? PRESET_WORD_OFF : CUSTOM_WORD_OFF;
return word_rect;
}
void record_click_talk_rect(word_rect_t word_rect, bool preset) {
std::map<std::string,std::string> info;
info["word"] = word_rect.word;
info["rect"] = boost::lexical_cast<std::string>(word_rect.rect);
info["node"] = boost::lexical_cast<std::string>(word_rect.node);
info["preset"] = bool_to_str(preset);
record_action("click_talk_rect", info);
}