diff --git a/Common/core/assetmanager.cpp b/Common/core/assetmanager.cpp index 3796a9b201..307131e213 100644 --- a/Common/core/assetmanager.cpp +++ b/Common/core/assetmanager.cpp @@ -14,7 +14,6 @@ #include "core/assetmanager.h" #include #include -#include "util/directory.h" #include "util/file.h" #include "util/multifilelib.h" #include "util/path.h" @@ -177,6 +176,30 @@ bool AssetManager::DoesAssetExist(const String &asset_name, const String &filter return false; } +bool AssetManager::GetAssetTime(const String &asset_name, time_t &ft, const String &filter) const +{ + for (const auto *lib : _activeLibs) + { + if (!lib->TestFilter(filter)) continue; // filter does not match + + if (IsAssetLibDir(lib)) + { + String filename = File::FindFileCI(lib->BaseDir, asset_name); + if (!filename.IsEmpty()) + { + ft = File::GetFileTime(filename); + return true; + } + } + else + { + ft = File::GetFileTime(lib->RealLibFiles[0]); + return true; + } + } + return false; +} + void AssetManager::FindAssets(std::vector &assets, const String &wildcard, const String &filter) const { @@ -211,6 +234,58 @@ void AssetManager::FindAssets(std::vector &assets, const String &wildcar assets.erase(std::unique(assets.begin(), assets.end(), StrEqNoCase()), assets.end()); } +void AssetManager::FindAssets(std::vector &assets, const String &wildcard, + const String &filter) const +{ + // TODO: consider refactoring and merging this with FindAssets(std::vector &assets); + // there are two separate methods now, because retrieving filename only is faster than + // full FileEntry (may require extra system calls on certain platforms). + + String pattern = StrUtil::WildcardToRegex(wildcard); + const std::regex regex(pattern.GetCStr(), std::regex_constants::icase); + std::cmatch mr; + + std::vector lib_fileents; + for (const auto *lib : _activeLibs) + { + if (!lib->TestFilter(filter)) continue; // filter does not match + + lib_fileents.clear(); + if (IsAssetLibDir(lib)) + { + // FIXME: do basedir/getparent(wildcard), getfilename(wildcard) instead? + // because FindFile does not support subdirs in wildcard!! + for (FindFile ff = FindFile::OpenFiles(lib->BaseDir, wildcard); + !ff.AtEnd(); ff.Next()) + lib_fileents.push_back(ff.GetEntry()); + } + else + { + time_t lib_time = File::GetFileTime(lib->RealLibFiles[0]); + for (const auto &a : lib->AssetInfos) + { + if (std::regex_match(a.FileName.GetCStr(), mr, regex)) + lib_fileents.push_back(FileEntry(a.FileName, true, false, lib_time)); + } + } + + // We have to filter out matching entries and keep only ones that were found first by lib priority + if (assets.empty()) + { + assets = std::move(lib_fileents); + } + else + { + for (const auto &fe : lib_fileents) + { + auto it_place = std::upper_bound(assets.begin(), assets.end(), fe, FileEntryCmpByNameCI()); + if (it_place != assets.begin() && (it_place - 1)->Name.CompareNoCase(fe.Name) != 0) + assets.insert(it_place, fe); + } + } + } +} + AssetError AssetManager::RegisterAssetLib(const String &path, AssetLibEx *&out_lib) { // Test for a directory diff --git a/Common/core/assetmanager.h b/Common/core/assetmanager.h index 0fc3f98562..f1adb22c6c 100644 --- a/Common/core/assetmanager.h +++ b/Common/core/assetmanager.h @@ -40,6 +40,7 @@ #include #include #include "core/asset.h" +#include "util/directory.h" #include "util/stream.h" #include "util/string_types.h" @@ -123,11 +124,19 @@ class AssetManager // Tells whether asset exists in any of the registered search locations bool DoesAssetExist(const String &asset_name, const String &filter = "") const; inline bool DoesAssetExist(const AssetPath &apath) const { return DoesAssetExist(apath.Name, apath.Filter); } + // Tries to get asset's "file time" (last modification time). + // Note that for the assets packed within a CLIB format this will return library's time instead. + bool GetAssetTime(const String &asset_name, time_t &ft, const String &filter = "") const; // Searches in all the registered locations and collects a list of // assets using given wildcard pattern // TODO: variant accepting std::regex instead of wildcard, and replace uses where convenient void FindAssets(std::vector &assets, const String &wildcard, const String &filter = "") const; + // Searches in all the registered locations and collects a list of + // FileEntry objects corresponding to assets, using given wildcard pattern. + // NOTE: lib file assets will have their time property equal to lib's time. + void FindAssets(std::vector &assets, const String &wildcard, + const String &filter = "") const; // Open asset stream in the given work mode; returns null if asset is not found or cannot be opened // This method only searches in libraries that do not have any defined filters std::unique_ptr OpenAsset(const String &asset_name) const; diff --git a/Common/util/directory.cpp b/Common/util/directory.cpp index 307d72f65b..4282c8708b 100644 --- a/Common/util/directory.cpp +++ b/Common/util/directory.cpp @@ -129,6 +129,23 @@ void GetFiles(const String &dir_path, std::vector &files) } } +void GetFiles(const String &dir_path, std::vector &files, const String &wildcard) +{ + for (FindFile ff = FindFile::OpenFiles(dir_path, wildcard); !ff.AtEnd(); ff.Next()) + files.push_back(ff.Current()); +} + +void GetFiles(const String &dir_path, std::vector &files, const String &wildcard) +{ + for (FindFile ff = FindFile::OpenFiles(dir_path, wildcard); !ff.AtEnd(); ff.Next()) + files.push_back(ff.GetEntry()); +} + +bool HasAnyFiles(const String &dir_path) +{ + return !FindFile::OpenFiles(dir_path).AtEnd(); +} + } // namespace Directory diff --git a/Common/util/directory.h b/Common/util/directory.h index 92ce65422b..328eae803e 100644 --- a/Common/util/directory.h +++ b/Common/util/directory.h @@ -31,6 +31,22 @@ namespace AGS namespace Common { +// FileEntry describes a single entry in the filesystem. +struct FileEntry +{ + String Name; + // TODO: make flags instead? + bool IsFile = false; + bool IsDir = false; + time_t Time{}; + + FileEntry() = default; + FileEntry(const String &name, bool is_file, bool is_dir, const time_t &time) + : Name(name), IsFile(is_file), IsDir(is_dir), Time(time) {} + + operator bool() const { return !Name.IsEmpty(); } +}; + namespace Directory { // Creates new directory (if it does not exist) @@ -47,25 +63,15 @@ namespace Directory void GetDirs(const String &dir_path, std::vector &dirs); // Get list of files found in the given directory void GetFiles(const String &dir_path, std::vector &files); + // Get list of files found in the given directory using wildcard pattern + void GetFiles(const String &dir_path, std::vector &files, const String &wildcard); + // Get list of file entries in the given directory using wildcard pattern + void GetFiles(const String &dir_path, std::vector &files, const String &wildcard); + // Tells whether there are any files in the given directory + bool HasAnyFiles(const String &dir_path); } // namespace Directory -// FileEntry describes a single entry in the filesystem. -struct FileEntry -{ - String Name; - // TODO: make flags instead? - bool IsFile = false; - bool IsDir = false; - time_t Time{}; - - FileEntry() = default; - FileEntry(const String &name, bool is_file, bool is_dir, const time_t &time) - : Name(name), IsFile(is_file), IsDir(is_dir), Time(time) {} - - operator bool() const { return !Name.IsEmpty(); } -}; - // // DirectoryIterator iterates entries in the directory. // The order of iteration is undefined. @@ -202,6 +208,74 @@ class FindFile bool _doDirs = false; }; + +// +// FileEntry comparators +// +struct FileEntryEqByName +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe1.Name == fe2.Name; + } +}; + +struct FileEntryEqByNameCI +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe1.Name.CompareNoCase(fe2.Name) == 0; + } +}; + +struct FileEntryCmpByName +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe1.Name.Compare(fe2.Name) < 0; + } +}; + +struct FileEntryCmpByNameDsc +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe2.Name.Compare(fe1.Name) < 0; + } +}; + +struct FileEntryCmpByNameCI +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe1.Name.CompareNoCase(fe2.Name) < 0; + } +}; + +struct FileEntryCmpByNameDscCI +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe2.Name.CompareNoCase(fe1.Name) < 0; + } +}; + +struct FileEntryCmpByTime +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe1.Time < fe2.Time; + } +}; + +struct FileEntryCmpByTimeDsc +{ + bool operator()(const FileEntry &fe1, const FileEntry &fe2) const + { + return fe2.Time < fe1.Time; + } +}; + } // namespace Common } // namespace AGS diff --git a/Common/util/file.cpp b/Common/util/file.cpp index d9302f163d..f320b07594 100644 --- a/Common/util/file.cpp +++ b/Common/util/file.cpp @@ -73,6 +73,12 @@ soff_t File::GetFileSize(const String &filename) return size; } +time_t File::GetFileTime(const String &filename) +{ + return ags_file_time(filename.GetCStr()); + // NOTE: ANDROID's AAsset storage seems to be unapplicable here +} + bool File::TestReadFile(const String &filename) { FILE *test_file = ags_fopen(filename.GetCStr(), "rb"); diff --git a/Common/util/file.h b/Common/util/file.h index 0b029d35aa..4522647c9e 100644 --- a/Common/util/file.h +++ b/Common/util/file.h @@ -58,6 +58,8 @@ namespace File bool IsFileOrDir(const String &filename); // Returns size of a file, or -1 if no such file found soff_t GetFileSize(const String &filename); + // Returns file's last writing time, or time_t() if no such file found + time_t GetFileTime(const String &filename); // Tests if file could be opened for reading bool TestReadFile(const String &filename); // Opens a file for writing or creates new one if it does not exist; deletes file if it was created during test diff --git a/Common/util/stdio_compat.c b/Common/util/stdio_compat.c index 962e98e98d..87ed34ccb3 100644 --- a/Common/util/stdio_compat.c +++ b/Common/util/stdio_compat.c @@ -126,17 +126,17 @@ int ags_directory_exists(const char *path) int ags_path_exists(const char *path) { - #if AGS_PLATFORM_OS_WINDOWS - WCHAR wstr[MAX_PATH_SZ]; - MultiByteToWideChar(CP_UTF8, 0, path, -1, wstr, MAX_PATH_SZ); - return PathFileExistsW(wstr); - #else - struct stat path_stat; - if (stat(path, &path_stat) != 0) { - return 0; - } - return S_ISREG(path_stat.st_mode) || S_ISDIR(path_stat.st_mode); - #endif +#if AGS_PLATFORM_OS_WINDOWS + WCHAR wstr[MAX_PATH_SZ]; + MultiByteToWideChar(CP_UTF8, 0, path, -1, wstr, MAX_PATH_SZ); + return PathFileExistsW(wstr); +#else + struct stat path_stat; + if (stat(path, &path_stat) != 0) { + return 0; + } + return S_ISREG(path_stat.st_mode) || S_ISDIR(path_stat.st_mode); +#endif } file_off_t ags_file_size(const char *path) @@ -158,6 +158,25 @@ file_off_t ags_file_size(const char *path) #endif } +time_t ags_file_time(const char *path) +{ +#if AGS_PLATFORM_OS_WINDOWS + WCHAR wstr[MAX_PATH_SZ]; + MultiByteToWideChar(CP_UTF8, 0, path, -1, wstr, MAX_PATH_SZ); + struct _stat64 path_stat; + if (_wstat64(wstr, &path_stat) != 0) { + return -1; + } + return path_stat.st_mtime; +#else + struct stat path_stat; + if (stat(path, &path_stat) != 0) { + return -1; + } + return path_stat.st_mtime; +#endif +} + int ags_file_remove(const char *path) { #if AGS_PLATFORM_OS_WINDOWS diff --git a/Common/util/stdio_compat.h b/Common/util/stdio_compat.h index eea223965f..d7853baad0 100644 --- a/Common/util/stdio_compat.h +++ b/Common/util/stdio_compat.h @@ -16,6 +16,7 @@ #include #include +#include typedef int64_t file_off_t; @@ -38,6 +39,7 @@ int ags_file_exists(const char *path); int ags_directory_exists(const char *path); int ags_path_exists(const char *path); file_off_t ags_file_size(const char *path); +time_t ags_file_time(const char *path); int ags_file_remove(const char *path); int ags_file_rename(const char *src, const char *dst); diff --git a/Common/util/string_types.h b/Common/util/string_types.h index 4d3a8f836c..e04b299ad2 100644 --- a/Common/util/string_types.h +++ b/Common/util/string_types.h @@ -70,6 +70,15 @@ namespace Common // Various comparison functors // +// Test case-sensitive String equality +struct StrEq +{ + bool operator()(const String &s1, const String &s2) const + { + return s1 == s2; + } +}; + // Test case-insensitive String equality struct StrEqNoCase { diff --git a/Editor/AGS.Editor/Resources/agsdefns.sh b/Editor/AGS.Editor/Resources/agsdefns.sh index 087305f376..fe31da3562 100644 --- a/Editor/AGS.Editor/Resources/agsdefns.sh +++ b/Editor/AGS.Editor/Resources/agsdefns.sh @@ -34,7 +34,7 @@ #define AGS_MAX_CONTROLS_PER_GUI 30 #endif #define MAX_LEGACY_GLOBAL_VARS 50 -#define MAX_LISTBOX_SAVED_GAMES 50 +#define MAX_LEGACY_SAVED_GAMES 50 #define PALETTE_SIZE 256 #define FOLLOW_EXACTLY 32766 #define NARRATOR -1 @@ -515,6 +515,20 @@ enum RenderLayer eRenderLayerRoom = 0x00000008, eRenderLayerAll = 0xFFFFFFFF }; + +enum FileSortStyle +{ + eFileSort_None = 0, + eFileSort_Name = 1, + eFileSort_Time = 2 +}; + +enum SortDirection +{ + eSortNoDirection = 0, + eSortAscending = 1, + eSortDescending = 2 +}; #endif @@ -819,15 +833,19 @@ import const string GetTranslation (const string originalText); /// Checks if a translation is currently in use. import int IsTranslationAvailable (); /// Displays the default built-in Restore Game dialog. -import void RestoreGameDialog(); +import void RestoreGameDialog(int min_slot = 1, int max_slot = 100); /// Displays the default built-in Save Game dialog. -import void SaveGameDialog(); +import void SaveGameDialog(int min_slot = 1, int max_slot = 100); /// Restarts the game from the restart point. import void RestartGame(); /// Saves the current game position to the specified slot. import void SaveGameSlot(int slot, const string description, int sprite = -1); /// Restores the game saved to the specified game slot. import void RestoreGameSlot(int slot); +#ifdef SCRIPT_API_v362 +/// Moves the save game from one slot to another, overwriting a save if one was already present at a new slot. +import void MoveSaveSlot(int old_slot, int new_slot); +#endif /// Deletes the specified save game. import void DeleteSaveSlot(int slot); /// Sets this as the point at which the game will be restarted. @@ -1274,6 +1292,8 @@ enum FileSeek { }; #endif +builtin managed struct DateTime; + builtin managed struct File { /// Delets the specified file from the disk. import static bool Delete(const string filename); // $AUTOCOMPLETESTATICONLY$ @@ -1324,6 +1344,10 @@ builtin managed struct File { import static String ResolvePath(const string filename); // $AUTOCOMPLETESTATICONLY$ /// Gets the path to opened file. readonly import attribute String Path; +#endif +#ifdef SCRIPT_API_v362 + /// Retrieves specified file's last write time; returns null if file does not exist + import static DateTime* GetFileTime(const string filename); // $AUTOCOMPLETESTATICONLY$ #endif int reserved[2]; // $AUTOCOMPLETEIGNORE$ }; @@ -1915,9 +1939,9 @@ builtin managed struct ListBox extends GUIControl { /// Removes all the items from the list. import void Clear(); /// Fills the list box with all the filenames that match the specified file mask. - import void FillDirList(const string fileMask); - /// Fills the list box with all the current user's saved games. - import int FillSaveGameList(); + import void FillDirList(const string fileMask, FileSortStyle fileSortStyle = eFileSort_Name, SortDirection sortDirection = eSortAscending); + /// Fills the list box with the current user's saved games in the given range of slots. + import int FillSaveGameList(int min_slot = 1, int max_slot = 100); /// Gets the item index at the specified screen co-ordinates, if they lie within the list box. import int GetItemAtLocation(int x, int y); #ifndef STRICT_STRINGS @@ -3041,6 +3065,10 @@ builtin struct Game { /// Preloads and caches sprites and linked sounds for a view, within a selected range of loops. import static void PrecacheView(int view, int first_loop, int last_loop); #endif +#ifdef SCRIPT_API_v362 + /// Gets the write time of the specified save game slot. + import static DateTime* GetSaveSlotTime(int saveSlot); +#endif }; builtin struct GameState { @@ -3304,12 +3332,13 @@ import Hotspot hotspot[AGS_MAX_HOTSPOTS]; import Region region[AGS_MAX_REGIONS]; import int gs_globals[MAX_LEGACY_GLOBAL_VARS]; -import short savegameindex[MAX_LISTBOX_SAVED_GAMES]; +import short savegameindex[MAX_LEGACY_SAVED_GAMES]; import ColorType palette[PALETTE_SIZE]; -#ifndef SCRIPT_API_v330 #undef MAX_LEGACY_GLOBAL_VARS -#undef MAX_LISTBOX_SAVED_GAMES +#undef MAX_LEGACY_SAVED_GAMES + +#ifndef SCRIPT_API_v330 #undef PALETTE_SIZE #endif diff --git a/Engine/ac/dynobj/scriptdatetime.cpp b/Engine/ac/dynobj/scriptdatetime.cpp index 6484992033..567af544e6 100644 --- a/Engine/ac/dynobj/scriptdatetime.cpp +++ b/Engine/ac/dynobj/scriptdatetime.cpp @@ -27,6 +27,20 @@ const char *ScriptDateTime::GetType() { return "DateTime"; } +void ScriptDateTime::SetFromStdTime(time_t time) +{ + // NOTE: subject to year 2038 problem due to shoving time_t in an integer + rawUnixTime = static_cast(time); + + struct tm *newtime = localtime(&time); + hour = newtime->tm_hour; + minute = newtime->tm_min; + second = newtime->tm_sec; + day = newtime->tm_mday; + month = newtime->tm_mon + 1; + year = newtime->tm_year + 1900; +} + size_t ScriptDateTime::CalcSerializeSize(const void* /*address*/) { return sizeof(int32_t) * 7; diff --git a/Engine/ac/dynobj/scriptdatetime.h b/Engine/ac/dynobj/scriptdatetime.h index 9a408f4617..7ec586f6c9 100644 --- a/Engine/ac/dynobj/scriptdatetime.h +++ b/Engine/ac/dynobj/scriptdatetime.h @@ -18,6 +18,7 @@ #ifndef __AGS_EE_DYNOBJ__SCRIPTDATETIME_H #define __AGS_EE_DYNOBJ__SCRIPTDATETIME_H +#include #include "ac/dynobj/cc_agsdynamicobject.h" struct ScriptDateTime final : AGSCCDynamicObject { @@ -29,6 +30,8 @@ struct ScriptDateTime final : AGSCCDynamicObject { const char *GetType() override; void Unserialize(int index, AGS::Common::Stream *in, size_t data_sz) override; + void SetFromStdTime(time_t time); + ScriptDateTime(); protected: diff --git a/Engine/ac/file.cpp b/Engine/ac/file.cpp index 12eb843fa7..1fb35cc045 100644 --- a/Engine/ac/file.cpp +++ b/Engine/ac/file.cpp @@ -54,6 +54,28 @@ int File_Exists(const char *fnmm) { return 1; // was found in fs } +ScriptDateTime* File_GetFileTime(const char *fnmm) { + const auto rp = ResolveScriptPathAndFindFile(fnmm, true); + if (!rp) + return nullptr; + + time_t ft; + if (rp.AssetMgr) + { + if (!AssetMgr->GetAssetTime(rp.FullPath, ft, "*")) + return nullptr; + } + else + { + ft = File::GetFileTime(rp.FullPath); + } + + ScriptDateTime *sdt = new ScriptDateTime(); + sdt->SetFromStdTime(ft); + ccRegisterManagedObject(sdt, sdt); + return sdt; +} + int File_Delete(const char *fnmm) { const auto rp = ResolveScriptPathAndFindFile(fnmm, false); if (!rp) @@ -763,6 +785,11 @@ RuntimeScriptValue Sc_File_Exists(const RuntimeScriptValue *params, int32_t para API_SCALL_INT_POBJ(File_Exists, const char); } +RuntimeScriptValue Sc_File_GetFileTime(const RuntimeScriptValue *params, int32_t param_count) +{ + API_SCALL_OBJAUTO_POBJ(ScriptDateTime, File_GetFileTime, const char); +} + // void *(const char *fnmm, int mode) RuntimeScriptValue Sc_sc_OpenFile(const RuntimeScriptValue *params, int32_t param_count) { @@ -884,6 +911,7 @@ void RegisterFileAPI() ScFnRegister file_api[] = { { "File::Delete^1", API_FN_PAIR(File_Delete) }, { "File::Exists^1", API_FN_PAIR(File_Exists) }, + { "File::GetFileTime^1", API_FN_PAIR(File_GetFileTime) }, { "File::Open^2", API_FN_PAIR(sc_OpenFile) }, { "File::ResolvePath^1", API_FN_PAIR(File_ResolvePath) }, diff --git a/Engine/ac/game.cpp b/Engine/ac/game.cpp index aee85d496e..f8c67a70ef 100644 --- a/Engine/ac/game.cpp +++ b/Engine/ac/game.cpp @@ -417,48 +417,75 @@ const char* Game_GetSaveSlotDescription(int slnum) { return nullptr; } +ScriptDateTime* Game_GetSaveSlotTime(int slnum) +{ + time_t ft = File::GetFileTime(get_save_game_path(slnum)); + ScriptDateTime *sdt = new ScriptDateTime(); + sdt->SetFromStdTime(ft); + ccRegisterManagedObject(sdt, sdt); + return sdt; +} + +void restore_game_dialog() +{ + restore_game_dialog2(1, LEGACY_TOP_BUILTINDIALOGSAVESLOT); +} + +void restore_game_dialog2(int min_slot, int max_slot) +{ + // Optionally override the max slot + max_slot = usetup.max_save_slot > 0 ? usetup.max_save_slot : max_slot; -void restore_game_dialog() { can_run_delayed_command(); if (thisroom.Options.SaveLoadDisabled == 1) { DisplayMessage(983); return; } if (inside_script) { - get_executingscript()->QueueAction(PostScriptAction(ePSARestoreGameDialog, 0, "RestoreGameDialog")); + get_executingscript()->QueueAction(PostScriptAction(ePSARestoreGameDialog, (min_slot & 0xFFFF) | (max_slot & 0xFFFF) << 16, "RestoreGameDialog")); return; } - do_restore_game_dialog(); + do_restore_game_dialog(min_slot, max_slot); } -bool do_restore_game_dialog() { +bool do_restore_game_dialog(int min_slot, int max_slot) +{ setup_for_dialog(); - int toload = loadgamedialog(); + int toload = loadgamedialog(min_slot, max_slot); restore_after_dialog(); if (toload >= 0) try_restore_save(toload); return toload >= 0; } -void save_game_dialog() { +void save_game_dialog() +{ + save_game_dialog2(1, LEGACY_TOP_BUILTINDIALOGSAVESLOT); +} + +void save_game_dialog2(int min_slot, int max_slot) +{ + // Optionally override the max slot + max_slot = usetup.max_save_slot > 0 ? usetup.max_save_slot : max_slot; + if (thisroom.Options.SaveLoadDisabled == 1) { DisplayMessage(983); return; } if (inside_script) { - get_executingscript()->QueueAction(PostScriptAction(ePSASaveGameDialog, 0, "SaveGameDialog")); + get_executingscript()->QueueAction(PostScriptAction(ePSASaveGameDialog, (min_slot & 0xFFFF) | (max_slot & 0xFFFF) << 16, "SaveGameDialog")); return; } - do_save_game_dialog(); + do_save_game_dialog(min_slot, max_slot); } -bool do_save_game_dialog() { +bool do_save_game_dialog(int min_slot, int max_slot) { setup_for_dialog(); - int toload = savegamedialog(); + int tosave = savegamedialog(min_slot, max_slot); restore_after_dialog(); - if (toload >= 0) - save_game(toload, get_gui_dialog_buffer()); - return toload >= 0; + if (tosave >= 0) + save_game(tosave, get_gui_dialog_buffer()); + return tosave >= 0; } void free_do_once_tokens() @@ -1601,6 +1628,11 @@ RuntimeScriptValue Sc_Game_GetSaveSlotDescription(const RuntimeScriptValue *para API_SCALL_OBJ_PINT(const char, myScriptStringImpl, Game_GetSaveSlotDescription); } +RuntimeScriptValue Sc_Game_GetSaveSlotTime(const RuntimeScriptValue *params, int32_t param_count) +{ + API_SCALL_OBJAUTO_PINT(ScriptDateTime, Game_GetSaveSlotTime); +} + // ScriptViewFrame* (int viewNumber, int loopNumber, int frame) RuntimeScriptValue Sc_Game_GetViewFrame(const RuntimeScriptValue *params, int32_t param_count) { @@ -1875,6 +1907,7 @@ void RegisterGameAPI() { "Game::GetMODPattern^0", API_FN_PAIR(Game_GetMODPattern) }, { "Game::GetRunNextSettingForLoop^2", API_FN_PAIR(Game_GetRunNextSettingForLoop) }, { "Game::GetSaveSlotDescription^1", API_FN_PAIR(Game_GetSaveSlotDescription) }, + { "Game::GetSaveSlotTime^1", API_FN_PAIR(Game_GetSaveSlotTime) }, { "Game::GetViewFrame^3", API_FN_PAIR(Game_GetViewFrame) }, { "Game::InputBox^1", API_FN_PAIR(Game_InputBox) }, { "Game::SetSaveGameDirectory^1", API_FN_PAIR(Game_SetSaveGameDirectory) }, diff --git a/Engine/ac/game.h b/Engine/ac/game.h index 0bd7e5f799..92d882fef6 100644 --- a/Engine/ac/game.h +++ b/Engine/ac/game.h @@ -152,13 +152,15 @@ bool get_save_slotnum(const Common::String &filename, int &slot); // Try calling built-in restore game dialog; // NOTE: this is a script command; may be aborted according to the game & room settings void restore_game_dialog(); +void restore_game_dialog2(int min_slot, int max_slot); // Unconditionally display a built-in restore game dialog -bool do_restore_game_dialog(); +bool do_restore_game_dialog(int min_slot, int max_slot); // Try calling built-in save game dialog; // NOTE: this is a script command; may be aborted according to the game & room settings void save_game_dialog(); +void save_game_dialog2(int min_slot, int max_slot); // Unconditionally display a built-in save game dialog -bool do_save_game_dialog(); +bool do_save_game_dialog(int min_slot, int max_slot); void free_do_once_tokens(); // Free all the memory associated with the game void unload_game(); diff --git a/Engine/ac/gamesetup.h b/Engine/ac/gamesetup.h index ea548f950a..3faefc6774 100644 --- a/Engine/ac/gamesetup.h +++ b/Engine/ac/gamesetup.h @@ -121,6 +121,8 @@ struct GameSetup // is not implemented (or does not work correctly). int key_save_game = 0; int key_restore_game = 0; + // Optional override for the max save slot + int max_save_slot = 0; // Accessibility settings and overrides; // these are meant to make playing the game easier, by modifying certain diff --git a/Engine/ac/gamestate.cpp b/Engine/ac/gamestate.cpp index e2ffbac988..3f328b0c4d 100644 --- a/Engine/ac/gamestate.cpp +++ b/Engine/ac/gamestate.cpp @@ -582,7 +582,7 @@ void GamePlayState::ReadFromSavegame(Stream *in, GameDataVersion data_ver, GameS in->ReadArrayOfInt16( parsed_words, MAX_PARSED_WORDS); in->Read( bad_parsed_word, 100); raw_color = in->ReadInt32(); - in->ReadArrayOfInt16( filenumbers, MAXSAVEGAMES); + in->ReadArrayOfInt16( filenumbers, LEGACY_MAXSAVEGAMES); mouse_cursor_hidden = in->ReadInt32(); silent_midi = in->ReadInt32(); silent_midi_channel = in->ReadInt32(); @@ -772,7 +772,7 @@ void GamePlayState::WriteForSavegame(Stream *out) const out->WriteArrayOfInt16( parsed_words, MAX_PARSED_WORDS); out->Write( bad_parsed_word, 100); out->WriteInt32( raw_color); - out->WriteArrayOfInt16( filenumbers, MAXSAVEGAMES); + out->WriteArrayOfInt16( filenumbers, LEGACY_MAXSAVEGAMES); out->WriteInt32( mouse_cursor_hidden); out->WriteInt32( silent_midi); out->WriteInt32( silent_midi_channel); diff --git a/Engine/ac/gamestate.h b/Engine/ac/gamestate.h index d2e459a52e..e8301f2c30 100644 --- a/Engine/ac/gamestate.h +++ b/Engine/ac/gamestate.h @@ -227,7 +227,7 @@ struct GamePlayState int raw_color = 0; int raw_modified[MAX_ROOM_BGFRAMES]{}; Common::PBitmap raw_drawing_surface; - short filenumbers[MAXSAVEGAMES]{}; + int16_t filenumbers[LEGACY_MAXSAVEGAMES]{}; int room_changes = 0; int mouse_cursor_hidden = 0; int silent_midi = 0; diff --git a/Engine/ac/global_api.cpp b/Engine/ac/global_api.cpp index 6af6801194..ea4cdf1da5 100644 --- a/Engine/ac/global_api.cpp +++ b/Engine/ac/global_api.cpp @@ -1140,6 +1140,11 @@ RuntimeScriptValue Sc_MoveOverlay(const RuntimeScriptValue *params, int32_t para API_SCALL_VOID_PINT3(MoveOverlay); } +RuntimeScriptValue Sc_MoveSaveSlot(const RuntimeScriptValue *params, int32_t param_count) +{ + API_SCALL_VOID_PINT2(MoveSaveSlot); +} + // void (int charid) RuntimeScriptValue Sc_MoveToWalkableArea(const RuntimeScriptValue *params, int32_t param_count) { @@ -1404,6 +1409,11 @@ RuntimeScriptValue Sc_restore_game_dialog(const RuntimeScriptValue *params, int3 API_SCALL_VOID(restore_game_dialog); } +RuntimeScriptValue Sc_restore_game_dialog2(const RuntimeScriptValue *params, int32_t param_count) +{ + API_SCALL_VOID_PINT2(restore_game_dialog2); +} + // void (int slnum) RuntimeScriptValue Sc_RestoreGameSlot(const RuntimeScriptValue *params, int32_t param_count) { @@ -1474,6 +1484,11 @@ RuntimeScriptValue Sc_save_game_dialog(const RuntimeScriptValue *params, int32_t API_SCALL_VOID(save_game_dialog); } +RuntimeScriptValue Sc_save_game_dialog2(const RuntimeScriptValue *params, int32_t param_count) +{ + API_SCALL_VOID_PINT2(save_game_dialog2); +} + RuntimeScriptValue Sc_SaveGameSlot(const RuntimeScriptValue *params, int32_t param_count) { API_SCALL_VOID_PINT_POBJ_PINT(SaveGameSlot, const char); @@ -2498,6 +2513,7 @@ void RegisterGlobalAPI(ScriptAPIVersion base_api, ScriptAPIVersion /*compat_api* { "MoveObject", API_FN_PAIR(MoveObject) }, { "MoveObjectDirect", API_FN_PAIR(MoveObjectDirect) }, { "MoveOverlay", API_FN_PAIR(MoveOverlay) }, + { "MoveSaveSlot", API_FN_PAIR(MoveSaveSlot) }, { "MoveToWalkableArea", API_FN_PAIR(MoveToWalkableArea) }, { "NewRoom", API_FN_PAIR(NewRoom) }, { "NewRoomEx", API_FN_PAIR(NewRoomEx) }, @@ -2542,7 +2558,6 @@ void RegisterGlobalAPI(ScriptAPIVersion base_api, ScriptAPIVersion /*compat_api* { "RemoveWalkableArea", API_FN_PAIR(RemoveWalkableArea) }, { "ResetRoom", API_FN_PAIR(ResetRoom) }, { "RestartGame", API_FN_PAIR(restart_game) }, - { "RestoreGameDialog", API_FN_PAIR(restore_game_dialog) }, { "RestoreGameSlot", API_FN_PAIR(RestoreGameSlot) }, { "RestoreWalkableArea", API_FN_PAIR(RestoreWalkableArea) }, { "RunAGSGame", API_FN_PAIR(RunAGSGame) }, @@ -2555,7 +2570,6 @@ void RegisterGlobalAPI(ScriptAPIVersion base_api, ScriptAPIVersion /*compat_api* { "Said", API_FN_PAIR(Said) }, { "SaidUnknownWord", API_FN_PAIR(SaidUnknownWord) }, { "SaveCursorForLocationChange", API_FN_PAIR(SaveCursorForLocationChange) }, - { "SaveGameDialog", API_FN_PAIR(save_game_dialog) }, { "SaveScreenShot", API_FN_PAIR(SaveScreenShot) }, { "SeekMIDIPosition", API_FN_PAIR(SeekMIDIPosition) }, { "SeekMODPattern", API_FN_PAIR(SeekMODPattern) }, @@ -2686,12 +2700,27 @@ void RegisterGlobalAPI(ScriptAPIVersion base_api, ScriptAPIVersion /*compat_api* { "SkipWait", API_FN_PAIR(SkipWait) }, }; + ccAddExternalFunctions(global_api); + // Few functions have to be selected based on API level, - // because historically AGS compiler did not generate "number of args" in the import name. + // We have to do this because AGS compiler did not generate + // "name^argnum" symbol id for non-member functions for some reason.... if (base_api < kScriptAPI_v362) - ccAddExternalStaticFunction("SaveGameSlot", API_FN_PAIR(SaveGameSlot2)); + { + ScFnRegister global_api_dlgs[] = { + { "SaveGameSlot", API_FN_PAIR(SaveGameSlot2) }, + { "RestoreGameDialog", API_FN_PAIR(restore_game_dialog) }, + { "SaveGameDialog", API_FN_PAIR(save_game_dialog) }, + }; + ccAddExternalFunctions(global_api_dlgs); + } else - ccAddExternalStaticFunction("SaveGameSlot", API_FN_PAIR(SaveGameSlot)); - - ccAddExternalFunctions(global_api); + { + ScFnRegister global_api_dlgs[] = { + { "SaveGameSlot", API_FN_PAIR(SaveGameSlot) }, + { "RestoreGameDialog", API_FN_PAIR(restore_game_dialog2) }, + { "SaveGameDialog", API_FN_PAIR(save_game_dialog2) }, + }; + ccAddExternalFunctions(global_api_dlgs); + } } diff --git a/Engine/ac/global_game.cpp b/Engine/ac/global_game.cpp index 94bfe1e5ce..23e8ddfe7a 100644 --- a/Engine/ac/global_game.cpp +++ b/Engine/ac/global_game.cpp @@ -12,6 +12,7 @@ // //============================================================================= #include "ac/global_game.h" +#include #include #include #include "core/platform.h" @@ -110,7 +111,15 @@ void restart_game() { try_restore_save(RESTART_POINT_SAVE_GAME_NUMBER); } -void RestoreGameSlot(int slnum) { +void MoveSaveSlot(int old_save, int new_save) +{ + String old_filename = get_save_game_path(old_save); + String new_filename = get_save_game_path(new_save); + File::RenameFile(old_filename, new_filename); +} + +void RestoreGameSlot(int slnum) +{ if (displayed_room < 0) quit("!RestoreGameSlot: a game cannot be restored from within game_start"); @@ -155,21 +164,28 @@ void SaveGameSlot2(int slnum, const char *descript) SaveGameSlot(slnum, descript, -1); } -void DeleteSaveSlot (int slnum) { - String nametouse; - nametouse = get_save_game_path(slnum); - File::DeleteFile(nametouse); - if ((slnum >= 1) && (slnum <= MAXSAVEGAMES)) { - String thisname; - for (int i = MAXSAVEGAMES; i > slnum; i--) { - thisname = get_save_game_path(i); - if (Common::File::IsFile(thisname)) { - // Rename the highest save game to fill in the gap - File::RenameFile(thisname, nametouse); - break; +void DeleteSaveSlot(int slnum) +{ + String save_filename = get_save_game_path(slnum); + File::DeleteFile(save_filename); + + // Pre-3.6.2 engine behavior: if the deleted save slot was from within + // MAXSAVEGAMES range, then move the topmost found save file from the same + // range to the freed slot index. + if (loaded_game_file_version < kGameVersion_362) + { + if ((slnum >= 1) && (slnum <= LEGACY_MAXSAVEGAMES)) + { + for (int i = LEGACY_MAXSAVEGAMES; i > slnum; i--) + { + String top_filename = get_save_game_path(i); + if (File::IsFile(top_filename)) + { + File::RenameFile(top_filename, save_filename); + break; + } } } - } } @@ -229,7 +245,7 @@ int LoadSaveSlotScreenshot(int slnum, int width, int height) { return add_dynamic_sprite(std::move(screenshot)); } -void FillSaveList(std::vector &saves, unsigned top_index, size_t max_count) +void FillSaveList(std::vector &saves, unsigned bot_index, unsigned top_index, size_t max_count) { if (max_count == 0) return; // duh @@ -237,6 +253,8 @@ void FillSaveList(std::vector &saves, unsigned top_index, size_t m String svg_dir = get_save_game_directory(); String svg_suff = get_save_game_suffix(); String pattern = String::FromFormat("agssave.???%s", svg_suff.GetCStr()); + bot_index = std::min(999u, bot_index); // NOTE: slots are limited by 0..999 range + top_index = std::min(999u, top_index); for (FindFile ff = FindFile::OpenFiles(svg_dir, pattern); !ff.AtEnd(); ff.Next()) { @@ -244,8 +262,9 @@ void FillSaveList(std::vector &saves, unsigned top_index, size_t m if (!svg_suff.IsEmpty()) slotname.ClipRight(svg_suff.GetLength()); int saveGameSlot = Path::GetFileExtension(slotname).ToInt(); - // only list games .000 to .XXX (to allow higher slots for other perposes) - if (saveGameSlot < 0 || static_cast(saveGameSlot) > top_index) + // only list games between .XXX to .YYY (to allow hidden slots for special perposes) + if (saveGameSlot < 0 || static_cast(saveGameSlot) < bot_index + || static_cast(saveGameSlot) > top_index) continue; String description; GetSaveSlotDescription(saveGameSlot, description); @@ -258,7 +277,7 @@ void FillSaveList(std::vector &saves, unsigned top_index, size_t m int GetLastSaveSlot() { std::vector saves; - FillSaveList(saves, RESTART_POINT_SAVE_GAME_NUMBER - 1); + FillSaveList(saves, 0, RESTART_POINT_SAVE_GAME_NUMBER - 1); if (saves.size() == 0) return -1; std::sort(saves.rbegin(), saves.rend()); diff --git a/Engine/ac/global_game.h b/Engine/ac/global_game.h index 5fb89fce1a..93af042829 100644 --- a/Engine/ac/global_game.h +++ b/Engine/ac/global_game.h @@ -42,13 +42,14 @@ struct SaveListItem void AbortGame(); void GiveScore(int amnt); void restart_game(); +void MoveSaveSlot(int old_slot, int new_slot); void RestoreGameSlot(int slnum); void SaveGameSlot(int slnum, const char *descript, int spritenum); void SaveGameSlot2(int slnum, const char *descript); void DeleteSaveSlot (int slnum); int GetSaveSlotDescription(int slnum,char*desbuf); int LoadSaveSlotScreenshot(int slnum, int width, int height); -void FillSaveList(std::vector &saves, unsigned top_index, size_t max_count = -1); +void FillSaveList(std::vector &saves, unsigned bot_index, unsigned top_index, size_t max_count = -1); // Find the latest save slot, returns the slot index or -1 at failure int GetLastSaveSlot(); void PauseGame(); diff --git a/Engine/ac/listbox.cpp b/Engine/ac/listbox.cpp index 106d225237..94abf967a6 100644 --- a/Engine/ac/listbox.cpp +++ b/Engine/ac/listbox.cpp @@ -17,6 +17,7 @@ #include // find files #include "ac/common.h" #include "ac/game.h" +#include "ac/gamesetup.h" #include "ac/gamesetupstruct.h" #include "ac/gamestate.h" #include "ac/global_game.h" @@ -50,43 +51,108 @@ void ListBox_Clear(GUIListBox *listbox) { listbox->Clear(); } -void FillDirList(std::vector &files, const FSLocation &loc, const String &pattern) +static void FillDirList(std::vector &files, const FSLocation &loc, const String &pattern) { // Do ci search for the location, as parts of the path may have case mismatch String path = File::FindFileCI(loc.BaseDir, loc.SubDir, true); if (path.IsEmpty()) return; - for (FindFile ff = FindFile::OpenFiles(path, pattern); !ff.AtEnd(); ff.Next()) - files.push_back(ff.Current()); + Directory::GetFiles(path, files, pattern); } -void ListBox_FillDirList(GUIListBox *listbox, const char *filemask) { - listbox->Clear(); +static void FillDirList(std::vector &files, const String &pattern, ScriptFileSortStyle file_sort, bool ascending) +{ + ResolvedPath rp, alt_rp; + if (!ResolveScriptPath(pattern, true, rp, alt_rp)) + return; - ResolvedPath rp, alt_rp; - if (!ResolveScriptPath(filemask, true, rp, alt_rp)) - return; + if (file_sort == kScFileSort_None) + ascending = true; - std::vector files; - if (rp.AssetMgr) - { - AssetMgr->FindAssets(files, rp.FullPath, "*"); - } - else - { - FillDirList(files, rp.Loc, Path::GetFilename(rp.FullPath)); - if (alt_rp) - FillDirList(files, alt_rp.Loc, Path::GetFilename(alt_rp.FullPath)); - // Sort and remove duplicates - std::sort(files.begin(), files.end(), StrLessNoCase()); - files.erase(std::unique(files.begin(), files.end(), StrEqNoCase()), files.end()); - } + std::vector fileents; + if (rp.AssetMgr) + { + AssetMgr->FindAssets(fileents, rp.FullPath, "*"); + } + else + { + FillDirList(fileents, rp.Loc, Path::GetFilename(rp.FullPath)); + if (alt_rp) + { + // Files from rp override alt_rp, so make certain we don't add matching files + if (fileents.empty()) + { + FillDirList(fileents, alt_rp.Loc, Path::GetFilename(alt_rp.FullPath)); + } + else + { + std::vector fileents_alt; + FillDirList(fileents_alt, alt_rp.Loc, Path::GetFilename(alt_rp.FullPath)); + std::sort(fileents.begin(), fileents.end(), FileEntryCmpByNameCI()); + // TODO: following algorithm pushes element if not matching any existing; + // pick this out as a common algorithm somewhere? + size_t src_size = fileents.size(); + for (const auto &alt_fe : fileents_alt) + { + if (std::binary_search(fileents.begin(), fileents.begin() + src_size, alt_fe, FileEntryEqByNameCI())) + continue; + fileents.push_back(alt_fe); + } + } + } + } - // TODO: method for adding item batch to speed up update - for (auto it = files.cbegin(); it != files.cend(); ++it) - { - listbox->AddItem(*it); - } + switch (file_sort) + { + case kScFileSort_Name: + if (ascending) + std::sort(fileents.begin(), fileents.end(), FileEntryCmpByNameCI()); + else + std::sort(fileents.begin(), fileents.end(), FileEntryCmpByNameDscCI()); + break; + case kScFileSort_Time: + if (ascending) + std::sort(fileents.begin(), fileents.end(), FileEntryCmpByTime()); + else + std::sort(fileents.begin(), fileents.end(), FileEntryCmpByTimeDsc()); + break; + default: break; + } + + for (const auto &fe : fileents) + { + files.push_back(fe.Name); + } +} + +void ListBox_FillDirList3(GUIListBox *listbox, const char *filemask, int file_sort, int sort_direction) +{ + if (file_sort < kScFileSort_None || file_sort > kScFileSort_Time) + { + debug_script_warn("ListBox.FillDirList: invalid file sort style (%d)", file_sort); + file_sort = kScFileSort_None; + } + if (sort_direction < kScSortNone || sort_direction > kScSortDescending) + { + debug_script_warn("ListBox.FillDirList: invalid sorting direction (%d)", sort_direction); + sort_direction = kScSortNone; + } + + listbox->Clear(); + + std::vector files; + FillDirList(files, filemask, (ScriptFileSortStyle)file_sort, sort_direction == kScSortAscending); + + // TODO: method for adding item batch to speed up update + for (auto it = files.cbegin(); it != files.cend(); ++it) + { + listbox->AddItem(*it); + } +} + +void ListBox_FillDirList(GUIListBox *listbox, const char *filemask) +{ + ListBox_FillDirList3(listbox, filemask, kScFileSort_Name, kScSortAscending); } int ListBox_GetSaveGameSlots(GUIListBox *listbox, int index) { @@ -96,11 +162,17 @@ int ListBox_GetSaveGameSlots(GUIListBox *listbox, int index) { return listbox->SavedGameIndex[index]; } -int ListBox_FillSaveGameList(GUIListBox *listbox) { - // TODO: find out if limiting to MAXSAVEGAMES is still necessary here +int ListBox_FillSaveGameList2(GUIListBox *listbox, int min_slot, int max_slot) +{ + // Optionally override the max slot + max_slot = usetup.max_save_slot > 0 ? usetup.max_save_slot : max_slot; + + max_slot = std::min(max_slot, TOP_SAVESLOT); + min_slot = std::min(max_slot, std::max(0, min_slot)); + std::vector saves; - FillSaveList(saves, TOP_LISTEDSAVESLOT, MAXSAVEGAMES); - std::sort(saves.rbegin(), saves.rend()); + FillSaveList(saves, min_slot, max_slot); + std::sort(saves.rbegin(), saves.rend()); // sort by modified time in reverse // fill in the list box listbox->Clear(); @@ -112,16 +184,20 @@ int ListBox_FillSaveGameList(GUIListBox *listbox) { } // update the global savegameindex[] array for backward compatibilty - for (size_t n = 0; n < saves.size(); ++n) + for (size_t n = 0; n < LEGACY_MAXSAVEGAMES && n < saves.size(); ++n) { play.filenumbers[n] = saves[n].Slot; } listbox->SetSvgIndex(true); - if (saves.size() >= MAXSAVEGAMES) - return 1; - return 0; + // Returns TRUE if the whole range of slots is occupied + return saves.size() > static_cast(max_slot - min_slot); +} + +int ListBox_FillSaveGameList(GUIListBox *listbox) +{ + return ListBox_FillSaveGameList2(listbox, 1, LEGACY_TOP_LISTEDSAVESLOT); } int ListBox_GetItemAtLocation(GUIListBox *listbox, int x, int y) { @@ -366,12 +442,21 @@ RuntimeScriptValue Sc_ListBox_FillDirList(void *self, const RuntimeScriptValue * API_OBJCALL_VOID_POBJ(GUIListBox, ListBox_FillDirList, const char); } -// int (GUIListBox *listbox) +RuntimeScriptValue Sc_ListBox_FillDirList3(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_VOID_POBJ_PINT2(GUIListBox, ListBox_FillDirList3, const char); +} + RuntimeScriptValue Sc_ListBox_FillSaveGameList(void *self, const RuntimeScriptValue *params, int32_t param_count) { API_OBJCALL_INT(GUIListBox, ListBox_FillSaveGameList); } +RuntimeScriptValue Sc_ListBox_FillSaveGameList2(void *self, const RuntimeScriptValue *params, int32_t param_count) +{ + API_OBJCALL_INT_PINT2(GUIListBox, ListBox_FillSaveGameList2); +} + // int (GUIListBox *listbox, int x, int y) RuntimeScriptValue Sc_ListBox_GetItemAtLocation(void *self, const RuntimeScriptValue *params, int32_t param_count) { @@ -570,7 +655,9 @@ void RegisterListBoxAPI() { "ListBox::AddItem^1", API_FN_PAIR(ListBox_AddItem) }, { "ListBox::Clear^0", API_FN_PAIR(ListBox_Clear) }, { "ListBox::FillDirList^1", API_FN_PAIR(ListBox_FillDirList) }, + { "ListBox::FillDirList^3", API_FN_PAIR(ListBox_FillDirList3) }, { "ListBox::FillSaveGameList^0", API_FN_PAIR(ListBox_FillSaveGameList) }, + { "ListBox::FillSaveGameList^2", API_FN_PAIR(ListBox_FillSaveGameList2) }, { "ListBox::GetItemAtLocation^2", API_FN_PAIR(ListBox_GetItemAtLocation) }, { "ListBox::GetItemText^2", API_FN_PAIR(ListBox_GetItemText) }, { "ListBox::InsertItemAt^2", API_FN_PAIR(ListBox_InsertItemAt) }, diff --git a/Engine/ac/listbox.h b/Engine/ac/listbox.h index 0c28f0300d..9340c78347 100644 --- a/Engine/ac/listbox.h +++ b/Engine/ac/listbox.h @@ -28,6 +28,7 @@ void ListBox_Clear(GUIListBox *listbox); void ListBox_FillDirList(GUIListBox *listbox, const char *filemask); int ListBox_GetSaveGameSlots(GUIListBox *listbox, int index); int ListBox_FillSaveGameList(GUIListBox *listbox); +int ListBox_FillSaveGameList2(GUIListBox *listbox, int min_slot, int max_slot); int ListBox_GetItemAtLocation(GUIListBox *listbox, int x, int y); char *ListBox_GetItemText(GUIListBox *listbox, int index, char *buffer); const char* ListBox_GetItems(GUIListBox *listbox, int index); diff --git a/Engine/ac/runtime_defines.h b/Engine/ac/runtime_defines.h index 4eaecaf7ff..986bf54b44 100644 --- a/Engine/ac/runtime_defines.h +++ b/Engine/ac/runtime_defines.h @@ -37,12 +37,7 @@ #define DIALOG_NEWTOPIC 12000 #define MAX_TIMERS 21 #define MAX_PARSED_WORDS 15 -// how many saves may be listed at once -#define MAXSAVEGAMES 50 -// topmost save index to be listed with a FillSaveGameList command -// NOTE: changing this may theoretically affect older games which -// use slots > 99 for special purposes! -#define TOP_LISTEDSAVESLOT 99 + #define MAX_QUEUED_MUSIC 10 #define GLED_INTERACTION 1 #define GLED_EFFECTS 2 @@ -159,6 +154,31 @@ const int LegacyRoomVolumeFactor = 30; #define MAX_DYNAMIC_SURFACES 20 +// legacy max number of saves which may be filled into the ListBox +#define LEGACY_MAXSAVEGAMES 50 +// topmost save index to be listed with a FillSaveGameList command +#define LEGACY_TOP_LISTEDSAVESLOT 50 +// topmost save index to be listed with a Save/RestoreGameDialog command +#define LEGACY_TOP_BUILTINDIALOGSAVESLOT 20 +// topmost supported save slot index +#define TOP_SAVESLOT 999 +// save slot reserved for the "restart point" #define RESTART_POINT_SAVE_GAME_NUMBER 999 +// Script API SortDirection +enum ScriptSortDirection +{ + kScSortNone = 0, + kScSortAscending = 1, + kScSortDescending = 2, +}; + +// Script API FileSortStyle +enum ScriptFileSortStyle +{ + kScFileSort_None = 0, // undefined order + kScFileSort_Name = 1, // by file name + kScFileSort_Time = 2, // by last write time +}; + #endif // __AC_RUNTIMEDEFINES_H diff --git a/Engine/ac/speech.cpp b/Engine/ac/speech.cpp index b78ef8d778..fdc6dc2f45 100644 --- a/Engine/ac/speech.cpp +++ b/Engine/ac/speech.cpp @@ -128,7 +128,7 @@ bool init_voicepak(const String &name) { // If we have custom voice directory set, we will enable voice-over even if speech.vox does not exist speech_subdir = name.IsEmpty() ? ResPaths.VoiceDir2 : Path::ConcatPaths(ResPaths.VoiceDir2, name); - if (File::IsDirectory(speech_subdir) && !FindFile::OpenFiles(speech_subdir).AtEnd()) + if (File::IsDirectory(speech_subdir) && Directory::HasAnyFiles(speech_subdir)) { Debug::Printf(kDbgMsg_Info, "Optional voice directory is defined: %s", speech_subdir.GetCStr()); ResPaths.VoiceAvail = true; diff --git a/Engine/gui/guidialog.cpp b/Engine/gui/guidialog.cpp index e665a4ed76..9a84d6fd18 100644 --- a/Engine/gui/guidialog.cpp +++ b/Engine/gui/guidialog.cpp @@ -42,13 +42,11 @@ int windowPosX, windowPosY, windowPosWidth, windowPosHeight; Bitmap *windowBuffer; IDriverDependantBitmap *dialogDDB; -#undef MAXSAVEGAMES -#define MAXSAVEGAMES 20 char *lpTemp, *lpTemp2; char bufTemp[260], buffer2[260]; int numsaves = 0, toomanygames; -int filenumbers[MAXSAVEGAMES]; -unsigned long filedates[MAXSAVEGAMES]; +std::vector filenumbers; +std::vector filedates; CSCIMessage smes; @@ -105,7 +103,9 @@ void refresh_gui_screen() render_graphics(dialogDDB, windowPosX, windowPosY); } -int loadgamedialog() +void preparesavegamelist(int ctrllist, int min_slot, int max_slot); + +int loadgamedialog(int min_slot, int max_slot) { const int wnd_width = 200; const int wnd_height = 120; @@ -123,7 +123,7 @@ int loadgamedialog() int ctrltex1 = CSCICreateControl(CNT_LABEL, 10, 5, 120, 0, get_global_message(MSG_SELECTLOAD)); CSCISendControlMessage(ctrllist, CLB_CLEAR, 0, 0); - preparesavegamelist(ctrllist); + preparesavegamelist(ctrllist, min_slot, max_slot); CSCIMessage mes; lpTemp = nullptr; int toret = -1; @@ -156,7 +156,7 @@ int loadgamedialog() return toret; } -int savegamedialog() +int savegamedialog(int min_slot, int max_slot) { char okbuttontext[50]; strcpy(okbuttontext, get_global_message(MSG_SAVEBUTTON)); @@ -177,7 +177,7 @@ int savegamedialog() int ctrltbox = 0; CSCISendControlMessage(ctrllist, CLB_CLEAR, 0, 0); // clear the list box - preparesavegamelist(ctrllist); + preparesavegamelist(ctrllist, min_slot, max_slot); if (toomanygames) { strcpy(okbuttontext, get_global_message(MSG_REPLACE)); strcpy(labeltext, get_global_message(MSG_MUSTREPLACE)); @@ -296,21 +296,26 @@ int savegamedialog() return toret; } -void preparesavegamelist(int ctrllist) +void preparesavegamelist(int ctrllist, int min_slot, int max_slot) { - // TODO: find out if limiting to MAXSAVEGAMES is still necessary here + max_slot = std::min(max_slot, TOP_SAVESLOT); + min_slot = std::min(max_slot, std::max(0, min_slot)); + std::vector saves; - FillSaveList(saves, TOP_LISTEDSAVESLOT, MAXSAVEGAMES); + FillSaveList(saves, min_slot, max_slot); std::sort(saves.rbegin(), saves.rend()); - // fill in the list box and global savegameindex[] array for backward compatibilty + filenumbers.resize(saves.size()); + filedates.resize(saves.size()); for (numsaves = 0; (size_t)numsaves < saves.size(); ++numsaves) { CSCISendControlMessage(ctrllist, CLB_ADDITEM, 0, (intptr_t)saves[numsaves].Description.GetCStr()); filenumbers[numsaves] = saves[numsaves].Slot; - filedates[numsaves] = (long int)saves[numsaves].FileTime; + filedates[numsaves] = saves[numsaves].FileTime; } - toomanygames = (numsaves >= MAXSAVEGAMES) ? 1 : 0; + // "toomanygames" if the whole range of slots is occupied + toomanygames = + saves.size() >= static_cast(max_slot - min_slot); // Select the first item CSCISendControlMessage(ctrllist, CLB_SETCURSEL, 0, 0); } diff --git a/Engine/gui/guidialog.h b/Engine/gui/guidialog.h index fe8e1148ed..6cd34556c3 100644 --- a/Engine/gui/guidialog.h +++ b/Engine/gui/guidialog.h @@ -32,9 +32,8 @@ void clear_gui_screen(); // Draws virtual screen contents on the GUI bitmaps and assignes them to // the renderer's draw chain void refresh_gui_screen(); -int loadgamedialog(); -int savegamedialog(); -void preparesavegamelist(int ctrllist); +int loadgamedialog(int min_slot, int max_slot); +int savegamedialog(int min_slot, int max_slot); void enterstringwindow(const char *prompttext, char *dst_buf, size_t dst_sz); int enternumberwindow(char *prompttext); int roomSelectorWindow(int currentRoom, int numRooms, diff --git a/Engine/main/config.cpp b/Engine/main/config.cpp index c27096d0ce..ed4023ff13 100644 --- a/Engine/main/config.cpp +++ b/Engine/main/config.cpp @@ -412,6 +412,7 @@ void apply_config(const ConfigTree &cfg) usetup.override_upscale = CfgReadBoolInt(cfg, "override", "upscale", usetup.override_upscale); usetup.key_save_game = CfgReadInt(cfg, "override", "save_game_key", 0); usetup.key_restore_game = CfgReadInt(cfg, "override", "restore_game_key", 0); + usetup.max_save_slot = CfgReadInt(cfg, "override", "max_save", 0); // Accessibility settings usetup.access_speechskip = parse_speechskip_style(CfgReadString(cfg, "access", "speechskip")); diff --git a/Engine/main/game_run.cpp b/Engine/main/game_run.cpp index e31eae126a..668dae309f 100644 --- a/Engine/main/game_run.cpp +++ b/Engine/main/game_run.cpp @@ -651,10 +651,10 @@ static void check_keyboard_controls() // Built-in key-presses if ((usetup.key_save_game > 0) && (agskey == usetup.key_save_game)) { - do_save_game_dialog(); + do_save_game_dialog(0, TOP_SAVESLOT - 1); return; } else if ((usetup.key_restore_game > 0) && (agskey == usetup.key_restore_game)) { - do_restore_game_dialog(); + do_restore_game_dialog(0, TOP_SAVESLOT - 1); return; } diff --git a/Engine/platform/base/agsplatformdriver.cpp b/Engine/platform/base/agsplatformdriver.cpp index be0c05eb19..a28f539cb3 100644 --- a/Engine/platform/base/agsplatformdriver.cpp +++ b/Engine/platform/base/agsplatformdriver.cpp @@ -78,19 +78,10 @@ const char *AGSPlatformDriver::GetDiskWriteAccessTroubleshootingText() return "Make sure you have write permissions, and also check the disk's free space."; } -void AGSPlatformDriver::GetSystemTime(ScriptDateTime *sdt) { +void AGSPlatformDriver::GetSystemTime(ScriptDateTime *sdt) +{ time_t t = time(nullptr); - - //note: subject to year 2038 problem due to shoving time_t in an integer - sdt->rawUnixTime = static_cast(t); - - struct tm *newtime = localtime(&t); - sdt->hour = newtime->tm_hour; - sdt->minute = newtime->tm_min; - sdt->second = newtime->tm_sec; - sdt->day = newtime->tm_mday; - sdt->month = newtime->tm_mon + 1; - sdt->year = newtime->tm_year + 1900; + sdt->SetFromStdTime(t); } void AGSPlatformDriver::DisplayAlert(const char *text, ...) diff --git a/Engine/script/script.cpp b/Engine/script/script.cpp index 22b18c458a..47ddde586a 100644 --- a/Engine/script/script.cpp +++ b/Engine/script/script.cpp @@ -704,7 +704,7 @@ void post_script_cleanup() { try_restore_save(thisData); return; case ePSARestoreGameDialog: - restore_game_dialog(); + restore_game_dialog2(thisData & 0xFFFF, (thisData >> 16)); return; case ePSARunAGSGame: cancel_all_scripts(); @@ -721,7 +721,7 @@ void post_script_cleanup() { save_game(thisData, act.Description.GetCStr(), std::move(act.Image)); break; case ePSASaveGameDialog: - save_game_dialog(); + save_game_dialog2(thisData & 0xFFFF, (thisData >> 16)); break; default: quitprintf("undefined post script action found: %d", act.Type); diff --git a/OPTIONS.md b/OPTIONS.md index 1d728aa381..e5b1e4d4d9 100644 --- a/OPTIONS.md +++ b/OPTIONS.md @@ -128,6 +128,7 @@ Locations of two latter files differ between running platforms: * mac - MacOS. * legacysave_assume_dataver = \[integer\] - when restoring a save of a legacy format, lets engine assume certain game data version ID. This may be necessary because of a mistake in pre-3.5.0 save formats, where contents depended on a game data version rather than a save version. * legacysave_let_gui_diff = \[0; 1\] - when restoring a save of a legacy format, lets engine read less gui elements than are registered by the current game. This was a (possibly unintended) effect in pre-3.5.0 save format. + * max_save = \[integer\] - topmost save slot that may be used in a standard save/restore dialog in game. This affects only built-in dialogs and listboxes filled by FillSaveGameList() script command. This does not and cannot affect any other custom scripted save systems. * restore_game_key = \[integer\] - key for calling built-in restore game dialog. Key value corresponds to the [AGS script keycode](https://github.com/adventuregamestudio/ags-manual/wiki/Keycodes). * save_game_key = \[integer\] - key for calling built-in save game dialog. * upscale = \[0; 1\] - run game in the "upscale mode". The earlier versions of AGS provided support for "upscaling" low-res games to hi-res. The script API has means for detecting if the game is running upscaled, and game developer could use this opportunity to setup game accordingly (e.g. assign hi-res fonts, etc). This options works **only** for games created before AGS 3.1.0 with low-res native resolution, such as 320x200 or 320x240, and it may somewhat improve