diff --git a/rsrc/dialogs/confirm-overwrite.xml b/rsrc/dialogs/confirm-overwrite.xml
new file mode 100644
index 000000000..c497c85d0
--- /dev/null
+++ b/rsrc/dialogs/confirm-overwrite.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/rsrc/dialogs/pick-save.xml b/rsrc/dialogs/pick-save.xml
new file mode 100644
index 000000000..60bb160bb
--- /dev/null
+++ b/rsrc/dialogs/pick-save.xml
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/rsrc/dialogs/pref-autosave.xml b/rsrc/dialogs/pref-autosave.xml
new file mode 100644
index 000000000..8f2ba63f3
--- /dev/null
+++ b/rsrc/dialogs/pref-autosave.xml
@@ -0,0 +1,20 @@
+
+
+
diff --git a/rsrc/dialogs/preferences.xml b/rsrc/dialogs/preferences.xml
index 7180de883..fed52bf62 100644
--- a/rsrc/dialogs/preferences.xml
+++ b/rsrc/dialogs/preferences.xml
@@ -49,7 +49,12 @@
(Holding Shift while using directional keys will do the opposite.)
Miscellaneous:
No Sounds
- Show room descriptions more than once
+ Use in-game save file browser
+ Autosave
+
+ Show room descriptions more than once
Make game easier (monsters much weaker)
Fewer wandering monsters
Skip splash screen on startup
diff --git a/rsrc/schemas/dialog.xsd b/rsrc/schemas/dialog.xsd
index a1bafb632..f5274e7ed 100644
--- a/rsrc/schemas/dialog.xsd
+++ b/rsrc/schemas/dialog.xsd
@@ -386,7 +386,7 @@
-
+
diff --git a/src/dialogxml/widgets/control.cpp b/src/dialogxml/widgets/control.cpp
index 1b38a51ea..e652c4825 100644
--- a/src/dialogxml/widgets/control.cpp
+++ b/src/dialogxml/widgets/control.cpp
@@ -260,6 +260,7 @@ void cControl::playClickSound(){
}
bool cControl::handleClick(location, cFramerateLimiter& fps_limiter){
+ if(!visible) return false;
sf::Event e;
bool done = false, clicked = false;
getWindow().setActive();
diff --git a/src/dialogxml/widgets/stack.cpp b/src/dialogxml/widgets/stack.cpp
index 56b8cb711..d34ebe5dd 100644
--- a/src/dialogxml/widgets/stack.cpp
+++ b/src/dialogxml/widgets/stack.cpp
@@ -13,6 +13,7 @@
#include "message.hpp"
#include "pict.hpp"
#include "scrollbar.hpp"
+#include "mathutil.hpp"
#include
bool cStack::hasChild(std::string id) const {
@@ -84,6 +85,17 @@ bool cStack::setPage(size_t n) {
return !failed;
}
+void cStack::changeSelectedPage(int dir, bool loop) {
+ curPage += dir;
+ if(loop){
+ if(curPage < 0) curPage += nPages;
+ else if(curPage >= nPages) curPage -= nPages;
+ }else{
+ curPage = minmax(0, nPages - 1, curPage);
+ }
+ setPage(curPage);
+}
+
size_t cStack::getPage() const {
return curPage;
}
diff --git a/src/dialogxml/widgets/stack.hpp b/src/dialogxml/widgets/stack.hpp
index d8c645ce0..c6d491e34 100644
--- a/src/dialogxml/widgets/stack.hpp
+++ b/src/dialogxml/widgets/stack.hpp
@@ -57,6 +57,10 @@ class cStack : public cContainer {
/// @param The new page number
/// @return false if the page could not be changed, usually due to a focus handler
bool setPage(size_t n);
+ /// Page forward or backward in the stack
+ /// @param dir Usually -1 or 1
+ /// @param loop Beyond the first and last page, loop to the other side
+ void changeSelectedPage(int dir, bool loop = true);
/// Get the current page the stack is displaying.
/// @return The current page number
size_t getPage() const;
diff --git a/src/fileio/fileio.cpp b/src/fileio/fileio.cpp
index 7558cc5a8..639523aeb 100644
--- a/src/fileio/fileio.cpp
+++ b/src/fileio/fileio.cpp
@@ -33,7 +33,7 @@ bool mac_is_intel(){
}
return _mac_is_intel;
}
-fs::path progDir, tempDir, scenDir, replayDir;
+fs::path progDir, tempDir, scenDir, replayDir, saveDir;
// This is here to avoid unnecessarily duplicating it in platform-specific files.
cursor_type Cursor::current = sword_curs;
@@ -81,6 +81,9 @@ void init_directories(const char* exec_path) {
replayDir = tempDir/"Replays";
fs::create_directories(replayDir);
+ saveDir = tempDir/"Saves";
+ fs::create_directories(saveDir);
+
add_resmgr_paths(tempDir/"data");
tempDir /= "Temporary Files";
diff --git a/src/fileio/fileio.hpp b/src/fileio/fileio.hpp
index 1ea9f4481..6cbacd44e 100644
--- a/src/fileio/fileio.hpp
+++ b/src/fileio/fileio.hpp
@@ -9,8 +9,10 @@
#ifndef BOE_FILEIO_HPP
#define BOE_FILEIO_HPP
+#include
#include
#include
+#include
#include
#include
#include
@@ -27,8 +29,17 @@ bool load_scenario(fs::path file_to_load, cScenario& scenario, bool only_header
fs::path nav_get_or_decode_party();
fs::path nav_put_or_temp_party(fs::path def = "");
-bool load_party(fs::path file_to_load, cUniverse& univ);
+fs::path os_file_picker(bool saving);
+// The game implements a fancy file picker, the editors just call the OS picker.
+extern fs::path run_file_picker(bool saving);
+
+const std::set save_extensions = {".exg", ".boe", ".SAV", ".mac"};
+// Return a directory's files sorted by last modified time
+std::vector> sorted_file_mtimes(fs::path dir, std::set valid_extensions = save_extensions);
+
+bool load_party(fs::path file_to_load, cUniverse& univ, bool record = true);
bool save_party(cUniverse& univ, bool save_as = false);
+bool save_party_force(cUniverse& univ, fs::path file);
void init_directories(const char* exec_path);
diff --git a/src/fileio/fileio_party.cpp b/src/fileio/fileio_party.cpp
index 3cf8b160b..f32d99b18 100644
--- a/src/fileio/fileio_party.cpp
+++ b/src/fileio/fileio_party.cpp
@@ -21,6 +21,7 @@
#include "fileio/tagfile.hpp"
#include "fileio/tarball.hpp"
#include "replay.hpp"
+#include "game/boe.dlgutil.hpp"
extern bool mac_is_intel();
extern fs::path progDir, tempDir;
@@ -39,6 +40,8 @@ fs::path nav_get_or_decode_party() {
decode_file(next_action.GetText(), tempDir / "temp.exg");
return tempDir / "temp.exg";
}else{
+ // TODO if the save is not in the saves folder, prompt about moving it in,
+ // and return the moved path?
return nav_get_party();
}
}
@@ -51,7 +54,14 @@ fs::path nav_put_or_temp_party(fs::path def) {
}
}
-bool load_party(fs::path file_to_load, cUniverse& univ){
+fs::path os_file_picker(bool saving) {
+ if(saving)
+ return nav_put_or_temp_party();
+ else
+ return nav_get_or_decode_party();
+}
+
+bool load_party(fs::path file_to_load, cUniverse& univ, bool record){
bool town_restore = false;
bool maps_there = false;
bool in_scen = false;
@@ -157,7 +167,7 @@ bool load_party(fs::path file_to_load, cUniverse& univ){
break;
}
- if(recording && result){
+ if(recording && record && result){
record_action("load_party", encode_file(file_to_load), true);
}
@@ -304,7 +314,6 @@ bool load_party_v1(fs::path file_to_load, cUniverse& real_univ, bool town_restor
return true;
}
-extern fs::path scenDir;
bool load_party_v2(fs::path file_to_load, cUniverse& real_univ){
igzstream zin(file_to_load.string().c_str());
tarball partyIn;
@@ -442,9 +451,12 @@ bool load_party_v2(fs::path file_to_load, cUniverse& real_univ){
return true;
}
-static bool save_party_const(const cUniverse& univ, bool save_as) {
+static bool save_party_const(const cUniverse& univ, bool save_as, fs::path dest_file = "") {
// Make sure it has the proper file extension
- fs::path dest_file = univ.file;
+ if(dest_file.empty()){
+ dest_file = univ.file;
+ }
+
if(dest_file.extension() != ".exg"){
dest_file += ".exg";
}
@@ -546,9 +558,30 @@ bool save_party(cUniverse& univ, bool save_as) {
// univ.file can be empty for prefab parties, so a file browser might be needed
// even for a regular save.
if(save_as || univ.file.empty()){
- univ.file = nav_put_or_temp_party();
+ univ.file = run_file_picker(true);
}
// A file wasn't chosen
if(univ.file.empty()) return false;
return save_party_const(univ, save_as);
}
+
+bool save_party_force(cUniverse& univ, fs::path file) {
+ return save_party_const(univ, false, file);
+}
+
+static bool compare_mtime(std::pair a, std::pair b) {
+ return std::difftime(a.second, b.second) > 0;
+}
+
+std::vector> sorted_file_mtimes(fs::path dir, std::set valid_extensions){
+ std::vector> files;
+ for(fs::directory_iterator it{dir}; it != fs::directory_iterator{}; ++it) {
+ fs::path file = *it;
+ if(valid_extensions.count(file.extension())){
+ files.push_back(std::make_pair(file, last_write_time(file)));
+ }
+ }
+
+ std::sort(files.begin(), files.end(), compare_mtime);
+ return files;
+}
\ No newline at end of file
diff --git a/src/game/boe.actions.cpp b/src/game/boe.actions.cpp
index 140a46798..dd2569ec3 100644
--- a/src/game/boe.actions.cpp
+++ b/src/game/boe.actions.cpp
@@ -467,7 +467,7 @@ void handle_rest(bool& need_redraw, bool& need_reprint) {
pause(25);
univ.party.food -= 6;
while(i < 50) {
- increase_age();
+ increase_age(false);
if(get_ran(1,1,2) == 2)
do_monsters();
if(get_ran(1,1,70) == 10)
@@ -485,6 +485,7 @@ void handle_rest(bool& need_redraw, bool& need_reprint) {
if(i == 50) {
do_rest(1200, get_ran(5,1,10), 50);
add_string_to_buf(" Rest successful.");
+ try_auto_save("RestComplete");
put_pc_screen();
pause(25);
}
@@ -1112,7 +1113,7 @@ static void handle_town_wait(bool& need_redraw, bool& need_reprint) {
}
for(int i = 0; i < 80 && !party_sees_a_monst(); i++){
- increase_age();
+ increase_age(false);
do_monsters();
do_monster_turn();
int make_wand = get_ran(1,1,160 - univ.town->difficulty);
@@ -1132,6 +1133,9 @@ static void handle_town_wait(bool& need_redraw, bool& need_reprint) {
redraw_screen(REFRESH_NONE);
}
put_pc_screen();
+ if(!party_sees_a_monst()){
+ try_auto_save("TownWaitComplete");
+ }
}
void handle_wait(bool& did_something, bool& need_redraw, bool& need_reprint) {
@@ -2900,6 +2904,7 @@ bool handle_scroll(const sf::Event& event) {
}
void do_load() {
+ // TODO this needs to be changed/moved because a picker dialog opens now!!!
// Edge case: Replay can be cut off before a file is chosen,
// or party selection can be canceled, and this will cause
// a crash trying to decode a party
@@ -2907,7 +2912,7 @@ void do_load() {
return;
}
- fs::path file_to_load = nav_get_or_decode_party();
+ fs::path file_to_load = run_file_picker(false);
if(file_to_load.empty()) return;
if(!load_party(file_to_load, univ))
return;
@@ -3060,7 +3065,7 @@ void do_rest(long length, int hp_restore, int mp_restore) {
adjust_spell_menus();
}
-void increase_age() {
+void increase_age(bool eating_trigger_autosave) {
short how_many_short = 0,r1;
@@ -3188,6 +3193,7 @@ void increase_age() {
else {
play_sound(6);
add_string_to_buf("You eat.");
+ if(eating_trigger_autosave) try_auto_save("Eat");
}
}
@@ -3421,7 +3427,7 @@ void handle_death() {
return;
}
else if(choice == "load") {
- fs::path file_to_load = nav_get_or_decode_party();
+ fs::path file_to_load = run_file_picker(false);
if(!file_to_load.empty()){
if(load_party(file_to_load, univ)){
finish_load_party();
diff --git a/src/game/boe.actions.hpp b/src/game/boe.actions.hpp
index 215b5ba83..bae9d7084 100644
--- a/src/game/boe.actions.hpp
+++ b/src/game/boe.actions.hpp
@@ -29,7 +29,7 @@ void do_load();
void post_load();
void do_save(bool save_as = false);
void do_abort();
-void increase_age();
+void increase_age(bool eating_trigger_autosave = true);
void handle_hunting();
void switch_pc(short which);
void handle_drop_pc();
diff --git a/src/game/boe.combat.cpp b/src/game/boe.combat.cpp
index 9a7993924..1f68247f4 100644
--- a/src/game/boe.combat.cpp
+++ b/src/game/boe.combat.cpp
@@ -6,6 +6,7 @@
#include "universe/universe.hpp"
#include "boe.monster.hpp"
#include "boe.graphics.hpp"
+#include "boe.fileio.hpp"
#include "boe.locutils.hpp"
#include "boe.newgraph.hpp"
#include "boe.infodlg.hpp"
@@ -4682,8 +4683,10 @@ bool hit_end_c_button() {
}
}
- if(end_ok)
+ if(end_ok){
end_combat();
+ if(which_combat_type == 0) try_auto_save("EndOutdoorCombat");
+ }
return end_ok;
}
diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp
index 20ccefa80..f96c09529 100644
--- a/src/game/boe.dlgutil.cpp
+++ b/src/game/boe.dlgutil.cpp
@@ -1,5 +1,9 @@
#include
+#include
+#include
+#include
+#include
#include "boe.global.hpp"
@@ -30,6 +34,7 @@
#include "fileio/fileio.hpp"
#include "fileio/resmgr/res_dialog.hpp"
#include "fileio/resmgr/res_strings.hpp"
+#include "dialogxml/widgets/field.hpp"
#include "dialogxml/widgets/scrollbar.hpp"
#include "dialogxml/widgets/button.hpp"
#include "dialogxml/widgets/ledgroup.hpp"
@@ -1197,6 +1202,69 @@ void save_prefs(){
}
}
+void autosave_preferences(cDialog* parent);
+
+static bool prefs_autosave_event_filter(cDialog& me, std::string id, eKeyMod mod) {
+ if(id == "autosave-toggle"){
+ dynamic_cast(me["autosave-toggle"]).defaultClickHandler(me, id, mod);
+ if(dynamic_cast(me["autosave-toggle"]).getState() != led_off){
+ me["autosave-details"].show();
+ }
+ else{
+ me["autosave-details"].hide();
+ }
+ }else if(id == "autosave-details"){
+ autosave_preferences(&me);
+ }
+ // Messy: Using the same handler for the autosave buttons in the main preferences,
+ // and the buttons in the autosave details window.
+ else{
+ bool did_cancel = false;
+ bool save_ok = false;
+
+ if(id == "okay") {
+ save_ok = me.toast(true);
+ } else if(id == "cancel") {
+ me.toast(false);
+ did_cancel = true;
+ }
+
+ if(!did_cancel && save_ok) {
+ set_pref("Autosave_Max", std::stoi(dynamic_cast(me["max-files"]).getText()));
+ for(cDialogIterator iter = me.begin(); iter != me.end(); ++iter){
+ std::string id = iter->first;
+ cControl* ctrl = iter->second;
+ cLed* led = dynamic_cast(ctrl);
+ if(led != nullptr){
+ set_pref("Autosave_" + id, led->getState() != led_off);
+ }
+ }
+ save_prefs();
+ }
+ }
+ return true;
+}
+
+void autosave_preferences(cDialog* parent) {
+ cDialog prefsDlog(*ResMgr::dialogs.get("pref-autosave"), parent);
+
+ int max_autosaves = get_int_pref("Autosave_Max", MAX_AUTOSAVE_DEFAULT);
+ cTextField& max_files = dynamic_cast(prefsDlog["max-files"]);
+ max_files.setText(std::to_string(max_autosaves));
+
+ for(cDialogIterator iter = prefsDlog.begin(); iter != prefsDlog.end(); ++iter){
+ std::string id = iter->first;
+ cControl* ctrl = iter->second;
+ cLed* led = dynamic_cast(ctrl);
+ if(led != nullptr){
+ led->setState(get_bool_pref("Autosave_" + id, true) ? led_red : led_off);
+ }
+ }
+ prefsDlog.attachClickHandlers(&prefs_autosave_event_filter, {"okay", "cancel"});
+ prefsDlog.run();
+}
+
+
static bool prefs_event_filter (cDialog& me, std::string id, eKeyMod) {
bool did_cancel = false;
@@ -1217,7 +1285,10 @@ static bool prefs_event_filter (cDialog& me, std::string id, eKeyMod) {
else if(cur_display_mode == "br") set_pref("DisplayMode", 4);
else if(cur_display_mode == "win") set_pref("DisplayMode", 5);
set_pref("PlaySounds", dynamic_cast(me["nosound"]).getState() == led_off);
+
set_pref("DirectionalKeyScrolling", dynamic_cast(me["screen-shift"]).getState() != led_off);
+ set_pref("FancyFilePicker", dynamic_cast(me["fancypicker"]).getState() != led_off);
+ set_pref("Autosave", dynamic_cast(me["autosave-toggle"]).getState() != led_off);
set_pref("RepeatRoomDescriptions", dynamic_cast(me["repeatdesc"]).getState() != led_off);
set_pref("ShowInstantHelp", dynamic_cast(me["nohelp"]).getState() == led_off);
@@ -1285,6 +1356,7 @@ void pick_preferences(bool record) {
set_cursor(sword_curs);
cDialog prefsDlog(*ResMgr::dialogs.get("preferences"));
+ prefsDlog.attachClickHandlers(&prefs_autosave_event_filter, {"autosave-toggle", "autosave-details"});
prefsDlog.attachClickHandlers(&prefs_event_filter, {"okay", "cancel"});
prefsDlog.attachClickHandlers(&reset_help, {"resethelp"});
@@ -1311,6 +1383,11 @@ void pick_preferences(bool record) {
}
dynamic_cast(prefsDlog["nosound"]).setState(get_bool_pref("PlaySounds", true) ? led_off : led_red);
+ dynamic_cast(prefsDlog["fancypicker"]).setState(get_bool_pref("FancyFilePicker", true) ? led_red : led_off);
+ bool autosave_on = get_bool_pref("Autosave", true);
+ dynamic_cast(prefsDlog["autosave-toggle"]).setState(autosave_on ? led_red : led_off);
+ if(!autosave_on)
+ prefsDlog["autosave-details"].hide();
dynamic_cast(prefsDlog["repeatdesc"]).setState(get_bool_pref("RepeatRoomDescriptions") ? led_red : led_off);
dynamic_cast(prefsDlog["nohelp"]).setState(get_bool_pref("ShowInstantHelp", true) ? led_off : led_red);
if(overall_mode == MODE_STARTUP && !party_in_memory) {
@@ -1599,13 +1676,9 @@ class cChooseScenario {
return true;
}
- bool doSelectPage(int dir) {
+ bool changeSelectedPage(int dir) {
auto& stk = dynamic_cast(me["list"]);
- int curPage = stk.getPage(), nPages = stk.getPageCount();
- curPage += dir;
- if(curPage < 0) curPage += nPages;
- else if(curPage >= nPages) curPage -= nPages;
- stk.setPage(curPage);
+ stk.changeSelectedPage(dir, true);
return true;
}
@@ -1645,8 +1718,8 @@ class cChooseScenario {
set_cursor(sword_curs);
me["cancel"].attachClickHandler(std::bind(&cChooseScenario::doCancel, this));
- me["next"].attachClickHandler(std::bind(&cChooseScenario::doSelectPage, this, 1));
- me["prev"].attachClickHandler(std::bind(&cChooseScenario::doSelectPage, this, -1));
+ me["next"].attachClickHandler(std::bind(&cChooseScenario::changeSelectedPage, this, 1));
+ me["prev"].attachClickHandler(std::bind(&cChooseScenario::changeSelectedPage, this, -1));
me["start1"].attachClickHandler(std::bind(&cChooseScenario::doSelectScenario, this, 0));
me["start2"].attachClickHandler(std::bind(&cChooseScenario::doSelectScenario, this, 1));
me["start3"].attachClickHandler(std::bind(&cChooseScenario::doSelectScenario, this, 2));
@@ -1676,3 +1749,348 @@ class cChooseScenario {
scen_header_type pick_a_scen() {
return cChooseScenario().run();
}
+
+extern fs::path saveDir;
+
+class cFilePicker {
+ const int SLOTS_PER_PAGE = 4;
+ int parties_per_page;
+
+ fs::path save_folder;
+ bool picking_auto;
+ bool saving;
+ cDialog me;
+ cStack& get_stack() { return dynamic_cast(me["list"]); }
+ std::string template_info_str;
+
+ std::vector> save_file_mtimes;
+ // We have to load the parties to get PC graphics, average level, location, etc.
+ // But we shouldn't load them all at once, because the amount is unlimited.
+ std::vector save_files;
+ int pages_populated = 0;
+ int saves_loaded = 0;
+
+ void init_pages() {
+ save_file_mtimes = sorted_file_mtimes(save_folder);
+ save_files.resize(save_file_mtimes.size());
+
+ cStack& stk = get_stack();
+ int num_pages = ceil((float)save_file_mtimes.size() / parties_per_page);
+ stk.setPageCount(num_pages);
+ }
+
+ void empty_slot(int idx) {
+ std::string suffix = std::to_string(idx+1);
+ me["file" + suffix].setText("");
+ me["pc" + suffix + "a"].hide();
+ me["pc" + suffix + "b"].hide();
+ me["pc" + suffix + "c"].hide();
+ me["pc" + suffix + "d"].hide();
+ me["pc" + suffix + "e"].hide();
+ me["pc" + suffix + "f"].hide();
+ me["info" + suffix].hide();
+ me["load" + suffix].hide();
+ me["auto" + suffix].hide();
+ me["auto" + suffix + "-more-recent"].hide();
+ }
+
+ void dummy_slot(int idx) {
+ std::string suffix = std::to_string(idx+1);
+ me["file" + suffix].setText("");
+ me["pc" + suffix + "a"].hide();
+ me["pc" + suffix + "b"].hide();
+ me["pc" + suffix + "c"].hide();
+ me["pc" + suffix + "d"].hide();
+ me["pc" + suffix + "e"].hide();
+ me["pc" + suffix + "f"].hide();
+ me["info" + suffix].hide();
+ me["auto" + suffix + "-more-recent"].hide();
+ }
+
+ void populate_slot(int idx, fs::path file, std::time_t mtime, cUniverse& party_univ) {
+ if(replaying){
+ dummy_slot(idx);
+ return;
+ }
+ std::string suffix = std::to_string(idx+1);
+ me["file" + suffix].setText(file.filename().string());
+
+ // Populate PC graphics
+ for(int i = 0; i < 6; ++i){
+ std::string key = "pc" + suffix;
+ key.push_back((char)('a' + i));
+ cPict& pict = dynamic_cast(me[key]);
+ if(party_univ.party[i].main_status != eMainStatus::ABSENT) {
+ pic_num_t pic = party_univ.party[i].which_graphic;
+ // TODO Apparently PCs are supposed to be able to have custom graphics and monster graphics,
+ // but the special node to Create PC only allows choosing preset PC pictures, so I don't
+ // think it's even possible to get a non-preset-PC graphic into a party without directly
+ // editing the tagfile. For now, I'm only rendering preset PCs.
+ if(pic >= 1000){
+ }else if(pic >= 100){
+ }else{
+ pict.setPict(pic, PIC_PC);
+ }
+ pict.show();
+ }else{
+ pict.hide();
+ }
+ }
+
+ // Populate party info
+ std::string party_info = template_info_str;
+
+ short level_sum = 0;
+ short num_pc = 0;
+ for(int i = 0; i < 6; ++i){
+ if(party_univ.party[i].main_status != eMainStatus::ABSENT) {
+ level_sum += party_univ.party[i].level;
+ ++num_pc;
+ }
+ }
+ short avg_level = round((float)(level_sum / num_pc));
+ boost::replace_first(party_info, "{Lv}", std::to_string(avg_level));
+ if(num_pc == 1){
+ boost::replace_first(party_info, "Avg. ", "");
+ }
+
+ auto local_time = *std::localtime(&mtime);
+ std::stringstream last_modified;
+ last_modified << std::put_time(&local_time, "%h %d, %Y %I:%M %p");
+ boost::replace_first(party_info, "{LastSaved}", last_modified.str());
+
+ if(!party_univ.party.scen_name.empty()){
+ boost::replace_first(party_info, "{Scenario}", party_univ.scenario.scen_name);
+ boost::replace_first(party_info, "{Location}", get_location(&party_univ));
+ }else{
+ boost::replace_first(party_info, "{Scenario}||{Location}", "");
+ }
+
+ me["info" + suffix].setText(party_info);
+
+ // Set up buttons
+ if(saving){
+ me["file1"].setText(""); // Keep the frame
+ me["auto" + suffix].hide();
+ me["auto" + suffix + "-more-recent"].hide();
+ me["save" + suffix].attachClickHandler(std::bind(&cFilePicker::doSave, this, file));
+ }else{
+ me["load" + suffix].attachClickHandler(std::bind(&cFilePicker::doLoad, this, file));
+
+ std::vector> auto_mtimes;
+ fs::path auto_folder = file;
+ if(!picking_auto){
+ auto_folder.replace_extension(".auto");
+ if(fs::is_directory(auto_folder)) auto_mtimes = sorted_file_mtimes(auto_folder);
+ }
+ if(auto_mtimes.empty()){
+ me["auto" + suffix].hide();
+ me["auto" + suffix + "-more-recent"].hide();
+ }else{
+ // If an autosave is newer than the main save, show an indicator
+ if(std::difftime(mtime, auto_mtimes.front().second) > 0)
+ me["auto" + suffix + "-more-recent"].show();
+
+ me["auto" + suffix].attachClickHandler(std::bind(&cFilePicker::showAuto, this, auto_folder));
+ }
+ }
+ }
+
+ void populate_page(int page) {
+ int parties_needed = min(save_file_mtimes.size(), (page+1) * parties_per_page);
+ while(saves_loaded < parties_needed){
+ fs::path next_file = save_file_mtimes[saves_loaded].first;
+ cUniverse party_univ;
+ if(!load_party(next_file, save_files[saves_loaded], false)){
+ // TODO show error, fatal? Show corrupted party?
+ }
+ saves_loaded++;
+ }
+
+ if(saving){
+ time_t now;
+ std::time(&now);
+ // Populate the first slot with the actual current party
+ populate_slot(0, "", now, univ);
+ }
+
+ int start_idx = page * parties_per_page;
+ for(int party_idx = start_idx; party_idx < start_idx + parties_per_page; ++party_idx){
+ int slot_idx = party_idx - start_idx;
+ if(saving) slot_idx++;
+ if(party_idx < parties_needed)
+ populate_slot(slot_idx, save_file_mtimes[party_idx].first, save_file_mtimes[party_idx].second, save_files[party_idx]);
+ else
+ empty_slot(party_idx - start_idx);
+ }
+
+ ++pages_populated;
+ }
+
+ bool showAuto(fs::path auto_folder) {
+ fs::path autosave = run_autosave_picker(auto_folder, &me);
+ if(!autosave.empty())
+ doLoad(autosave);
+ return true;
+ }
+
+ bool doLoad(fs::path selected_file) {
+ me.setResult(selected_file);
+ me.toast(false);
+ return true;
+ }
+
+ bool confirm_overwrite(fs::path selected_file) {
+ cChoiceDlog dlog("confirm-overwrite", {"save","cancel"}, &me);
+ cDialog& inner = *(dlog.operator->());
+ std::string prompt = inner["prompt"].getText();
+ boost::replace_first(prompt, "{File}", selected_file.filename().string());
+ inner["prompt"].setText(prompt);
+ std::string choice = dlog.show();
+ return choice == "save";
+ }
+
+ bool doSave(fs::path selected_file) {
+ if(selected_file.empty()){
+ selected_file = save_folder / me["file1-field"].getText();
+ selected_file += ".exg";
+ }
+ if(!fs::exists(selected_file) || confirm_overwrite(selected_file)){
+ me.setResult(selected_file);
+ me.toast(false);
+ }
+ return true;
+ }
+
+ bool doCancel() {
+ me.toast(false);
+ return true;
+ }
+
+ bool changeSelectedPage(int dir) {
+ auto& stk = dynamic_cast(me["list"]);
+ // This stack doesn't loop. It's easier to implement loading the files one page at a time
+ // if I know we're not gonna jump from page 0 to the last page, leaving a gap in the vector.
+ stk.changeSelectedPage(dir);
+ me["prev"].show();
+ me["next"].show();
+ if(stk.getPage() == 0){
+ me["prev"].hide();
+ }
+ if(stk.getPage() == stk.getPageCount() - 1){
+ me["next"].hide();
+ }
+
+ populate_page(stk.getPage());
+ return true;
+ }
+
+ bool doFileBrowser() {
+ fs::path from_browser = "";
+ if(replaying){
+ if(has_next_action("load_party")){
+ from_browser = "DUMMY";
+ }
+ }else{
+ from_browser = nav_get_party();
+ }
+ if(!from_browser.empty()){
+ me.setResult(from_browser);
+ me.toast(false);
+ }
+ return true;
+ }
+
+public:
+ cFilePicker(fs::path save_folder, bool saving, cDialog* parent = nullptr, bool picking_auto = false) :
+ me(*ResMgr::dialogs.get("pick-save"), parent),
+ save_folder(save_folder),
+ picking_auto(picking_auto),
+ saving(saving),
+ parties_per_page(saving ? SLOTS_PER_PAGE - 1 : SLOTS_PER_PAGE) {}
+
+ fs::path run() {
+ template_info_str = me["info1"].getText();
+
+ if(saving){
+ me["title-load"].hide();
+ me["title-auto"].hide();
+ me["file1"].setText(""); // Keep the frame
+ for(int i = 0; i < SLOTS_PER_PAGE; ++i){
+ me["load" + std::to_string(i+1)].hide();
+ }
+ }else{
+ if(picking_auto){
+ me["title-load"].hide();
+ std::string title = me["title-auto"].getText();
+ fs::path party_name = save_folder.filename();
+ party_name.replace_extension();
+ boost::replace_first(title, "{Folder}", party_name.string());
+ me["title-auto"].setText(title);
+ }else{
+ me["title-auto"].hide();
+ }
+ me["title-save"].hide();
+ me["file1-field"].hide();
+ me["file1-extension-label"].hide();
+ for(int i = 0; i < SLOTS_PER_PAGE; ++i){
+ me["save" + std::to_string(i+1)].hide();
+ }
+ }
+
+ me["cancel"].attachClickHandler(std::bind(&cFilePicker::doCancel, this));
+ me["find"].attachClickHandler(std::bind(&cFilePicker::doFileBrowser, this));
+ // Since it would be crazy to record and replay the metadata shown on a player's save picker
+ // dialog (which is what we do for the scenario picker),
+ // when replaying, basically make Left/Right buttons no-op.
+ // Load/Save buttons should send a dummy result.
+ if(!replaying){
+ me["next"].attachClickHandler(std::bind(&cFilePicker::changeSelectedPage, this, 1));
+ me["prev"].attachClickHandler(std::bind(&cFilePicker::changeSelectedPage, this, -1));
+ init_pages();
+ }else{
+ for(int i = 0; i < SLOTS_PER_PAGE; ++i){
+ std::string suffix = std::to_string(i+1);
+ // When replaying, a click on a load or save button means the dummy file picker can go away:
+ me["load" + suffix].attachClickHandler(std::bind(&cFilePicker::doLoad, this, "DUMMY"));
+ me["save" + suffix].attachClickHandler(std::bind(&cFilePicker::doSave, this, "DUMMY"));
+ // A click on an autosave button means another dummy file picker should open:
+ me["auto" + suffix].attachClickHandler(std::bind(&cFilePicker::showAuto, this, ""));
+ }
+ }
+
+ // Hide the prev button and populate the first page
+ changeSelectedPage(0);
+
+ me.run();
+ if(!me.hasResult()) return "";
+ fs::path file = me.getResult();
+ return file;
+ }
+};
+
+static fs::path run_file_picker(fs::path save_folder, bool saving) {
+ return cFilePicker(save_folder, saving).run();
+}
+
+fs::path run_autosave_picker(fs::path auto_folder, cDialog* parent) {
+ return cFilePicker(auto_folder, false, parent, true).run();
+}
+
+fs::path fancy_file_picker(bool saving) {
+ if(recording){
+ record_action("fancy_file_picker", bool_to_str(saving));
+ }
+ // TODO this is set up to be configurable, but not yet exposed in preferences.
+ fs::path save_folder = get_string_pref("SaveFolder", saveDir.string());
+
+ return run_file_picker(save_folder, saving);
+}
+
+fs::path run_file_picker(bool saving){
+ if(has_feature_flag("file-picker-dialog", "V1") && get_bool_pref("FancyFilePicker", true)){
+ return fancy_file_picker(saving);
+ }
+
+ return os_file_picker(saving);
+}
diff --git a/src/game/boe.dlgutil.hpp b/src/game/boe.dlgutil.hpp
index 54f74ad0f..295b52e47 100644
--- a/src/game/boe.dlgutil.hpp
+++ b/src/game/boe.dlgutil.hpp
@@ -25,5 +25,8 @@ void pick_preferences(bool record = true);
void save_prefs();
void tip_of_day();
struct scen_header_type pick_a_scen();
+fs::path fancy_file_picker(bool saving);
+// Pick from the autosaves made while playing in a given save file
+fs::path run_autosave_picker(fs::path auto_folder, cDialog* parent = nullptr);
#endif
diff --git a/src/game/boe.fileio.cpp b/src/game/boe.fileio.cpp
index 08d53db75..fc5b68b7b 100644
--- a/src/game/boe.fileio.cpp
+++ b/src/game/boe.fileio.cpp
@@ -482,3 +482,49 @@ bool load_scenario_header(fs::path file,scen_header_type& scen_head){
return true;
}
+
+// Some autosave triggers are more dangerous than others:
+std::map autosave_trigger_defaults = {
+ {"EnterTown", true},
+ {"ExitTown", true},
+ {"RestComplete", true},
+ {"TownWaitComplete", true},
+ {"EndOutdoorCombat", true},
+ {"Eat", false}
+};
+
+void try_auto_save(std::string reason) {
+ if(!get_bool_pref("Autosave", true)) return;
+ bool reason_default_on = false;
+ if(autosave_trigger_defaults.find(reason) != autosave_trigger_defaults.end())
+ reason_default_on = autosave_trigger_defaults[reason];
+ if(!get_bool_pref("Autosave_" + reason, reason_default_on)) return;
+ if(univ.file.empty()){
+ ASB("Autosave: Make a manual save first.");
+ print_buf();
+ return;
+ }
+
+ fs::path auto_folder = univ.file;
+ auto_folder.replace_extension(".auto");
+ fs::create_directories(auto_folder);
+ std::vector> auto_mtimes = sorted_file_mtimes(auto_folder);
+
+ int max_autosaves = get_int_pref("Autosave_Max", MAX_AUTOSAVE_DEFAULT);
+ fs::path target_path;
+ if(auto_mtimes.size() < max_autosaves){
+ target_path = auto_folder / std::to_string(auto_mtimes.size() + 1);
+ target_path += ".exg";
+ }
+ // Save file buffer is full, so overwrite the oldest autosave
+ else{
+ target_path = auto_mtimes.back().first;
+ }
+
+ if(save_party_force(univ, target_path)){
+ ASB("Autosave: Game saved");
+ }else{
+ ASB("Autosave: Save not completed");
+ }
+ print_buf();
+}
\ No newline at end of file
diff --git a/src/game/boe.fileio.hpp b/src/game/boe.fileio.hpp
index ae73d53a4..a3f38e354 100644
--- a/src/game/boe.fileio.hpp
+++ b/src/game/boe.fileio.hpp
@@ -30,4 +30,8 @@ fs::path locate_scenario(std::string scen_name);
void alter_rect(rectangle *r);
+// The player can configure autosaves on/off globally, or individually
+// for a variety of different trigger reasons
+void try_auto_save(std::string reason);
+
#endif
diff --git a/src/game/boe.global.hpp b/src/game/boe.global.hpp
index 91c683e29..950404840 100644
--- a/src/game/boe.global.hpp
+++ b/src/game/boe.global.hpp
@@ -19,6 +19,8 @@ const int NUM_ITEM_G = 120;
const int NUM_FACE_G = 80;
const int NUM_DLOG_G = 28;
+const int MAX_AUTOSAVE_DEFAULT = 5;
+
struct scen_header_type{
int intro_pic;
eContentRating rating;
diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp
index 01353fceb..2cb7d2b2d 100644
--- a/src/game/boe.graphics.cpp
+++ b/src/game/boe.graphics.cpp
@@ -312,6 +312,8 @@ void draw_startup_stats() {
to_rect = party_to;
to_rect.offset(pc_rect.left,pc_rect.top);
pic_num_t pic = univ.party[i].which_graphic;
+ // TODO This doesn't make sense. If we're in the startup menu, there are no scenario custom graphics.
+ // Doesn't this need to find it saved in the party?
if(pic >= 1000) {
std::shared_ptr gw;
graf_pos_ref(gw, from_rect) = spec_scen_g.find_graphic(pic % 1000, pic >= 10000);
@@ -596,30 +598,8 @@ std::pair text_bar_text() {
std::string text = "";
std::string right_text = "";
- location loc = (is_out()) ? global_to_local(univ.party.out_loc) : univ.party.town_loc;
- bool in_area = false;
+ text = get_location();
- if(is_out()) {
- for(short i = 0; i < univ.out->area_desc.size(); i++)
- if(loc.in(univ.out->area_desc[i])) {
- text = univ.out->area_desc[i].descr;
- in_area = true;
- }
- if(!in_area) {
- text = univ.out->name;
- }
- }
- if(is_town()) {
- for(short i = 0; i < univ.town->area_desc.size(); i++)
- if(loc.in(univ.town->area_desc[i])) {
- text = univ.town->area_desc[i].descr;
- in_area = true;
- }
- if(!in_area) {
- text = univ.town->name;
- }
-
- }
if((is_combat()) && (univ.cur_pc < 6) && !monsters_going) {
std::ostringstream sout;
diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp
index af28cff01..b7ed42953 100644
--- a/src/game/boe.main.cpp
+++ b/src/game/boe.main.cpp
@@ -99,7 +99,9 @@ std::string help_text_rsrc = "help";
std::map> feature_flags = {
// Legacy behavior of the T debug action (used by some replays)
// does not change the party's outdoors location
- {"debug-enter-town", {"move-outdoors"}}
+ {"debug-enter-town", {"move-outdoors"}},
+ // New in-game save file picker
+ {"file-picker-dialog", {"V1"}}
};
struct cParseEntrance {
@@ -583,6 +585,9 @@ static void replay_action(Element& action) {
new_fps = boost::lexical_cast(action.GetText());
}
replay_fps_limit.emplace(new_fps);
+ }else if(t == "fancy_file_picker"){
+ bool saving = str_to_bool(action.GetText());
+ fancy_file_picker(saving);
}else if(t == "load_party"){
decode_file(action.GetText(), tempDir / "temp.exg");
load_party(tempDir / "temp.exg", univ);
diff --git a/src/game/boe.text.cpp b/src/game/boe.text.cpp
index 28bf657bb..1e7c842e7 100644
--- a/src/game/boe.text.cpp
+++ b/src/game/boe.text.cpp
@@ -1220,3 +1220,37 @@ bool day_reached(unsigned short which_day, unsigned short which_event) {
return true;
else return false;
}
+
+std::string get_location(cUniverse* specific_univ) {
+ // This function is used to determine text bar text, which may be intended to be blank
+ // even if the party is technically outdoors, depending on the active game mode.
+ // I'm just trying to keep the old behavior the same.
+ bool outdoors = is_out();
+ bool town = is_town();
+ // For checking a save file's location, it can only be one or the other.
+ if(specific_univ != nullptr){
+ outdoors = specific_univ->party.town_num >= 200;
+ town = !outdoors;
+ }else{
+ specific_univ = &univ;
+ }
+
+ std::string loc_str = "";
+
+ location loc = outdoors ? global_to_local(specific_univ->party.out_loc) : specific_univ->party.town_loc;
+ if(outdoors) {
+ loc_str = specific_univ->out->name;
+ for(short i = 0; i < specific_univ->out->area_desc.size(); i++)
+ if(loc.in(specific_univ->out->area_desc[i])) {
+ loc_str = specific_univ->out->area_desc[i].descr;
+ }
+ }
+ if(town){
+ loc_str = specific_univ->town->name;
+ for(short i = 0; i < specific_univ->town->area_desc.size(); i++)
+ if(loc.in(specific_univ->town->area_desc[i])) {
+ loc_str = specific_univ->town->area_desc[i].descr;
+ }
+ }
+ return loc_str;
+}
\ No newline at end of file
diff --git a/src/game/boe.text.hpp b/src/game/boe.text.hpp
index 2f5d45136..5619f1996 100644
--- a/src/game/boe.text.hpp
+++ b/src/game/boe.text.hpp
@@ -48,3 +48,5 @@ struct text_label_t {
void place_text_label(std::string string, location at, bool centred);
void draw_text_label(const text_label_t& label);
+
+std::string get_location(cUniverse* specific_univ = nullptr);
diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp
index 138b4b10b..f0bad04a0 100644
--- a/src/game/boe.town.cpp
+++ b/src/game/boe.town.cpp
@@ -510,6 +510,8 @@ void start_town_mode(short which_town, short entry_dir) {
// TODO: One problem with this - it paints the terrain after the town entry dialog is dismissed
// ... except it actually doesn't, because the town enter special is only queued, not run immediately.
draw_terrain(1);
+
+ try_auto_save("EnterTown");
}
@@ -632,6 +634,10 @@ location end_town_mode(bool switching_level,location destination, bool debug_lea
univ.party.town_num = 200; // should be harmless...
+ if(!switching_level){
+ try_auto_save("ExitTown");
+ }
+
return to_return;
}
diff --git a/src/pcedit/pc.main.cpp b/src/pcedit/pc.main.cpp
index 9a897f3ed..76560f77c 100644
--- a/src/pcedit/pc.main.cpp
+++ b/src/pcedit/pc.main.cpp
@@ -568,3 +568,6 @@ void pick_preferences() {
#endif
}
+fs::path run_file_picker(bool saving){
+ return os_file_picker(saving);
+}
\ No newline at end of file
diff --git a/src/scenedit/scen.core.cpp b/src/scenedit/scen.core.cpp
index d4b897a97..9110dae4b 100644
--- a/src/scenedit/scen.core.cpp
+++ b/src/scenedit/scen.core.cpp
@@ -3776,3 +3776,7 @@ void edit_custom_sounds() {
snd_names.swap(scenario.snd_names);
}
}
+
+fs::path run_file_picker(bool saving){
+ return os_file_picker(saving);
+}
\ No newline at end of file
diff --git a/src/tools/winutil.linux.cpp b/src/tools/winutil.linux.cpp
index 433334a72..b5acb1dbd 100644
--- a/src/tools/winutil.linux.cpp
+++ b/src/tools/winutil.linux.cpp
@@ -134,6 +134,7 @@ void setWindowFloating(sf::Window& win, bool floating) {
}
}
+// TODO this check is only required when trying nav_get_* specifically, now that there's a save file picker
void init_fileio(){
// if init_fileio() is called more than once, only check once
static bool checked_zenity = false;