Merge pull request #657 from NQNStudios:replay-fixes
A round of replay system fixes and improvements. * replaying the debug help window was broken in 2 ways which I fixed * Fix #559 -- When we observe bugs in random sessions, we no longer have to regret that we didn't pass `--record` at the command line. The replay data is kept in memory and writing it out can be enabled with debug action Z. * Recording replays into the working directory by default is pretty wonky, because the working directory is usually a build output folder that might be clobbered by a `scons -c`, wiping out important replays. Also, if the working directory is the Visual Studio project folder, you end up adding untracked files to your repo folder which can get annoying. So - I've made the default replay directory a folder alongside the tempDir that already exists. On Windows, this is in AppData, on Mac it's in Application Support, etc. But if you pass an explicitly relative or absolute path, it is resolved as expected. * I made a mistake a while ago that made the Debug action N (end scenario) not get recorded. Fixed!
This commit is contained in:
@@ -33,7 +33,7 @@ bool mac_is_intel(){
|
||||
}
|
||||
return _mac_is_intel;
|
||||
}
|
||||
fs::path progDir, tempDir, scenDir;
|
||||
fs::path progDir, tempDir, scenDir, replayDir;
|
||||
|
||||
// This is here to avoid unnecessarily duplicating it in platform-specific files.
|
||||
cursor_type Cursor::current = sword_curs;
|
||||
@@ -77,6 +77,10 @@ void init_directories(const char* exec_path) {
|
||||
#endif
|
||||
scenDir = tempDir/"Scenarios";
|
||||
fs::create_directories(scenDir);
|
||||
|
||||
replayDir = tempDir/"Replays";
|
||||
fs::create_directories(replayDir);
|
||||
|
||||
add_resmgr_paths(tempDir/"data");
|
||||
tempDir /= "Temporary Files";
|
||||
|
||||
@@ -100,6 +104,7 @@ void init_directories(const char* exec_path) {
|
||||
std::cout << "Program directory: " << progDir << std::endl;
|
||||
std::cout << "Scenario directory: " << scenDir << std::endl;
|
||||
std::cout << "Temporary directory: " << tempDir << std::endl;
|
||||
std::cout << "Replay directory: " << replayDir << std::endl;
|
||||
}
|
||||
|
||||
#if !defined(_WIN32) && !defined(_WIN64) && !defined(__APPLE__)
|
||||
|
||||
@@ -2364,7 +2364,9 @@ void show_debug_help() {
|
||||
if(action.action != &show_debug_help){
|
||||
button.attachClickHandler([action](cDialog& me, std::string, eKeyMod) -> bool {
|
||||
me.toast(false);
|
||||
action.action();
|
||||
// In a replay, the action will have been recorded next anyway, so the dialog doesn't need to trigger it.
|
||||
if(!replaying)
|
||||
action.action();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -2385,7 +2387,7 @@ void show_debug_help() {
|
||||
}
|
||||
|
||||
// Non-comprehensive list of unused keys:
|
||||
// UYZ chijklnoqvy @#$-_+[]{},.'"`~/\|;:
|
||||
// Y chijklnoqvy @#$-_+[]{},.'"`~/\|;:
|
||||
void init_debug_actions() {
|
||||
add_debug_action({'B'}, "Leave town", debug_leave_town);
|
||||
add_debug_action({'C'}, "Get cleaned up (lose negative status effects)", debug_clean_up);
|
||||
@@ -2401,7 +2403,7 @@ void init_debug_actions() {
|
||||
add_debug_action({'J'}, "Preview a dialog's layout", preview_dialog_xml);
|
||||
add_debug_action({'U'}, "Preview EVERY dialog's layout", preview_every_dialog_xml);
|
||||
add_debug_action({'K'}, "Kill everything", debug_kill);
|
||||
add_debug_action({'N'}, "End scenario", []() -> void {handle_victory(true);});
|
||||
add_debug_action({'N'}, "End scenario", []() -> void {handle_victory(true, true);});
|
||||
add_debug_action({'O'}, "Print your location", debug_print_location);
|
||||
add_debug_action({'Q'}, "Magic map", debug_magic_map);
|
||||
add_debug_action({'R'}, "Return to start", debug_return_to_start);
|
||||
@@ -2425,6 +2427,7 @@ void init_debug_actions() {
|
||||
add_debug_action({'%'}, "Fight wandering encounter from this section", []() -> void {debug_fight_encounter(true);});
|
||||
add_debug_action({'^'}, "Fight special encounter from this section", []() -> void {debug_fight_encounter(false);});
|
||||
add_debug_action({'/', '?'}, "Bring up this window", show_debug_help);
|
||||
add_debug_action({'Z'}, "Save the current action log for bug reporting", save_replay_log);
|
||||
}
|
||||
|
||||
// Later we might want to know whether the key is used or not
|
||||
@@ -3967,4 +3970,13 @@ void preview_every_dialog_xml() {
|
||||
preview_dialog_xml(path);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void save_replay_log(){
|
||||
// This doesn't need to be recorded or replayed.
|
||||
if(replaying) return;
|
||||
|
||||
fs::path out_file = nav_put_rsrc({"xml"});
|
||||
|
||||
start_log_file(out_file.string());
|
||||
}
|
||||
@@ -117,5 +117,6 @@ void update_item_stats_area(bool& need_reprint);
|
||||
void easter_egg(int idx);
|
||||
void preview_dialog_xml();
|
||||
void preview_every_dialog_xml();
|
||||
void save_replay_log();
|
||||
|
||||
#endif
|
||||
|
||||
@@ -112,8 +112,8 @@ static bool display_spells_event_filter(cDialog& me, std::string item_hit, eSkil
|
||||
return true;
|
||||
}
|
||||
//short force_spell; // if 100, ignore
|
||||
void display_spells(eSkill mode,short force_spell,cDialog* parent) {
|
||||
if(recording){
|
||||
void display_spells(eSkill mode,short force_spell,cDialog* parent, bool record) {
|
||||
if(recording && record){
|
||||
std::map<std::string,std::string> info;
|
||||
info["mode"] = boost::lexical_cast<std::string>(mode);
|
||||
info["force_spell"] = boost::lexical_cast<std::string>(force_spell);
|
||||
@@ -174,8 +174,8 @@ static bool display_skills_event_filter(cDialog& me, std::string item_hit, eKeyM
|
||||
return true;
|
||||
}
|
||||
|
||||
void display_skills(eSkill force_skill,cDialog* parent) {
|
||||
if(recording){
|
||||
void display_skills(eSkill force_skill,cDialog* parent, bool record) {
|
||||
if(recording && record){
|
||||
record_action("display_skills", boost::lexical_cast<std::string>(force_skill));
|
||||
}
|
||||
if(force_skill != eSkill::INVALID)
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#include "universe/creature.hpp"
|
||||
|
||||
class cDialog;
|
||||
void display_spells(eSkill mode,short force_spell,cDialog* parent);
|
||||
void display_skills(eSkill force_skill,cDialog* parent);
|
||||
void display_spells(eSkill mode,short force_spell,cDialog* parent, bool record=false);
|
||||
void display_skills(eSkill force_skill,cDialog* parent, bool record=false);
|
||||
void display_pc_item(short pc_num,short item,class cItem si,cDialog* parent);
|
||||
void display_monst(short array_pos,cCreature *which_m,short mode);
|
||||
void display_alchemy();
|
||||
|
||||
@@ -304,6 +304,7 @@ extern bool record_verbose;
|
||||
extern bool replay_verbose;
|
||||
extern bool replay_strict;
|
||||
|
||||
bool record_in_memory = true;
|
||||
|
||||
static void process_args(int argc, char* argv[]) {
|
||||
preprocess_args(argc, argv);
|
||||
@@ -374,6 +375,11 @@ static void process_args(int argc, char* argv[]) {
|
||||
exit(1);
|
||||
}
|
||||
// Don't return, because we want to support recording a run that starts with a party from the CLI.
|
||||
}else if(record_in_memory){
|
||||
if(!init_action_log("record", "")){
|
||||
std::cerr << "Failed to start recording in memory." << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if(saved_game){
|
||||
@@ -818,7 +824,7 @@ static void replay_action(Element& action) {
|
||||
debug_return_to_start();
|
||||
return;
|
||||
}else if(t == "handle_victory"){
|
||||
handle_victory();
|
||||
handle_victory(true); // This is for the debug action which forces it.
|
||||
return;
|
||||
}else if(t == "debug_increase_age"){
|
||||
debug_increase_age();
|
||||
@@ -910,8 +916,9 @@ static void replay_action(Element& action) {
|
||||
cancel_item_target(did_something, need_redraw, need_reprint);
|
||||
}else if(t == "easter_egg"){
|
||||
easter_egg(boost::lexical_cast<int>(action.GetText()));
|
||||
}else if(t == "show_debug_panel"){
|
||||
}else if(t == "show_debug_help"){
|
||||
show_debug_help();
|
||||
return;
|
||||
}else if(t == "debug_fight_encounter"){
|
||||
debug_fight_encounter(str_to_bool(action.GetText()));
|
||||
}else if(t == "preview_every_dialog_xml"){
|
||||
@@ -1487,13 +1494,13 @@ void handle_menu_choice(eMenu item_hit) {
|
||||
dialogToShow = "about-boe";
|
||||
break;
|
||||
case eMenu::LIBRARY_MAGE:
|
||||
display_spells(eSkill::MAGE_SPELLS,100,nullptr);
|
||||
display_spells(eSkill::MAGE_SPELLS,100,nullptr,true);
|
||||
break;
|
||||
case eMenu::LIBRARY_PRIEST:
|
||||
display_spells(eSkill::PRIEST_SPELLS,100,nullptr);
|
||||
display_spells(eSkill::PRIEST_SPELLS,100,nullptr,true);
|
||||
break;
|
||||
case eMenu::LIBRARY_SKILLS:
|
||||
display_skills(eSkill::INVALID,nullptr);
|
||||
display_skills(eSkill::INVALID,nullptr,true);
|
||||
break;
|
||||
case eMenu::LIBRARY_ALCHEMY:
|
||||
// TODO: Create a dedicated dialog for alchemy info
|
||||
|
||||
@@ -2046,7 +2046,7 @@ void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_
|
||||
if(replaying && has_next_action("step_through_exit")){
|
||||
pop_next_action();
|
||||
univ.node_step_through = false;
|
||||
}else if(evt.type == sf::Event::KeyPressed && evt.key.code == sf::Keyboard::Escape){
|
||||
}else if(!replaying && evt.type == sf::Event::KeyPressed && evt.key.code == sf::Keyboard::Escape){
|
||||
record_action("step_through_exit", "");
|
||||
univ.node_step_through = false;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ void display_alchemy(bool allowEdit,cDialog* parent);
|
||||
bool spend_xp(short pc_num, short mode, cDialog* parent);
|
||||
// TODO: There's probably a more logical way of arranging this
|
||||
|
||||
void display_skills(eSkill skill,cDialog* parent);
|
||||
void display_skills(eSkill skill,cDialog* parent, bool record = false);
|
||||
|
||||
extern cUniverse univ;
|
||||
extern short store_flags[3];
|
||||
|
||||
@@ -459,7 +459,7 @@ bool verify_restore_quit(std::string dlog) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void display_skills(eSkill skill,cDialog* parent) {
|
||||
void display_skills(eSkill skill,cDialog* parent, bool record) {
|
||||
extern std::map<eSkill,short> skill_cost;
|
||||
extern std::map<eSkill,short> skill_max;
|
||||
extern std::map<eSkill,short> skill_g_cost;
|
||||
|
||||
@@ -40,10 +40,22 @@ const std::string replay_error = "Replay system internal error! ";
|
||||
|
||||
using namespace ticpp;
|
||||
Document log_document;
|
||||
std::string log_file;
|
||||
fs::path log_file;
|
||||
Element* next_action;
|
||||
boost::optional<cFramerateLimiter> replay_fps_limit;
|
||||
|
||||
static void save_log() {
|
||||
if(!log_file.empty()) log_document.SaveFile(log_file.string());
|
||||
}
|
||||
|
||||
void start_log_file(std::string file) {
|
||||
log_file = file;
|
||||
std::cout << "Recording this session: " << log_file << std::endl;
|
||||
save_log();
|
||||
}
|
||||
|
||||
extern fs::path replayDir;
|
||||
|
||||
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.
|
||||
@@ -63,6 +75,9 @@ bool init_action_log(std::string command, std::string file) {
|
||||
}
|
||||
if(command == "record") {
|
||||
log_file = file;
|
||||
if(!log_file.empty() && log_file == log_file.filename()){
|
||||
log_file = replayDir / log_file;
|
||||
}
|
||||
try {
|
||||
Element root_element("actions");
|
||||
#ifndef MSBUILD_GITREV
|
||||
@@ -72,9 +87,12 @@ bool init_action_log(std::string command, std::string file) {
|
||||
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;
|
||||
if(log_file.empty()){
|
||||
std::cout << "Recording this session in memory." << std::endl;
|
||||
}else{
|
||||
start_log_file(log_file.string());
|
||||
}
|
||||
} catch(...) {
|
||||
std::cout << "Failed to write to file " << log_file << std::endl;
|
||||
}
|
||||
@@ -82,7 +100,11 @@ bool init_action_log(std::string command, std::string file) {
|
||||
}
|
||||
else if (command == "replay") {
|
||||
try {
|
||||
log_document.LoadFile(file);
|
||||
fs::path file_path = file;
|
||||
if(file_path == file_path.filename() && !fs::exists(file_path)){
|
||||
file_path = replayDir / file_path;
|
||||
}
|
||||
log_document.LoadFile(file_path.string());
|
||||
|
||||
Element* root = log_document.FirstChildElement();
|
||||
next_action = root->FirstChildElement();
|
||||
@@ -125,7 +147,7 @@ void record_action(std::string action_type, std::string inner_text, bool cdata)
|
||||
action_text.SetCDATA(cdata);
|
||||
next_action.InsertEndChild(action_text);
|
||||
root->InsertEndChild(next_action);
|
||||
log_document.SaveFile(log_file);
|
||||
save_log();
|
||||
}
|
||||
|
||||
void record_action(std::string action_type, std::map<std::string,std::string> info) {
|
||||
@@ -138,13 +160,13 @@ void record_action(std::string action_type, std::map<std::string,std::string> in
|
||||
next_action.InsertEndChild(next_child);
|
||||
}
|
||||
root->InsertEndChild(next_action);
|
||||
log_document.SaveFile(log_file);
|
||||
save_log();
|
||||
}
|
||||
|
||||
void record_action(Element& action) {
|
||||
Element* root = log_document.FirstChildElement();
|
||||
root->InsertEndChild(action);
|
||||
log_document.SaveFile(log_file);
|
||||
save_log();
|
||||
}
|
||||
|
||||
void record_field_input(cKey key) {
|
||||
|
||||
@@ -47,6 +47,7 @@ extern short short_from_action(Element& action);
|
||||
extern cKey key_from_action(Element& action);
|
||||
extern word_rect_t word_rect_from_action(Element& action);
|
||||
extern void record_click_talk_rect(word_rect_t word_rect, bool preset);
|
||||
extern void start_log_file(std::string file);
|
||||
|
||||
extern const std::string replay_warning;
|
||||
extern const std::string replay_error;
|
||||
|
||||
Reference in New Issue
Block a user