Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replay system part 1 #379

Merged
merged 41 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c324647
record and replay srand() in an action log
NQNStudios May 26, 2024
5374a20
pre-declare action log vars/functions for global use
NQNStudios May 27, 2024
8ae1585
windows and linux record preferences at startup
NQNStudios May 27, 2024
a9f098d
don't save preferences when replaying
NQNStudios Jul 27, 2024
75bd1e9
allow providing base filename for log recording
NQNStudios Jun 13, 2024
aee9d8f
Reorganize global replay code
NQNStudios Jun 13, 2024
fead544
don't double-compile ticpp.cpp
NQNStudios Jun 13, 2024
77b73a9
all scons platforms add src include directories
NQNStudios Jun 13, 2024
8c63c4f
include string and sstream directly
NQNStudios Jun 13, 2024
d692f70
visual studio projects add new files
NQNStudios Jun 13, 2024
f50bfe4
try adding replay.cpp to xcode projects
NQNStudios Jun 13, 2024
c29931e
refactor without raw pointers
NQNStudios Jun 14, 2024
f508235
add cppcodec header library for base64
NQNStudios Jun 14, 2024
4349fb6
action log has_next_action()
NQNStudios Jun 14, 2024
8cee7a4
WIP encode and decode save files in action logs
NQNStudios Jun 14, 2024
63e7a07
put decoded replay save in tempDir
NQNStudios Jun 14, 2024
da76e16
finish_load_party()
NQNStudios Jun 14, 2024
f9a7615
add cppcodec to visual studio include paths
NQNStudios Jun 14, 2024
36a1e1d
xcode add cppcodec to search paths
NQNStudios Jun 14, 2024
aee878e
add src/tools to xcode header search path
NQNStudios Jun 14, 2024
edf7ca3
fix command-line arg docs
NQNStudios Jul 3, 2024
93fffe3
allow parsing locations
NQNStudios Jul 3, 2024
3eaa3fe
record movement actions
NQNStudios Jul 3, 2024
9961a09
no spaces after ifs
NQNStudios Jul 3, 2024
78b615d
only record movement in recording mode
NQNStudios Jul 3, 2024
b3cb33f
replay movement actions
NQNStudios Jul 3, 2024
3a79aac
allow recording action with multiple parameters
NQNStudios Jul 4, 2024
fd785fb
record clicks on dialog controls
NQNStudios Jul 4, 2024
e307bc8
allow peeking the type of next replay action
NQNStudios Jul 4, 2024
3d61a67
parse std::map of replay action info elements
NQNStudios Jul 4, 2024
eade4fd
replay control clicks in open dialogs
NQNStudios Jul 4, 2024
9d36086
pop_next_action() iterate through without actually removing
NQNStudios Jul 5, 2024
391fb3b
record and replay control focus events
NQNStudios Jul 5, 2024
2b68e13
record and replay startup button clicks
NQNStudios Jul 5, 2024
aab34a7
when replaying, don't open file browsers
NQNStudios Jul 5, 2024
5fec4e9
use nullptr
NQNStudios Jul 27, 2024
ded0a2f
use specific stringstream types
NQNStudios Jul 27, 2024
b4c300c
put extern forward-declarations first
NQNStudios Jul 27, 2024
43f1482
make pop_next_action() return a reference
NQNStudios Jul 28, 2024
cea3edd
throw error when checking next_action_type() of null
NQNStudios Jul 28, 2024
c935d55
throw std::strings
NQNStudios Jul 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/game/boe.global.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,15 @@ struct effect_pat_type {
unsigned short pattern[9][9];
};

// Input recording system
namespace ticpp { class Element; }
using ticpp::Element;

extern bool recording;
extern bool replaying;

extern bool init_action_log(std::string command, std::string file);
extern void record_action(std::string action_type, std::string inner_text);
extern Element* pop_next_action(std::string expected_action_type="");

#endif
111 changes: 108 additions & 3 deletions src/game/boe.main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
#include "boe.global.hpp"
#include "universe/universe.hpp"

#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/lexical_cast.hpp>
#include <unordered_map>
#include <string>
#include <memory>
#include <iostream>
#include <ctime>
#include <sstream>
#include "boe.graphics.hpp"
#include "boe.newgraph.hpp"
#include "boe.fileio.hpp"
Expand Down Expand Up @@ -215,12 +220,95 @@ static void init_ui() {
init_buttons();
}

// Input recording system

#include <ctime>
#include <iomanip>

bool recording = false;
bool replaying = false;

using namespace ticpp;
Document log_document;
std::string log_file;

bool init_action_log(std::string command, std::string file) {
if (command == "record") {
// 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"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Boost.Algorithm is a new dependency, right? I don't mind adding it, but it needs to be mentioned in the README.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't need to be added to the setup of the CI for any of the platforms--it must be a dependency of one of the packages we already install.

The readme says

  • Boost - Filesystem and System, plus several header-only
    libraries; if you're picky, you can run scons and see it enumerate exactly which
    libraries are needed

So I think it could fall under that umbrella.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see that the readme is contradicting itself slightly – the vcpkg instructions do enumerate every necessary Boost library, after all. (Though I'm not confident there are no others missing.)

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";
log_file = stream.str();

try {
Element root_element("actions");
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);
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) {
Element* root = log_document.FirstChildElement();
Element next_action(action_type);
Text action_text(inner_text);
next_action.InsertEndChild(action_text);
root->InsertEndChild(next_action);
log_document.SaveFile(log_file);
}

Element* pop_next_action(std::string expected_action_type) {
Element* root = log_document.FirstChildElement();
Element* next_action = root->FirstChildElement();

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* clone = next_action->Clone()->ToElement();
root->RemoveChild(next_action);
return clone;
}

void process_args(int argc, char* argv[]) {
// Command line usage:
// "Blades of Exile" # basic launch
// "Blades of Exile" <save file> # launch and load save file

// "Blades of Exile" record <optional file> # record this session in a time-stamped xml file
// "Blades of Exile" play <file> # replay a session from an xml file
if (argc > 1) {
std::string file = "";
if (argc > 2) {
file = argv[2];
}
if (init_action_log(argv[1], file))
return;

if(!load_party(argv[1], univ)) {
std::cout << "Failed to load save file: " << argv[1] << std::endl;
return;
Expand All @@ -245,21 +333,38 @@ void init_boe(int argc, char* argv[]) {
#ifdef __APPLE__
init_menubar(); // Do this first of all because otherwise a default File and Window menu will be seen
#endif

// TODO preferences need to be recorded in the action log for deterministic replay
sync_prefs();
init_shaders();
init_tiling();
init_snd_tool();

init_ui();

adjust_window_mode();
// If we don't do this now it'll flash white to start with
mainPtr.clear(sf::Color::Black);
mainPtr.display();

set_cursor(watch_curs);
init_buf();
srand(time(nullptr));

// Seed the RNG
if (replaying) {
auto srand_element = pop_next_action("srand");

std::string ts(srand_element->GetText());
srand(atoi(ts.c_str()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this made me realize that we may want to consider moving away from rand() in favour of a Mersenne twister… but that would be something for a separate PR.

} else {
auto t = time(nullptr);
if (recording) {
std::string ts = boost::lexical_cast<std::string>(t);
record_action("srand", ts);
}
srand(t);
}
std::cout << rand() << std::endl;
init_screen_locs();
init_startup();
flushingInput = true;
Expand Down
29 changes: 26 additions & 3 deletions src/tools/prefs.win.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

#include "prefs.hpp"
#include "game/boe.global.hpp"
#include "ticpp.h"
#include <map>
#include <string>
#include <vector>
Expand Down Expand Up @@ -70,6 +72,9 @@ void clear_pref(std::string keypath) {
}

static bool save_prefs(fs::path fpath) {
// Don't save preferences when running a replay which might change them
if(replaying) return true;

if(!prefsDirty) return true;
fs::create_directories(fpath.parent_path());
std::ofstream fout(fpath.string().c_str());
Expand Down Expand Up @@ -99,10 +104,18 @@ static bool save_prefs(fs::path fpath) {
static bool load_prefs(fs::path fpath) {
prefsDirty = false;
std::map<std::string,boost::any> temp_prefs;
std::ifstream fin(fpath.string().c_str());
std::istream* in;
CelticMinstrel marked this conversation as resolved.
Show resolved Hide resolved
if (replaying) {
Element* prefs_action = pop_next_action("load_prefs");
in = new std::istringstream(prefs_action->GetText());
} else {
in = new std::ifstream(fpath.c_str());
}
(fpath.string().c_str());
std::string line;
while(std::getline(fin, line)) {
if(!fin) {
std::ostringstream prefs_recording;
while(std::getline(*in, line)) {
if(!in) {
perror("Error reading preferences");
return false;
}
Expand All @@ -121,6 +134,10 @@ static bool load_prefs(fs::path fpath) {
printf("Error reading preferences: line is missing key:\n\t%s\n", line.c_str());
return false;
}
if (recording) {
prefs_recording << line << std::endl;
}

std::string key = line.substr(0, key_end + 1), val = line.substr(val_beg);
if(val == "true") temp_prefs[key] = true;
else if(val == "false") temp_prefs[key] = false;
Expand All @@ -138,6 +155,12 @@ static bool load_prefs(fs::path fpath) {
}
}
prefs.swap(temp_prefs);
delete in;

if (recording) {
record_action("load_prefs", prefs_recording.str());
}

return prefsLoaded = true;
}

Expand Down