diff --git a/CMakeLists.txt b/CMakeLists.txt index ea42e183cb..ce5c230bb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,6 +200,8 @@ add_library(${PROJECT_NAME} OBJECT src/game_pictures.h src/game_player.cpp src/game_player.h + src/game_powerpatch.cpp + src/game_powerpatch.h src/game_quit.cpp src/game_quit.h src/game_screen.cpp diff --git a/Makefile.am b/Makefile.am index 63fc20c3b5..0c62174562 100644 --- a/Makefile.am +++ b/Makefile.am @@ -176,6 +176,8 @@ libeasyrpg_player_a_SOURCES = \ src/game_pictures.h \ src/game_player.cpp \ src/game_player.h \ + src/game_powerpatch.cpp \ + src/game_powerpatch.h \ src/game_screen.cpp \ src/game_screen.h \ src/game_strings.cpp \ diff --git a/src/async_op.h b/src/async_op.h index 847bdf3106..bf3756615b 100644 --- a/src/async_op.h +++ b/src/async_op.h @@ -43,6 +43,7 @@ class AsyncOp { eTerminateBattle, eSave, eLoad, + eLoadParallel, eYield, eYieldRepeat, eCloneMapEvent, @@ -78,6 +79,9 @@ class AsyncOp { /** @return a Load async operation */ static AsyncOp MakeLoad(int save_slot); + /** @return a LoadParallel async operation (to be used for patch compatibility) */ + static AsyncOp MakeLoadParallel(int save_slot); + /** @return a Yield for one frame to e.g. fetch an important asset */ static AsyncOp MakeYield(); @@ -128,7 +132,7 @@ class AsyncOp { /** * @return the desired slot to save or load - * @pre If GetType() is not eSave or eLoad, the return value is undefined. + * @pre If GetType() is not eSave, eLoad, eLoadParallel, the return value is undefined. **/ int GetSaveSlot() const; @@ -220,7 +224,7 @@ inline int AsyncOp::GetBattleResult() const { } inline int AsyncOp::GetSaveSlot() const { - assert(GetType() == eSave || GetType() == eLoad); + assert(GetType() == eSave || GetType() == eLoad || GetType() == eLoadParallel); return _args[0]; } @@ -305,6 +309,10 @@ inline AsyncOp AsyncOp::MakeLoad(int save_slot) { return AsyncOp(eLoad, save_slot); } +inline AsyncOp AsyncOp::MakeLoadParallel(int save_slot) { + return AsyncOp(eLoadParallel, save_slot); +} + inline AsyncOp AsyncOp::MakeYield() { return AsyncOp(eYield); } diff --git a/src/filefinder.cpp b/src/filefinder.cpp index ae22004eed..3c858d59a8 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -399,10 +399,8 @@ bool FileFinder::HasSavegame() { int FileFinder::GetSavegames() { auto fs = Save(); - for (int i = 1; i <= 15; i++) { - std::stringstream ss; - ss << "Save" << (i <= 9 ? "0" : "") << i << ".lsd"; - std::string filename = fs.FindFile(ss.str()); + for (int i = 1; i <= Player::Constants::MaxSaveFiles(); i++) { + std::string filename = fs.FindFile(GetSaveFilename(i)); if (!filename.empty()) { return true; @@ -411,6 +409,22 @@ int FileFinder::GetSavegames() { return false; } +std::string FileFinder::GetSaveFilename(int slot) { + std::stringstream ss; + ss << "Save" << (slot <= 9 ? "0" : "") << (slot) << ".lsd"; + return ss.str(); +} + +std::string FileFinder::GetSaveFilename(const FilesystemView& fs, int slot, bool validate_exists) { + auto filename = GetSaveFilename(slot); + auto filename_fs = fs.FindFile(filename); + + if (filename_fs.empty() && !validate_exists) { + return filename; + } + return filename_fs; +} + std::string find_generic(const DirectoryTree::Args& args) { if (!Tr::GetCurrentTranslationId().empty()) { auto tr_fs = Tr::GetCurrentTranslationFilesystem(); diff --git a/src/filefinder.h b/src/filefinder.h index f9b371abca..5481830141 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -362,6 +362,9 @@ namespace FileFinder { /** @returns Amount of savegames in the save directory */ int GetSavegames(); + std::string GetSaveFilename(int slot); + std::string GetSaveFilename(const FilesystemView& fs, int slot, bool validate_exists = true); + /** * Known file sizes */ diff --git a/src/game_config_game.cpp b/src/game_config_game.cpp index 4a719b5556..c8586a463f 100644 --- a/src/game_config_game.cpp +++ b/src/game_config_game.cpp @@ -88,6 +88,7 @@ void Game_ConfigGame::LoadFromArgs(CmdlineParser& cp) { patch_rpg2k3_commands.Lock(false); patch_anti_lag_switch.Lock(0); patch_direct_menu.Lock(0); + patch_better_aep.Lock(0); patch_override = true; continue; } @@ -155,6 +156,18 @@ void Game_ConfigGame::LoadFromArgs(CmdlineParser& cp) { } continue; } + if (cp.ParseNext(arg, 1, { "--patch-better-aep", "--no-patch-better-aep" })) { + if (arg.ArgIsOn() && arg.ParseValue(0, li_value)) { + patch_better_aep.Set(li_value); + patch_override = true; + } + + if (arg.ArgIsOff()) { + patch_better_aep.Set(0); + patch_override = true; + } + continue; + } if (cp.ParseNext(arg, 6, "--patch")) { // For backwards compatibility only for (int i = 0; i < arg.NumValues(); ++i) { @@ -229,6 +242,10 @@ void Game_ConfigGame::LoadFromStream(Filesystem_Stream::InputStream& is) { if (patch_direct_menu.FromIni(ini)) { patch_override = true; } + + if (patch_better_aep.FromIni(ini)) { + patch_override = true; + } } void Game_ConfigGame::PrintActivePatches() { @@ -257,6 +274,7 @@ void Game_ConfigGame::PrintActivePatches() { add_int(patch_maniac); add_int(patch_anti_lag_switch); add_int(patch_direct_menu); + add_int(patch_better_aep); if (patches.empty()) { Output::Debug("Patch configuration: None"); diff --git a/src/game_config_game.h b/src/game_config_game.h index fe9e3ec0c0..8828cdfd94 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -45,9 +45,12 @@ struct Game_ConfigGame { BoolConfigParam patch_common_this_event{ "Common This Event", "Support \"This Event\" in Common Events", "Patch", "CommonThisEvent", false }; BoolConfigParam patch_unlock_pics{ "Unlock Pictures", "Allow picture commands while a message is shown", "Patch", "PicUnlock", false }; BoolConfigParam patch_key_patch{ "Ineluki Key Patch", "Support \"Ineluki Key Patch\"", "Patch", "KeyPatch", false }; + //BoolConfigParam patch_key_patch_no_async{ "Ineluki Key Patch", "Force KeyPatch scripts to behave synchronously", "Patch", "KeyPatch.NoAsync", false }; BoolConfigParam patch_rpg2k3_commands{ "RPG2k3 Event Commands", "Enable support for RPG2k3 event commands", "Patch", "RPG2k3Commands", false }; ConfigParam patch_anti_lag_switch{ "Anti-Lag Switch", "Disable event page refreshes when switch is set", "Patch", "AntiLagSwitch", 0 }; ConfigParam patch_direct_menu{ "Direct Menu", " Allows direct access to subscreens of the default menu", "Patch", "DirectMenu", 0 }; + ConfigParam patch_better_aep{ "BetterAEP", "Emulates the \"BetterAEP\" patch, commonly used for custom title screens.", "Patch", "BetterAEP", 0 }; + ConfigParam patch_custom_save_load{ "BetterAEP", "Emulates the \"BetterAEP\" addon which allows for saving/loading by save slot.", "Patch", "BetterAEP.CustomSaveLoad", 0 }; // Command line only BoolConfigParam patch_support{ "Support patches", "When OFF all patch support is disabled", "", "", true }; diff --git a/src/game_ineluki.cpp b/src/game_ineluki.cpp index 7a8d660453..7848050d6c 100644 --- a/src/game_ineluki.cpp +++ b/src/game_ineluki.cpp @@ -17,6 +17,7 @@ // Headers #include "game_ineluki.h" +#include "game_powerpatch.h" #include "async_handler.h" #include "filefinder.h" #include "utils.h" @@ -27,6 +28,7 @@ #include "system.h" #include +#include namespace { #if defined(SUPPORT_KEYBOARD) @@ -66,19 +68,28 @@ bool Game_Ineluki::Execute(const lcf::rpg::Sound& se) { std::string ini_file = FileFinder::FindSound(se.name); if (!ini_file.empty()) { - return Execute(ini_file); + Execute(ini_file); + return true; } else { Output::Debug("Ineluki: Script {} not found", se.name); } return false; } -bool Game_Ineluki::Execute(std::string_view ini_file) { +AsyncOp Game_Ineluki::Execute(std::string_view ini_file) { + auto p = FileFinder::GetPathAndFilename(ini_file); + if (StartsWith(Utils::LowerCase(p.second), "saves.script")) { + // Support for the script written by SaveCount.dat + // It counts the amount of savegames and outputs the result + output_mode = OutputMode::Output; + output_list.push_back(FileFinder::GetSavegames()); + return {}; + } auto ini_file_s = ToString(ini_file); if (functions.find(ini_file_s) == functions.end()) { if (!Parse(ini_file)) { - return false; + return {}; } } @@ -88,15 +99,7 @@ bool Game_Ineluki::Execute(std::string_view ini_file) { if (cmd.name == "writetolog") { Output::InfoStr(cmd.arg); } else if (cmd.name == "execprogram") { - // Fake execute some known programs - if (StartsWith(cmd.arg, "exitgame") || - StartsWith(cmd.arg, "taskkill")) { - Player::exit_flag = true; - } else if (StartsWith(cmd.arg, "SaveCount.dat")) { - // no-op, detected through saves.script access - } else { - Output::Warning("Ineluki ExecProgram {}: Not supported", cmd.arg); - } + return ExecProgram(Utils::LowerCase(cmd.arg)); } else if (cmd.name == "mcicommand") { Output::Warning("Ineluki MciProgram {}: Not supported", cmd.arg); } else if (cmd.name == "miditickfunction") { @@ -156,7 +159,7 @@ bool Game_Ineluki::Execute(std::string_view ini_file) { } } else if (cmd.name == "getmouseposition") { if (!mouse_support) { - return true; + return {}; } Point mouse_pos = Input::GetMousePosition(); @@ -176,7 +179,7 @@ bool Game_Ineluki::Execute(std::string_view ini_file) { } else if (cmd.name == "setmouseasreturn") { // This command is only found in a few uncommon versions of the patch if (!mouse_support) { - return true; + return {}; } std::string arg_lower = Utils::LowerCase(cmd.arg); if (arg_lower == "left") { @@ -194,7 +197,7 @@ bool Game_Ineluki::Execute(std::string_view ini_file) { } else if (cmd.name == "setmousewheelaskeys") { // This command is only found in a few uncommon versions of the patch if (!mouse_support) { - return true; + return {}; } std::string arg_lower = Utils::LowerCase(cmd.arg); if (arg_lower == "updown") { @@ -210,7 +213,7 @@ bool Game_Ineluki::Execute(std::string_view ini_file) { } } - return true; + return {}; } bool Game_Ineluki::ExecuteScriptList(std::string_view list_file) { @@ -338,6 +341,13 @@ void Game_Ineluki::Update() { if (mouse_support) { UpdateMouse(); } + for (auto& [ key, frames ] : Game_PowerPatch::simulate_keypresses) { + if (frames == 0) { + continue; + } + Input::GetInputSource()->SimulateKeyPress(key); + frames--; + } } void Game_Ineluki::UpdateKeys() { @@ -403,6 +413,28 @@ void Game_Ineluki::UpdateMouse() { #endif } +AsyncOp Game_Ineluki::ExecProgram(std::string_view command) { + // Fake execute some known programs + if (StartsWith(command, "exitgame") || StartsWith(command, "taskkill")) { + Player::exit_flag = true; + } else if (StartsWith(command, "savecount.dat")) { + // no-op, detected through saves.script access + } else if (StartsWith(command, "ls.dat")) { + // (arg1 commonly given for "LS.dat" refers to the version-dependent + // virtual address of the RPG_RT loading mechanism) + Scene::instance->SetRequestedScene(std::make_shared()); + } else if (StartsWith(command, "ppcomp")) { + auto args = Utils::Tokenize(command, [&](char32_t c) { return std::isspace(c); }); + if (args.size() > 1) { + return Game_PowerPatch::ExecutePPC(Utils::UpperCase(args[1]), Span(args).subspan(2)); + } + } else { + Output::Warning("Ineluki ExecProgram {}: Not supported", command); + } + + return {}; +} + void Game_Ineluki::OnScriptFileReady(FileRequestResult* result) { auto it = std::find_if(async_scripts.begin(), async_scripts.end(), [&](const auto& a) { return a.script_name == result->file; diff --git a/src/game_ineluki.h b/src/game_ineluki.h index 8bd49bbc5b..b470f2e049 100644 --- a/src/game_ineluki.h +++ b/src/game_ineluki.h @@ -28,6 +28,7 @@ #include "keys.h" #include "string_view.h" #include "async_handler.h" +#include "async_op.h" /** * Implements Ineluki's Key Patch @@ -52,7 +53,7 @@ class Game_Ineluki { * @param ini_file INI file to execute * @return Whether the file is a valid script */ - bool Execute(std::string_view ini_file); + AsyncOp Execute(std::string_view ini_file); /** * Executes a file containing a list of script files. @@ -97,6 +98,8 @@ class Game_Ineluki { */ bool Parse(std::string_view ini_file); + AsyncOp ExecProgram(std::string_view command); + struct InelukiCommand { std::string name; std::string arg; diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index c4ae07e809..3797a836b6 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -71,6 +71,7 @@ #include "baseui.h" #include "algo.h" #include "rand.h" +#include using namespace Game_Interpreter_Shared; @@ -1970,9 +1971,41 @@ bool Game_Interpreter::CommandWait(lcf::rpg::EventCommand const& com) { // code return false; } +namespace InelukiKeyPatch { + bool HandleScriptFile(std::string_view file_name, bool is_music, AsyncOp& async_op) { + if (Player::IsPatchKeyPatch() && EndsWith(file_name, ".script")) { + //if (!Player::game_config.patch_key_patch_no_async.Get()) { + // // Script will be handled in place of the usual sound processing -> Game_System + // return false; + //} + // Is a Ineluki Script File + FileRequestAsync* request = AsyncHandler::RequestFile(is_music ? "Music" : "Sound", file_name); + request->SetImportantFile(true); + request->Start(); + + if (!request->IsReady()) { + async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + auto op = Main_Data::game_ineluki->Execute(is_music ? FileFinder::FindMusic(file_name) : FileFinder::FindSound(file_name)); + if (op.IsActive()) { + async_op = op; + } + return true; + } + return false; + } +} + bool Game_Interpreter::CommandPlayBGM(lcf::rpg::EventCommand const& com) { // code 11510 lcf::rpg::Music music; - music.name = ToString(CommandStringOrVariableBitfield(com, 4, 0, 5)); + auto music_name = CommandStringOrVariableBitfield(com, 4, 0, 5); + + if (InelukiKeyPatch::HandleScriptFile(music_name, true, _async_op)) { + return true; + } + + music.name = ToString(music_name); music.fadein = ValueOrVariableBitfield(com, 4, 1, 0); music.volume = ValueOrVariableBitfield(com, 4, 2, 1); @@ -1991,7 +2024,13 @@ bool Game_Interpreter::CommandFadeOutBGM(lcf::rpg::EventCommand const& com) { // bool Game_Interpreter::CommandPlaySound(lcf::rpg::EventCommand const& com) { // code 11550 lcf::rpg::Sound sound; - sound.name = ToString(CommandStringOrVariableBitfield(com, 3, 0, 4)); + auto sound_name = CommandStringOrVariableBitfield(com, 3, 0, 4); + + if (InelukiKeyPatch::HandleScriptFile(sound_name, false, _async_op)) { + return true; + } + + sound.name = ToString(sound_name); sound.volume = ValueOrVariableBitfield(com, 3, 1, 0); sound.tempo = ValueOrVariableBitfield(com, 3, 2, 1); @@ -2002,6 +2041,25 @@ bool Game_Interpreter::CommandPlaySound(lcf::rpg::EventCommand const& com) { // } bool Game_Interpreter::CommandEndEventProcessing(lcf::rpg::EventCommand const& /* com */) { // code 12310 + if (auto var_id = Player::game_config.patch_better_aep.Get()) { + switch (Main_Data::game_variables->Get(var_id)) { + case 1: + if (var_id = Player::game_config.patch_custom_save_load.Get()) { + if (auto save_slot = Main_Data::game_variables->Get(var_id) > 0) { + _async_op = MakeLoadOp("CustomSaveLoad", save_slot); + return true; + } + } + Scene::instance->SetRequestedScene(std::make_shared()); + return true; + case 2: + Player::exit_flag = true; + return true; + default: + break; + } + } + EndEventProcessing(); return true; } @@ -4271,25 +4329,16 @@ bool Game_Interpreter::CommandManiacGetSaveInfo(lcf::rpg::EventCommand const& co Main_Data::game_variables->Set(com.parameters[4], 0); Main_Data::game_variables->Set(com.parameters[5], 0); - if (save_number <= 0) { - Output::Debug("ManiacGetSaveInfo: Invalid save number {}", save_number); - return true; - } + bool save_corrupted = false; + auto save = ValidateAndLoadSave("ManiacGetSaveInfo", FileFinder::Save(), save_number, save_corrupted); - auto savefs = FileFinder::Save(); - std::string save_name = Scene_Save::GetSaveFilename(savefs, save_number); - auto save_stream = FileFinder::Save().OpenInputStream(save_name); - - if (!save_stream) { - Output::Debug("ManiacGetSaveInfo: Save not found {}", save_number); + if (save_corrupted) { + // Maniac Patch writes this for whatever reason + Main_Data::game_variables->Set(com.parameters[2], 8991230); return true; } - auto save = lcf::LSD_Reader::Load(save_stream, Player::encoding); if (!save) { - Output::Debug("ManiacGetSaveInfo: Save corrupted {}", save_number); - // Maniac Patch writes this for whatever reason - Main_Data::game_variables->Set(com.parameters[2], 8991230); return true; } @@ -4364,27 +4413,19 @@ bool Game_Interpreter::CommandManiacLoad(lcf::rpg::EventCommand const& com) { } int slot = ValueOrVariable(com.parameters[0], com.parameters[1]); - if (slot <= 0) { - Output::Debug("ManiacLoad: Invalid save slot {}", slot); - return true; - } // Not implemented (kinda useless feature): // When com.parameters[2] is 1 the check whether the file exists is skipped // When skipped and missing RPG_RT will crash - auto savefs = FileFinder::Save(); - std::string save_name = Scene_Save::GetSaveFilename(savefs, slot); - auto save_stream = FileFinder::Save().OpenInputStream(save_name); - std::unique_ptr save = lcf::LSD_Reader::Load(save_stream, Player::encoding); + auto op = MakeLoadOp("ManiacLoad", slot); - if (!save) { - Output::Debug("ManiacLoad: Save not found {}", slot); + if (!op.IsActive()) { return true; } // FIXME: In Maniac the load causes a blackscreen followed by a fade-in that can be cancelled by a transition event // This is not implemented yet, the loading is instant without fading - _async_op = AsyncOp::MakeLoad(slot); + _async_op = op; return true; } diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 25f1e895ee..0a37827085 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -776,6 +776,13 @@ bool Game_Interpreter_Map::CommandOpenSaveMenu(lcf::rpg::EventCommand const& com auto& frame = GetFrame(); auto& index = frame.current_command; + if (auto var_id = Player::game_config.patch_custom_save_load.Get()) { + if (auto save_slot = Main_Data::game_variables->Get(var_id) > 0) { + _async_op = AsyncOp::MakeSave(save_slot, -1); + return true; + } + } + Scene::instance->SetRequestedScene(std::make_shared()); int current_system_function = 0; diff --git a/src/game_interpreter_shared.cpp b/src/game_interpreter_shared.cpp index ee923ada5b..532b2a3e15 100644 --- a/src/game_interpreter_shared.cpp +++ b/src/game_interpreter_shared.cpp @@ -37,6 +37,7 @@ #include #include #include +#include using Main_Data::game_switches, Main_Data::game_variables, Main_Data::game_strings; @@ -238,6 +239,101 @@ lcf::rpg::MoveCommand Game_Interpreter_Shared::DecodeMove(lcf::DBArray: return cmd; } +std::unique_ptr Game_Interpreter_Shared::ValidateAndLoadSave(const char* caller, const FilesystemView& fs, int save_slot) { + bool save_corrupted = false; + return ValidateAndLoadSave(caller, fs, save_slot, save_corrupted); +} + +std::unique_ptr Game_Interpreter_Shared::ValidateAndLoadSave(const char* caller, const FilesystemView& fs, int save_slot, bool& save_corrupted) { + save_corrupted = false; + + if (save_slot <= 0) { + Output::Debug("{}: Invalid save number {}", caller, save_slot); + return {}; + } + + std::string save_name = FileFinder::GetSaveFilename(fs, save_slot, false); + auto save_stream = FileFinder::Save().OpenInputStream(save_name); + + if (!save_stream) { + Output::Debug("{}: Save not found {}", caller, save_slot); + return {}; + } + + auto save = lcf::LSD_Reader::Load(save_stream, Player::encoding); + if (!save) { + Output::Debug("{}: Save corrupted {}", caller, save_slot); + save_corrupted = true;; + return {}; + } + return save; +} + +int Game_Interpreter_Shared::GetLatestSaveSlot(const FilesystemView& fs) { + int latest_slot = 0; + double latest_time = 0; + + //FIXME: Maybe consider just checking the file's modify dates instead of parsing them for the timestamp? + for (int i = 1; i <= Player::Constants::MaxSaveFiles(); i++) { + std::string file = FileFinder::GetSaveFilename(fs, i); + + if (!file.empty()) { + // File found + auto save_stream = FileFinder::Save().OpenInputStream(file); + if (!save_stream) { + //corrupted + continue; + } + + std::unique_ptr savegame = lcf::LSD_Reader::Load(save_stream, Player::encoding); + + if (savegame && savegame->title.timestamp > latest_time) { + latest_time = savegame->title.timestamp; + latest_slot = i; + } + } + } + return latest_slot; +} + +AsyncOp Game_Interpreter_Shared::MakeLoadOp(const char* caller, int save_slot) { + if (save_slot <= 0) { + Output::Debug("{}: Invalid save slot {}", caller, save_slot); + return {}; + } + + auto savefs = FileFinder::Save(); + std::string save_name = FileFinder::GetSaveFilename(savefs, save_slot, false); + auto save_stream = FileFinder::Save().OpenInputStream(save_name); + std::unique_ptr save = lcf::LSD_Reader::Load(save_stream, Player::encoding); + + if (!save) { + Output::Debug("{}: Save not found {}", caller, save_slot); + return {}; + } + + return AsyncOp::MakeLoad(save_slot); +} + +AsyncOp Game_Interpreter_Shared::MakeLoadParallel(const char* caller, int save_slot) { + if (save_slot <= 0) { + Output::Debug("{}: Invalid save slot {}", caller, save_slot); + return {}; + } + + auto savefs = FileFinder::Save(); + std::string save_name = FileFinder::GetSaveFilename(savefs, save_slot, false); + auto save_stream = FileFinder::Save().OpenInputStream(save_name); + std::unique_ptr save = lcf::LSD_Reader::Load(save_stream, Player::encoding); + + if (!save) { + Output::Debug("{}: Save not found {}", caller, save_slot); + return {}; + } + + return AsyncOp::MakeLoadParallel(save_slot); +} + //explicit declarations for target evaluation logic shared between ControlSwitches/ControlVariables/ControlStrings template bool Game_Interpreter_Shared::DecodeTargetEvaluationMode(lcf::rpg::EventCommand const&, int&, int&, Game_BaseInterpreterContext const&); template bool Game_Interpreter_Shared::DecodeTargetEvaluationMode(lcf::rpg::EventCommand const&, int&, int&, Game_BaseInterpreterContext const&); diff --git a/src/game_interpreter_shared.h b/src/game_interpreter_shared.h index 6cea473a31..6f1bfd371b 100644 --- a/src/game_interpreter_shared.h +++ b/src/game_interpreter_shared.h @@ -22,8 +22,11 @@ #include #include #include +#include #include +#include "async_op.h" #include "compiler.h" +#include "filesystem.h" class Game_Character; class Game_BaseInterpreterContext; @@ -103,6 +106,22 @@ namespace Game_Interpreter_Shared { lcf::rpg::MoveCommand DecodeMove(lcf::DBArray::const_iterator& it); bool ManiacCheckContinueLoop(int val, int val2, int type, int op); + + std::unique_ptr ValidateAndLoadSave(const char* caller, const FilesystemView& fs, int save_slot); + std::unique_ptr ValidateAndLoadSave(const char* caller, const FilesystemView& fs, int save_slot, bool& save_corrupted); + int GetLatestSaveSlot(const FilesystemView& fs); + + + AsyncOp MakeLoadOp(const char* caller, int save_slot); + + /** + * Sets up a "Parallel Load" which is a loading operation that is meant to run + * in the background while not actually stopping the game's execution. + * Note: This is coded to only emulate the behavior of certain RPG_RT patches + * & will likely lead to many unexpected results, depending on the usage. + * DO NOT USE this mechanic for implementing load-mechanics in a new game! + */ + AsyncOp MakeLoadParallel(const char* caller, int save_slot); } inline bool Game_Interpreter_Shared::CheckOperator(int val, int val2, int op) { diff --git a/src/game_powerpatch.cpp b/src/game_powerpatch.cpp new file mode 100644 index 0000000000..c81b80e393 --- /dev/null +++ b/src/game_powerpatch.cpp @@ -0,0 +1,392 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +// Headers +#include "game_powerpatch.h" +#include "filefinder.h" +#include "utils.h" +#include "output.h" +#include "player.h" +#include "system.h" +#include "scene_load.h" +#include "scene_save.h" +#include "scene_debug.h" +#include "scene_title.h" +#include "scene_menu.h" +#include "main_data.h" +#include "game_interpreter_map.h" +#include "game_map.h" +#include "game_switches.h" +#include "game_variables.h" +#include + +namespace { + void OverrideSystemMusic(lcf::rpg::Music& music, Span& args) { + music.name = args[0]; + if (args.size() > 1) { + music.volume = atoi(args[1].c_str()); + } + if (args.size() > 2) { + music.tempo = atoi(args[2].c_str()); + } + if (args.size() > 3) { + music.fadein = atoi(args[3].c_str()); + } + } + + void StoreTimestamp(int dest_v, std::tm* tm) { + Main_Data::game_variables->Set(dest_v, tm->tm_year + 1900); + Main_Data::game_variables->Set(dest_v + 1, tm->tm_mon + 1); + Main_Data::game_variables->Set(dest_v + 2, tm->tm_mday); + Main_Data::game_variables->Set(dest_v + 3, tm->tm_wday + 1); + Main_Data::game_variables->Set(dest_v + 4, tm->tm_hour); + Main_Data::game_variables->Set(dest_v + 5, tm->tm_min); + Main_Data::game_variables->Set(dest_v + 6, tm->tm_sec); + } + + //FIXME: Move this call to game_runtime_patches.h (part of another branch) + constexpr Input::Keys::InputKey VirtualKeyToInputKey(uint32_t key_id) { + // see https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + switch (key_id) { +#if defined(USE_MOUSE) && defined(SUPPORT_MOUSE) + case 0x1: return Input::Keys::MOUSE_LEFT; + case 0x2: return Input::Keys::MOUSE_RIGHT; + case 0x4: return Input::Keys::MOUSE_MIDDLE; + case 0x5: return Input::Keys::MOUSE_XBUTTON1; + case 0x6: return Input::Keys::MOUSE_XBUTTON2; +#endif + case 0x8: return Input::Keys::BACKSPACE; + case 0x9: return Input::Keys::TAB; + case 0xD: return Input::Keys::RETURN; + case 0x10: return Input::Keys::SHIFT; + case 0x11: return Input::Keys::CTRL; + case 0x12: return Input::Keys::ALT; + case 0x13: return Input::Keys::PAUSE; + case 0x14: return Input::Keys::CAPS_LOCK; + case 0x1B: return Input::Keys::ESCAPE; + case 0x20: return Input::Keys::SPACE; + case 0x21: return Input::Keys::PGUP; + case 0x22: return Input::Keys::PGDN; + case 0x23: return Input::Keys::ENDS; + case 0x24: return Input::Keys::HOME; + case 0x25: return Input::Keys::LEFT; + case 0x26: return Input::Keys::UP; + case 0x27: return Input::Keys::RIGHT; + case 0x28: return Input::Keys::DOWN; + case 0x2D: return Input::Keys::INSERT; + case 0x2E: return Input::Keys::DEL; + case 0x30: return Input::Keys::N0; + case 0x31: return Input::Keys::N1; + case 0x32: return Input::Keys::N2; + case 0x33: return Input::Keys::N3; + case 0x34: return Input::Keys::N4; + case 0x35: return Input::Keys::N5; + case 0x36: return Input::Keys::N6; + case 0x37: return Input::Keys::N7; + case 0x38: return Input::Keys::N8; + case 0x39: return Input::Keys::N9; + case 0x41: return Input::Keys::A; + case 0x42: return Input::Keys::B; + case 0x43: return Input::Keys::C; + case 0x44: return Input::Keys::D; + case 0x45: return Input::Keys::E; + case 0x46: return Input::Keys::F; + case 0x47: return Input::Keys::G; + case 0x48: return Input::Keys::H; + case 0x49: return Input::Keys::I; + case 0x4A: return Input::Keys::J; + case 0x4B: return Input::Keys::K; + case 0x4C: return Input::Keys::L; + case 0x4D: return Input::Keys::M; + case 0x4E: return Input::Keys::N; + case 0x4F: return Input::Keys::O; + case 0x50: return Input::Keys::P; + case 0x51: return Input::Keys::Q; + case 0x52: return Input::Keys::R; + case 0x53: return Input::Keys::S; + case 0x54: return Input::Keys::T; + case 0x55: return Input::Keys::U; + case 0x56: return Input::Keys::V; + case 0x57: return Input::Keys::W; + case 0x58: return Input::Keys::X; + case 0x59: return Input::Keys::Y; + case 0x5A: return Input::Keys::Z; + case 0x60: return Input::Keys::KP0; + case 0x61: return Input::Keys::KP1; + case 0x62: return Input::Keys::KP2; + case 0x63: return Input::Keys::KP3; + case 0x64: return Input::Keys::KP4; + case 0x65: return Input::Keys::KP5; + case 0x66: return Input::Keys::KP6; + case 0x67: return Input::Keys::KP7; + case 0x68: return Input::Keys::KP8; + case 0x69: return Input::Keys::KP9; + case 0x6A: return Input::Keys::KP_MULTIPLY; + case 0x6B: return Input::Keys::KP_ADD; + case 0x6D: return Input::Keys::KP_SUBTRACT; + case 0x6E: return Input::Keys::KP_PERIOD; + case 0x6F: return Input::Keys::KP_DIVIDE; + case 0x70: return Input::Keys::F1; + case 0x71: return Input::Keys::F2; + case 0x72: return Input::Keys::F3; + case 0x73: return Input::Keys::F4; + case 0x74: return Input::Keys::F5; + case 0x75: return Input::Keys::F6; + case 0x76: return Input::Keys::F7; + case 0x77: return Input::Keys::F8; + case 0x78: return Input::Keys::F9; + case 0x79: return Input::Keys::F10; + case 0x7A: return Input::Keys::F11; + case 0x7B: return Input::Keys::F12; + case 0x90: return Input::Keys::NUM_LOCK; + case 0x91: return Input::Keys::SCROLL_LOCK; + case 0xA0: return Input::Keys::LSHIFT; + case 0xA1: return Input::Keys::RSHIFT; + case 0xA2: return Input::Keys::LCTRL; + case 0xA3: return Input::Keys::RCTRL; + + default: return Input::Keys::NONE; + } + } +} + +std::map Game_PowerPatch::simulate_keypresses; + +AsyncOp Game_PowerPatch::ExecutePPC(std::string_view ppc_cmd, Span args) { + auto cmd = std::find_if(PPC_commands.begin(), PPC_commands.end(), [&ppc_cmd](auto& cmd) { + return ppc_cmd == cmd.name; + }); + if (cmd == PPC_commands.end()) { + Output::Warning("PPCOMP {}: unknown command", ppc_cmd); + return {}; + } + + if (args.size() < cmd->min_args) { + Output::Warning("PPCOMP {}: Missing required arguments (Min: {})", ppc_cmd, cmd->min_args); + return {}; + } + + AsyncOp async_op = {}; + Output::Debug("Executing PPC: {}", ppc_cmd); + if (!Execute(cmd->type, args, async_op)) { + Output::Warning("PPCOMP {}: Not supported", ppc_cmd); + return {}; + } + return async_op; +} + +bool Game_PowerPatch::Execute(PPC_CommandType command, Span args, AsyncOp& async_op) { + using Type = PPC_CommandType; + + switch (command) { + case Type::Debug: + // Note: PowerPatchCompact has a few nice debug options which might + // be neat to also have in EasyRPG's extended debug menu + Scene::instance->SetRequestedScene(std::make_shared()); + break; + case Type::Quit: + Player::exit_flag = true; + break; + case Type::Restart: + Player::reset_flag = true; + break; + case Type::Save: { + int slot = args.size() >= 1 ? atoi(args[0].c_str()) : 0; + int map = args.size() >= 2 ? atoi(args[1].c_str()) : 0; + int map_x = args.size() >= 3 ? atoi(args[2].c_str()) : -1; + int map_y = args.size() >= 4 ? atoi(args[3].c_str()) : -1; + + auto fs = FileFinder::Save(); + if (slot == 0) { + slot = Game_Interpreter_Shared::GetLatestSaveSlot(fs); + } + if (slot <= 0) { + Output::Warning("PowerPatch Save: Invalid save slot {}", slot); + return true; + } + //TODO: map_id, map_x, map_y + async_op = AsyncOp::MakeSave(slot, -1); + break; + } + case Type::Load: { + int slot = args.size() >= 1 ? atoi(args[0].c_str()) : 0; + + auto fs = FileFinder::Save(); + if (slot == 0) { + slot = Game_Interpreter_Shared::GetLatestSaveSlot(fs); + } + auto save = Game_Interpreter_Shared::ValidateAndLoadSave("PowerPatch Load", fs, slot); + if (!save) { + return true; + } + // In RPG_RT the loading operation happens asynchronously while the + // current interpreter is still running. Any other Ineluki scripts might + // be executed before the loading process is complete. + // This breaks some games, such as "Take it cheesy" + // -> Here, Save01 is loaded automatically, but the interpreter still + // executes a few other script commands right after, which set up + // the mouse patch. As a result, mouse functionality will work normally + // in RPG_RT, but not in EasyRPG Player, which never ran the + // neccessary setup scripts. + async_op = Game_Interpreter_Shared::MakeLoadParallel("PowerPatch Load", slot); + break; + } + case Type::CheckSave: { + int slot = atoi(args[0].c_str()); + int dest_sw = atoi(args[1].c_str()); + + auto fs = FileFinder::Save(); + auto exists = !FileFinder::GetSaveFilename(fs, slot, true).empty(); + Main_Data::game_switches->Set(dest_sw, exists); + break; + } + case Type::CopySave: { + int slot = atoi(args[0].c_str()); + int dest_slot = atoi(args[1].c_str()); + + auto fs = FileFinder::Save(); + auto save = Game_Interpreter_Shared::ValidateAndLoadSave("PowerPatch CopySave", fs, slot); + if (!save) { + return true; + } + if (dest_slot <= 0) { + Output::Warning("PowerPatch CopySave: Invalid save number {}", dest_slot); + return true; + } + //TODO: Not implemented + break; + } + case Type::DeleteSave: { + int slot = atoi(args[0].c_str()); + + if (slot <= 0) { + Output::Warning("PowerPatch DeleteSave: Invalid save number {}", slot); + return true; + } + //TODO: Not implemented + break; + } + case Type::GetSaveDateTime: { + int slot = atoi(args[0].c_str()); + int dest_v = atoi(args[1].c_str()); + + auto fs = FileFinder::Save(); + auto save = Game_Interpreter_Shared::ValidateAndLoadSave("PowerPatch GetSaveDateTime", fs, slot); + if (!save) { + return true; + } + std::time_t t = lcf::LSD_Reader::ToUnixTimestamp(save->title.timestamp); + std::tm* tm = std::gmtime(&t); + StoreTimestamp(dest_v, tm); + break; + } + case Type::GetSystemDateTime: { + int dest_v = atoi(args[0].c_str()); + + std::time_t t = lcf::LSD_Reader::GenerateTimestamp(); + std::tm* tm = std::gmtime(&t); + StoreTimestamp(dest_v, tm); + break; + } + case Type::SetGlobalBrightness: + // Not implemented. + // In some cases, changing the Scene via ppcomp might skip on the transition + // routine and leave the screen black in RPG_RT. + // This command was added to the patch, to be able to manually set the + // internal brightness back to '100'. + break; + case Type::CallLoadMenu: + Scene::instance->SetRequestedScene(std::make_shared()); + break; + case Type::CallSaveMenu: + Scene::instance->SetRequestedScene(std::make_shared()); + break; + case Type::CallGameMenu: { + if (args.size() >= 1) { + if (atoi(args[0].c_str())) { + Scene_Menu::force_cursor_index = Scene_Menu::CommandOptionType::Save; + } else { + Scene_Menu::force_cursor_index = Scene_Menu::CommandOptionType::Item; + } + } + Scene::instance->SetRequestedScene(std::make_shared()); + break; + } + case Type::CallTitleScreen: { + if (args.size() >= 1) { + if (atoi(args[0].c_str())) { + Scene_Title::force_cursor_index = Scene_Title::CommandOptionType::ContinueGame; + } else { + Scene_Title::force_cursor_index = Scene_Title::CommandOptionType::NewGame; + } + } + //Player::force_make_to_title_flag = true; + async_op = AsyncOp::MakeToTitle(); + break; + } + case Type::SetTitleBGM: + OverrideSystemMusic(lcf::Data::system.title_music, args); + break; + case Type::SetTitleScreen: + lcf::Data::system.title_name = lcf::DBString(args[0]); + break; + case Type::SetGameOverScreen: + lcf::Data::system.gameover_name = lcf::DBString(args[0]); + break; + case Type::UnlockPictures: { + int value = atoi(args[0].c_str()); + if (Player::game_config.patch_unlock_pics.IsLocked()) { + Player::game_config.patch_unlock_pics.SetLocked(false); + } + Player::game_config.patch_unlock_pics.Set(value); + break; + } + case Type::SimulateKeyPress: { + int vk = atoi(args[0].c_str()); + auto input_key = VirtualKeyToInputKey(vk); + + if (input_key == Input::Keys::NONE) { + Output::Debug("PowerPatch SimulateKeyPress: Unsupported keycode {}", vk); + return true; + } + + //TODO: Needs some proper testing + if (Utils::LowerCase(args[1]) == "down") { + simulate_keypresses[input_key] = 1; + } else if (Utils::LowerCase(args[1]) == "up") { + simulate_keypresses[input_key] = 0; + } else { + int duration = atoi(args[1].c_str()); + if (duration <= 0) { + Output::Debug("PowerPatch SimulateKeyPress: Unexpected arg {} ()", duration); + return true; + } + duration = DEFAULT_FPS * duration / 1000; + if (duration == 0) { + duration = 1; + } + simulate_keypresses[input_key] = duration; + } + break; + } + default: + return false; + } + + return true; +} + diff --git a/src/game_powerpatch.h b/src/game_powerpatch.h new file mode 100644 index 0000000000..bbb44a4b6e --- /dev/null +++ b/src/game_powerpatch.h @@ -0,0 +1,106 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_GAME_POWERPATCH_H +#define EP_GAME_POWERPATCH_H + +// Headers +#include + +#include "async_op.h" +#include "game_ineluki.h" +#include "string_view.h" +#include "span.h" + +namespace Game_PowerPatch { + enum class PPC_CommandType { + Debug, + Quit, + Restart, + Save, + Load, + CheckSave, + CopySave, + DeleteSave, + GetSaveDateTime, + GetSystemDateTime, + SetGlobalBrightness, + PauseTimer, + ChangeScene, + CallLoadMenu, + CallSaveMenu, + CallGameMenu, + CallTitleScreen, + SimulateKeyPress, + ChangeFunctionKey, + SetTitleBGM, + SetTitleScreen, + SetGameOverScreen, + UnlockPictures, + SetMsgBoxColor, + PauseGame, + SetVar, + LAST + }; + + AsyncOp ExecutePPC(std::string_view pcc_cmd, Span args); + + bool Execute(PPC_CommandType command, Span args, AsyncOp& async_op); + + struct PPC_Command { + PPC_CommandType type; + uint8_t min_args; + const char* name; + }; + + static constexpr std::array(PPC_CommandType::LAST)> PPC_commands = {{ + { PPC_CommandType::Debug, 0, "DEBUG" }, + { PPC_CommandType::Quit, 0, "QUIT" }, + { PPC_CommandType::Restart, 0, "RESTART" }, + { PPC_CommandType::Save, 0, "SAVE" }, + { PPC_CommandType::Load, 0, "LOAD" }, + { PPC_CommandType::CheckSave, 2, "CHECKSAVE" }, + { PPC_CommandType::CopySave, 2, "COPYSAVE" }, + { PPC_CommandType::DeleteSave, 1, "DELETESAVE" }, + { PPC_CommandType::GetSaveDateTime, 2, "GETSAVEDATETIME" }, + { PPC_CommandType::GetSystemDateTime, 1, "GETSYSTEMDATETIME" }, + { PPC_CommandType::SetGlobalBrightness, 1, "SETGLOBALBRIGHTNESS" }, + { PPC_CommandType::PauseTimer, 1, "PAUSETIMER" }, + { PPC_CommandType::ChangeScene, 1, "CHANGESCENE" }, + { PPC_CommandType::CallLoadMenu, 0, "CALLLOADMENU" }, + { PPC_CommandType::CallSaveMenu, 0, "CALLSAVEMENU" }, + { PPC_CommandType::CallGameMenu, 0, "CALLGAMEMENU" }, + { PPC_CommandType::CallTitleScreen, 0, "CALLTITLESCREEN" }, + { PPC_CommandType::SimulateKeyPress, 2, "SIMULATEKEYPRESS" }, + { PPC_CommandType::ChangeFunctionKey, 0, "CHANGEFUNCTIONKEY" }, + { PPC_CommandType::SetTitleBGM, 1, "SETTITLEBGM" }, + { PPC_CommandType::SetTitleScreen, 1, "SETTITLESCREEN" }, + { PPC_CommandType::SetGameOverScreen, 1, "SETGAMEOVERSCREEN" }, + { PPC_CommandType::UnlockPictures, 1, "UNLOCKPICTURES" }, + { PPC_CommandType::SetMsgBoxColor, 4, "SETMSGBOXCOLOR" }, + { PPC_CommandType::PauseGame, 0, "PAUSEGAME" }, + { PPC_CommandType::SetVar, 0, "SETVAR" } + }}; + + /** + * Map of simulated keypresses handled via ppcomp + * 'int' value refers to remaining frames + */ + extern std::map simulate_keypresses; +}; + +#endif diff --git a/src/game_system.cpp b/src/game_system.cpp index a4f0556ad0..fc6ee4209c 100644 --- a/src/game_system.cpp +++ b/src/game_system.cpp @@ -112,6 +112,10 @@ void Game_System::BgmPlay(lcf::rpg::Music const& bgm) { bgm_pending = true; FileRequestAsync* request = AsyncHandler::RequestFile("Music", bgm.name); music_request_id = request->Bind(&Game_System::OnBgmReady, this); + if (EndsWith(bgm.name, ".script")) { + // Is a Ineluki Script File + request->SetImportantFile(true); + } request->Start(); } } else { @@ -536,6 +540,12 @@ void Game_System::OnBgmReady(FileRequestResult* result) { return; } + if (Player::IsPatchKeyPatch() && EndsWith(result->file, ".script")) { + // Is a Ineluki Script File + Main_Data::game_ineluki->Execute(result->file); + return; + } + if (Player::IsPatchKeyPatch() && EndsWith(result->file, ".link") && stream.GetSize() < 500) { // Handle Ineluki's MP3 patch std::string line = InelukiReadLink(stream); diff --git a/src/meta.cpp b/src/meta.cpp index bdd47aab51..0848eef05a 100644 --- a/src/meta.cpp +++ b/src/meta.cpp @@ -177,14 +177,13 @@ std::vector Meta::BuildImportCandidateList(const FilesystemView& if (is_match) { // Scan over every possible save file and see if any match. - for (int saveId = 0; saveId < 15; saveId++) { - std::stringstream ss; - ss << "Save" << (saveId <= 8 ? "0" : "") << (saveId + 1) << ".lsd"; + for (int saveId = 0; saveId < Player::Constants::MaxSaveFiles(); saveId++) { + auto filename = FileFinder::GetSaveFilename(saveId + 1); // Check for an existing, non-corrupt file with the right mapID // Note that corruptness is checked later (in window_savefile.cpp) - if (child_tree.Exists(ss.str())) { - auto filePath= child_tree.GetSubPath() + "/" + ss.str(); + if (child_tree.Exists(filename)) { + auto filePath = child_tree.GetSubPath() + "/" + filename; std::unique_ptr savegame = lcf::LSD_Reader::Load(filePath, Player::encoding); if (savegame != nullptr) { if (savegame->party_location.map_id == pivot_map_id || pivot_map_id==0) { diff --git a/src/platform/emscripten/interface.cpp b/src/platform/emscripten/interface.cpp index 458fe4c3ad..249d243a1d 100644 --- a/src/platform/emscripten/interface.cpp +++ b/src/platform/emscripten/interface.cpp @@ -28,7 +28,7 @@ #include "filefinder.h" #include "filesystem_stream.h" #include "player.h" -#include "scene_save.h" +#include "scene.h" #include "output.h" void Emscripten_Interface::Reset() { @@ -37,7 +37,7 @@ void Emscripten_Interface::Reset() { bool Emscripten_Interface::DownloadSavegame(int slot) { auto fs = FileFinder::Save(); - std::string name = Scene_Save::GetSaveFilename(fs, slot); + std::string name = FileFinder::GetSaveFilename(fs, slot); auto is = fs.OpenInputStream(name); if (!is) { return false; @@ -85,7 +85,7 @@ void Emscripten_Interface::TakeScreenshot() { bool Emscripten_Interface_Private::UploadSavegameStep2(int slot, int buffer_addr, int size) { auto fs = FileFinder::Save(); - std::string name = Scene_Save::GetSaveFilename(fs, slot); + std::string name = FileFinder::GetSaveFilename(fs, slot); std::istream is(new Filesystem_Stream::InputMemoryStreamBufView(lcf::Span(reinterpret_cast(buffer_addr), size))); diff --git a/src/player.cpp b/src/player.cpp index 5557b24539..8b9480aa46 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -136,6 +136,7 @@ namespace Player { int rng_seed = -1; Game_ConfigPlayer player_config; Game_ConfigGame game_config; + bool force_make_to_title_flag = false; #ifdef EMSCRIPTEN std::string emscripten_game_name; #endif @@ -827,6 +828,44 @@ void Player::CreateGameObjects() { if (!FileFinder::Game().FindFile(DESTINY_DLL).empty()) { game_config.patch_destiny.Set(true); } + + if (!FileFinder::Game().FindFile("powerp.oex").empty()) { + Output::Warning("This game uses Power Patch and might not run properly."); + } + + // PowerMode2003 can be detected via the existence of the files "hvm.dll", "fmodex.dll" & + // "warp.dll", but some games seem to only ship with the latter of the three. + if (!FileFinder::Game().FindFile("warp.dll").empty()) { + Output::Warning("This game uses Power Mode 2003 and might not run properly."); + } + + /*if (game_config.patch_key_patch.Get()) { + auto exe_util_types = Utils::MakeSvArray(".exe", ".dll", ".dat"); + auto exe_util_names = Utils::MakeSvArray("ppcomp", "sfx"); + + auto exe_util_names_with_ext = Utils::MakeSvArray("savecount.dat", "ls.dat"); + + for (auto util_name : exe_util_names) { + if (!FileFinder::Game().FindFile(util_name, exe_util_types).empty()) { + Output::Debug("KeyPatch: Found external program '{}'. Patch scripts will behave synchronously.", util_name); + game_config.patch_key_patch_no_async.Set(true); + break; + } + } + if (!game_config.patch_key_patch_no_async.Get()) { + for (auto util_name : exe_util_names_with_ext) { + if (!FileFinder::Game().FindFile(util_name).empty()) { + Output::Debug("KeyPatch: Found external program '{}'. Patch scripts will behave synchronously.", util_name); + game_config.patch_key_patch_no_async.Set(true); + break; + } + } + } + }*/ + + if (Player::game_config.patch_better_aep.Get()) { + Player::game_config.new_game.Set(true); + } } game_config.PrintActivePatches(); @@ -1121,12 +1160,12 @@ static void OnMapSaveFileReady(FileRequestResult*, lcf::rpg::Save save) { std::move(save.common_events)); } -void Player::LoadSavegame(const std::string& save_name, int save_id) { +void Player::LoadSavegame(const std::string& save_name, int save_id, bool load_parallel) { Output::Debug("Loading Save {}", save_name); bool load_on_map = Scene::instance->type == Scene::Map; - if (!load_on_map) { + if (!load_on_map && !load_parallel) { Main_Data::game_system->BgmFade(800); // We erase the screen now before loading the saved game. This prevents an issue where // if the save game has a different system graphic, the load screen would change before @@ -1184,7 +1223,7 @@ void Player::LoadSavegame(const std::string& save_name, int save_id) { save->airship_location.animation_type = Game_Character::AnimType::AnimType_non_continuous; } - if (!load_on_map) { + if (!load_on_map || load_parallel) { Scene::PopUntil(Scene::Title); } @@ -1206,12 +1245,12 @@ void Player::LoadSavegame(const std::string& save_name, int save_id) { FileRequestAsync* map = Game_Map::RequestMap(map_id); save_request_id = map->Bind( - [save=std::move(*save), load_on_map, save_id](auto* request) { + [save=std::move(*save), load_on_map, load_parallel, save_id](auto* request) { Game_Map::Dispose(); OnMapSaveFileReady(request, std::move(save)); - if (load_on_map) { + if (load_on_map && !load_parallel) { // Increment frame counter for consistency with a normal savegame load IncFrame(); static_cast(Scene::instance.get())->StartFromSave(save_id); @@ -1223,7 +1262,7 @@ void Player::LoadSavegame(const std::string& save_name, int save_id) { map->Start(); // load_on_map is handled in the async callback - if (!load_on_map) { + if (!load_on_map || load_parallel) { Scene::Push(std::make_shared(save_id)); } } @@ -1453,6 +1492,9 @@ Engine options: --patch-direct-menu VAR Directly access subscreens of the default menu by setting VAR. + --patch-better-aep VAR + Emulates the behavior of the "BetterAEP" patch, which + is commonly used for implementing customized title screens. --patch-dynrpg Enable support of DynRPG patch by Cherry (very limited). --patch-easyrpg Enable EasyRPG extensions. --patch-key-patch Enable Key Patch by Ineluki. @@ -1613,3 +1655,7 @@ std::string Player::GetEngineVersion() { if (EngineVersion() > 0) return std::to_string(EngineVersion()); return std::string(); } + +int32_t Player::Constants::MaxSaveFiles() { + return Utils::Clamp(lcf::Data::system.easyrpg_max_savefiles, 3, 99); +} diff --git a/src/player.h b/src/player.h index 13575c7738..e78e6118be 100644 --- a/src/player.h +++ b/src/player.h @@ -172,7 +172,7 @@ namespace Player { * @param save_file Savegame file to load * @param save_id ID of the savegame to load */ - void LoadSavegame(const std::string& save_file, int save_id = 0); + void LoadSavegame(const std::string& save_file, int save_id = 0, bool load_parallel = false); /** * Starts a new game @@ -415,6 +415,9 @@ namespace Player { /** Translation manager, including list of languages and current translation. */ extern Translation translation; + /** If true, the game will be forced to stay at the title scene, even if the "new_game" option is set. */ + extern bool force_make_to_title_flag; + /** * The default speed modifier applied when the speed up button is pressed * Only used for configuring the speedup, don't read this var directly use @@ -435,6 +438,10 @@ namespace Player { /** Name of game emscripten uses */ extern std::string emscripten_game_name; #endif + + namespace Constants { + int32_t MaxSaveFiles(); + } } inline bool Player::IsRPG2k() { diff --git a/src/scene_file.cpp b/src/scene_file.cpp index 4e23f64c9c..a9a4b4a517 100644 --- a/src/scene_file.cpp +++ b/src/scene_file.cpp @@ -89,11 +89,7 @@ void Scene_File::UpdateLatestTimestamp(int id, lcf::rpg::Save& savegame) { } void Scene_File::PopulateSaveWindow(Window_SaveFile& win, int id) { - // Try to access file - std::stringstream ss; - ss << "Save" << (id <= 8 ? "0" : "") << (id + 1) << ".lsd"; - - std::string file = fs.FindFile(ss.str()); + std::string file = FileFinder::GetSaveFilename(fs, id + 1); if (!file.empty()) { // File found @@ -123,7 +119,7 @@ void Scene_File::Start() { // Refresh File Finder Save Folder fs = FileFinder::Save(); - for (int i = 0; i < Utils::Clamp(lcf::Data::system.easyrpg_max_savefiles, 3, 99); i++) { + for (int i = 0; i < Player::Constants::MaxSaveFiles(); i++) { std::shared_ptr w(new Window_SaveFile(Player::menu_offset_x, 40 + i * 64, MENU_WIDTH, 64)); w->SetIndex(i); diff --git a/src/scene_import.cpp b/src/scene_import.cpp index d1e5856c75..a919f3bf1e 100644 --- a/src/scene_import.cpp +++ b/src/scene_import.cpp @@ -54,9 +54,8 @@ void Scene_Import::Start() { CreateHelpWindow(); border_top = Scene_File::MakeBorderSprite(32); - // For consistency, we only show 15 windows - // We don't populate them until later (once we've loaded all potential importable files). - for (int i = 0; i < 15; i++) { + // We don't populate the windows until later (once we've loaded all potential importable files). + for (int i = 0; i < Player::Constants::MaxSaveFiles(); i++) { std::shared_ptr w(new Window_SaveFile(0, 40 + i * 64, Player::screen_width, 64)); w->SetIndex(i); @@ -128,7 +127,7 @@ void Scene_Import::UpdateScanAndProgress() { } void Scene_Import::FinishScan() { - for (int i = 0; i < 15; i++) { + for (int i = 0; i < Player::Constants::MaxSaveFiles(); i++) { auto w = file_windows[i]; PopulateSaveWindow(*w, i); w->Refresh(); diff --git a/src/scene_load.cpp b/src/scene_load.cpp index ffdc9ff717..d9d8a88f7e 100644 --- a/src/scene_load.cpp +++ b/src/scene_load.cpp @@ -29,8 +29,7 @@ Scene_Load::Scene_Load() : } void Scene_Load::Action(int index) { - std::string save_name = fs.FindFile(fmt::format("Save{:02d}.lsd", index + 1)); - + std::string save_name = FileFinder::GetSaveFilename(fs, index + 1); Player::LoadSavegame(save_name, index + 1); } diff --git a/src/scene_logo.cpp b/src/scene_logo.cpp index 11f0ef53e3..6906e3b283 100644 --- a/src/scene_logo.cpp +++ b/src/scene_logo.cpp @@ -104,14 +104,9 @@ void Scene_Logo::vUpdate() { Scene::PushTitleScene(true); if (Player::load_game_id > 0) { - auto save = FileFinder::Save(); + Output::Debug("Loading Save {}", FileFinder::GetSaveFilename(Player::load_game_id)); - std::stringstream ss; - ss << "Save" << (Player::load_game_id <= 9 ? "0" : "") << Player::load_game_id << ".lsd"; - - Output::Debug("Loading Save {}", ss.str()); - - std::string save_name = save.FindFile(ss.str()); + std::string save_name = FileFinder::GetSaveFilename(FileFinder::Save(), Player::load_game_id); Player::LoadSavegame(save_name, Player::load_game_id); } } diff --git a/src/scene_map.cpp b/src/scene_map.cpp index 9c9037af8c..ba3909851c 100644 --- a/src/scene_map.cpp +++ b/src/scene_map.cpp @@ -264,6 +264,10 @@ void Scene_Map::UpdateStage1(MapUpdateAsyncContext actx) { return; } + if (HandleLoadParallel()) { + return; + } + // On platforms with async loading (emscripten) graphical assets loaded this frame // may require us to wait for them to download before we can start the transitions. AsyncNext([this]() { UpdateStage2(); }); @@ -393,9 +397,30 @@ void Scene_Map::PerformAsyncTeleport(TeleportTarget original_tt) { AsyncNext(std::move(map_async_continuation)); } +bool Scene_Map::HandleLoadParallel() { + // Note: Depending on the individual implementation, all sorts of + // unexpected behaviors might arise. + // This code is only meant for emulating the behavior of RPG_RT + // patches which were coded to use the internal loading mechanics + // while also failing to add any safeguards & allowing the interpreter + // to resume execution during the loading process. + + if (!load_parallel_save_name.empty()) { + Player::LoadSavegame(load_parallel_save_name, 0); + load_parallel_save_name = {}; + + // Finish any active messages so that the interpreter isn't blocked + if (Game_Message::IsMessageActive()) { + Game_Message::GetWindow()->FinishMessageProcessing(); + } + return true; + } + return false; +} + template void Scene_Map::OnAsyncSuspend(F&& f, AsyncOp aop, bool is_preupdate) { - if (CheckSceneExit(aop)) { + if (CheckSceneExit(aop) || HandleLoadParallel()) { return; } @@ -410,10 +435,19 @@ void Scene_Map::OnAsyncSuspend(F&& f, AsyncOp aop, bool is_preupdate) { if (aop.GetType() == AsyncOp::eLoad) { auto savefs = FileFinder::Save(); - std::string save_name = Scene_Save::GetSaveFilename(savefs, aop.GetSaveSlot()); + std::string save_name = FileFinder::GetSaveFilename(savefs, aop.GetSaveSlot(), false); Player::LoadSavegame(save_name, aop.GetSaveSlot()); } + if (aop.GetType() == AsyncOp::eLoadParallel) { + // Set up emulation for a "Parallel Load". + // This is a delayed load that will either execute on the next async supend, + // or when the current interpreter frame has been completed. + // ( See notes on the declaration of Game_Interpreter_Shared::MakeLoadParallel() + // & on the implementation of Scene_Map::HandleLoadParallel() for more information) + load_parallel_save_name = FileFinder::GetSaveFilename(FileFinder::Save(), aop.GetSaveSlot(), false); + } + if (aop.GetType() == AsyncOp::eCloneMapEvent) { Game_Map::CloneMapEvent(aop.GetMapId(), aop.GetSourceEventId(), aop.GetX(), aop.GetY(), aop.GetTargetEventId(), aop.GetEventName()); } diff --git a/src/scene_map.h b/src/scene_map.h index 60833f8711..4131499815 100644 --- a/src/scene_map.h +++ b/src/scene_map.h @@ -88,6 +88,8 @@ class Scene_Map: public Scene { template void OnAsyncSuspend(F&& f, AsyncOp aop, bool is_preupdate); + bool HandleLoadParallel(); + void UpdateGraphics() override; std::unique_ptr message_window; @@ -100,6 +102,7 @@ class Scene_Map: public Scene { bool activate_inn = false; bool inn_started = false; Game_Clock::time_point inn_timer = {}; + std::string load_parallel_save_name = {}; }; #endif diff --git a/src/scene_menu.cpp b/src/scene_menu.cpp index 98c3b81128..3bdc16ad64 100644 --- a/src/scene_menu.cpp +++ b/src/scene_menu.cpp @@ -40,6 +40,8 @@ constexpr int menu_command_width = 88; constexpr int gold_window_width = 88; constexpr int gold_window_height = 32; +Scene_Menu::CommandOptionType Scene_Menu::force_cursor_index = Scene_Menu::CommandOption_None; + Scene_Menu::Scene_Menu(int menu_index) : menu_index(menu_index) { type = Scene::Menu; @@ -78,28 +80,30 @@ void Scene_Menu::CreateCommandWindow() { // Create Options Window std::vector options; + using Cmd = CommandOptionType; + if (Player::IsRPG2k()) { - command_options.push_back(Item); - command_options.push_back(Skill); - command_options.push_back(Equipment); - command_options.push_back(Save); + command_options.push_back(Cmd::Item); + command_options.push_back(Cmd::Skill); + command_options.push_back(Cmd::Equipment); + command_options.push_back(Cmd::Save); if (Player::player_config.settings_in_menu.Get()) { - command_options.push_back(Settings); + command_options.push_back(Cmd::Settings); } if (Player::debug_flag) { - command_options.push_back(Debug); + command_options.push_back(Cmd::Debug); } - command_options.push_back(Quit); + command_options.push_back(Cmd::Quit); } else { for (std::vector::iterator it = lcf::Data::system.menu_commands.begin(); it != lcf::Data::system.menu_commands.end(); ++it) { switch (*it) { - case Row: + case static_cast(Cmd::Row): if (Feature::HasRow()) { command_options.push_back((CommandOptionType)*it); } break; - case Wait: + case static_cast(Cmd::Wait): if (Feature::HasRpg2k3BattleSystem()) { command_options.push_back((CommandOptionType)*it); } @@ -110,46 +114,46 @@ void Scene_Menu::CreateCommandWindow() { } } if (Player::player_config.settings_in_menu.Get()) { - command_options.push_back(Settings); + command_options.push_back(Cmd::Settings); } if (Player::debug_flag) { - command_options.push_back(Debug); + command_options.push_back(Cmd::Debug); } - command_options.push_back(Quit); + command_options.push_back(Cmd::Quit); } // Add all menu items std::vector::iterator it; for (it = command_options.begin(); it != command_options.end(); ++it) { switch(*it) { - case Item: + case Cmd::Item: options.push_back(ToString(lcf::Data::terms.command_item)); break; - case Skill: + case Cmd::Skill: options.push_back(ToString(lcf::Data::terms.command_skill)); break; - case Equipment: + case Cmd::Equipment: options.push_back(ToString(lcf::Data::terms.menu_equipment)); break; - case Save: + case Cmd::Save: options.push_back(ToString(lcf::Data::terms.menu_save)); break; - case Status: + case Cmd::Status: options.push_back(ToString(lcf::Data::terms.status)); break; - case Row: + case Cmd::Row: options.push_back(ToString(lcf::Data::terms.row)); break; - case Order: + case Cmd::Order: options.push_back(ToString(lcf::Data::terms.order)); break; - case Wait: + case Cmd::Wait: options.push_back(ToString(Main_Data::game_system->GetAtbMode() == lcf::rpg::SaveSystem::AtbMode_atb_wait ? lcf::Data::terms.wait_on : lcf::Data::terms.wait_off)); break; - case Settings: + case Cmd::Settings: options.push_back("Settings"); break; - case Debug: + case Cmd::Debug: options.push_back("Debug"); break; default: @@ -161,22 +165,30 @@ void Scene_Menu::CreateCommandWindow() { command_window.reset(new Window_Command(options, menu_command_width)); command_window->SetX(Player::menu_offset_x); command_window->SetY(Player::menu_offset_y); - command_window->SetIndex(menu_index); + + if (force_cursor_index != CommandOption_None) { + if (auto idx = GetCommandIndex(force_cursor_index); idx != -1) { + command_window->SetIndex(idx); + } + force_cursor_index = CommandOption_None; + } else { + command_window->SetIndex(menu_index); + } // Disable items for (it = command_options.begin(); it != command_options.end(); ++it) { switch(*it) { - case Save: + case Cmd::Save: // If save is forbidden disable this item if (!Main_Data::game_system->GetAllowSave()) { command_window->DisableItem(it - command_options.begin()); } - case Wait: - case Quit: - case Settings: - case Debug: + case Cmd::Wait: + case Cmd::Quit: + case Cmd::Settings: + case Cmd::Debug: break; - case Order: + case Cmd::Order: if (Main_Data::game_party->GetActors().size() <= 1) { command_window->DisableItem(it - command_options.begin()); } @@ -191,6 +203,7 @@ void Scene_Menu::CreateCommandWindow() { } void Scene_Menu::UpdateCommand() { + using Cmd = CommandOptionType; if (Input::IsTriggered(Input::CANCEL)) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cancel)); Scene::Pop(); @@ -198,7 +211,7 @@ void Scene_Menu::UpdateCommand() { menu_index = command_window->GetIndex(); switch (command_options[menu_index]) { - case Item: + case Cmd::Item: if (Main_Data::game_party->GetActors().empty()) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); } else { @@ -206,10 +219,10 @@ void Scene_Menu::UpdateCommand() { Scene::Push(std::make_shared()); } break; - case Skill: - case Equipment: - case Status: - case Row: + case Cmd::Skill: + case Cmd::Equipment: + case Cmd::Status: + case Cmd::Row: if (Main_Data::game_party->GetActors().empty()) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); } else { @@ -219,7 +232,7 @@ void Scene_Menu::UpdateCommand() { menustatus_window->SetIndex(0); } break; - case Save: + case Cmd::Save: if (!Main_Data::game_system->GetAllowSave()) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); } else { @@ -227,7 +240,7 @@ void Scene_Menu::UpdateCommand() { Scene::Push(std::make_shared()); } break; - case Order: + case Cmd::Order: if (Main_Data::game_party->GetActors().size() <= 1) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); } else { @@ -235,21 +248,21 @@ void Scene_Menu::UpdateCommand() { Scene::Push(std::make_shared()); } break; - case Wait: + case Cmd::Wait: Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Main_Data::game_system->ToggleAtbMode(); command_window->SetItemText(menu_index, Main_Data::game_system->GetAtbMode() == lcf::rpg::SaveSystem::AtbMode_atb_wait ? lcf::Data::terms.wait_on : lcf::Data::terms.wait_off); break; - case Settings: + case Cmd::Settings: Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Decision)); Scene::Push(std::make_shared()); break; - case Debug: + case Cmd::Debug: Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Scene::Push(std::make_shared()); break; - case Quit: + case Cmd::Quit: Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Scene::Push(std::make_shared()); break; @@ -258,6 +271,7 @@ void Scene_Menu::UpdateCommand() { } void Scene_Menu::UpdateActorSelection() { + using Cmd = CommandOptionType; if (Input::IsTriggered(Input::CANCEL)) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cancel)); command_window->SetActive(true); @@ -265,7 +279,7 @@ void Scene_Menu::UpdateActorSelection() { menustatus_window->SetIndex(-1); } else if (Input::IsTriggered(Input::DECISION)) { switch (command_options[command_window->GetIndex()]) { - case Skill: + case Cmd::Skill: if (!menustatus_window->GetActor()->CanAct()) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); return; @@ -273,15 +287,15 @@ void Scene_Menu::UpdateActorSelection() { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Scene::Push(std::make_shared(Main_Data::game_party->GetActors(), menustatus_window->GetIndex())); break; - case Equipment: + case Cmd::Equipment: Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Scene::Push(std::make_shared(Main_Data::game_party->GetActors(), menustatus_window->GetIndex())); break; - case Status: + case Cmd::Status: Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Scene::Push(std::make_shared(Main_Data::game_party->GetActors(), menustatus_window->GetIndex())); break; - case Row: + case Cmd::Row: { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); // Don't allow entire party in the back row. @@ -313,3 +327,11 @@ void Scene_Menu::UpdateActorSelection() { menustatus_window->SetIndex(-1); } } + +int Scene_Menu::GetCommandIndex(CommandOptionType cmd) const { + auto it = std::find(command_options.begin(), command_options.end(), cmd); + if (it != command_options.end()) { + return (it - command_options.begin()); + } + return -1; +} diff --git a/src/scene_menu.h b/src/scene_menu.h index 0b7eec1d97..d896bf19ec 100644 --- a/src/scene_menu.h +++ b/src/scene_menu.h @@ -56,7 +56,7 @@ class Scene_Menu : public Scene { void UpdateActorSelection(); /** Options available in a Rpg2k3 menu. */ - enum CommandOptionType { + enum class CommandOptionType { Item = 1, Skill, Equipment, @@ -71,6 +71,11 @@ class Scene_Menu : public Scene { Settings = 101, }; + int GetCommandIndex(CommandOptionType cmd) const; + + static const CommandOptionType CommandOption_None = static_cast(0); + static CommandOptionType force_cursor_index; + private: /** Selected index on startup. */ int menu_index; diff --git a/src/scene_save.cpp b/src/scene_save.cpp index c802b103ad..2ea41a0aa0 100644 --- a/src/scene_save.cpp +++ b/src/scene_save.cpp @@ -65,19 +65,8 @@ void Scene_Save::Action(int index) { Scene::Pop(); } -std::string Scene_Save::GetSaveFilename(const FilesystemView& fs, int slot_id) { - const auto save_file = fmt::format("Save{:02d}.lsd", slot_id); - - std::string filename = fs.FindFile(save_file); - - if (filename.empty()) { - filename = save_file; - } - return filename; -} - bool Scene_Save::Save(const FilesystemView& fs, int slot_id, bool prepare_save) { - const auto filename = GetSaveFilename(fs, slot_id); + const auto filename = FileFinder::GetSaveFilename(fs, slot_id, false); Output::Debug("Saving to {}", filename); auto save_stream = FileFinder::Save().OpenOutputStream(filename); diff --git a/src/scene_save.h b/src/scene_save.h index 9fa5b2adc7..73620ae209 100644 --- a/src/scene_save.h +++ b/src/scene_save.h @@ -39,7 +39,6 @@ class Scene_Save : public Scene_File { void Action(int index) override; bool IsSlotValid(int index) override; - static std::string GetSaveFilename(const FilesystemView& tree, int slot_id); static bool Save(const FilesystemView& tree, int slot_id, bool prepare_save = true); static bool Save(std::ostream& os, int slot_id, bool prepare_save = true); }; diff --git a/src/scene_title.cpp b/src/scene_title.cpp index d43c8d9835..ce53d048b8 100644 --- a/src/scene_title.cpp +++ b/src/scene_title.cpp @@ -45,6 +45,8 @@ #include "baseui.h" #include +Scene_Title::CommandOptionType Scene_Title::force_cursor_index = Scene_Title::CommandOption_None; + Scene_Title::Scene_Title() { type = Scene::Title; } @@ -108,7 +110,7 @@ void Scene_Title::Continue(SceneType prev_scene) { } void Scene_Title::TransitionIn(SceneType prev_scene) { - if (Game_Battle::battle_test.enabled || !Check2k3ShowTitle() || Player::game_config.new_game.Get()) + if (Game_Battle::battle_test.enabled || CheckStartNewGame()) return; if (prev_scene == Scene::Load || Player::hide_title_flag) { @@ -133,7 +135,7 @@ void Scene_Title::vUpdate() { return; } - if (!Check2k3ShowTitle() || Player::game_config.new_game.Get()) { + if (CheckStartNewGame()) { Player::SetupNewGame(); if (Player::debug_flag && Player::hide_title_flag) { Scene::Push(std::make_shared()); @@ -170,12 +172,26 @@ void Scene_Title::vUpdate() { void Scene_Title::Refresh() { // Enable load game if available continue_enabled = FileFinder::HasSavegame(); - if (continue_enabled) { + + if (force_cursor_index != CommandOption_None) { + if (auto idx = GetCommandIndex(force_cursor_index); idx != -1) { + command_window->SetIndex(idx); + } + force_cursor_index = CommandOption_None; + } else if (continue_enabled) { command_window->SetIndex(1); } command_window->SetItemEnabled(1, continue_enabled); } +int Scene_Title::GetCommandIndex(CommandOptionType cmd) const { + auto it = std::find(command_options.begin(), command_options.end(), cmd); + if (it != command_options.end()) { + return (it - command_options.begin()); + } + return -1; +} + void Scene_Title::OnTranslationChanged() { Start(); @@ -211,35 +227,59 @@ void Scene_Title::RepositionWindow(Window_Command& window, bool center_vertical) void Scene_Title::CreateCommandWindow() { // Create Options Window std::vector options; - options.push_back(ToString(lcf::Data::terms.new_game)); - options.push_back(ToString(lcf::Data::terms.load_game)); + + using Cmd = CommandOptionType; // Reset index to fix issues on reuse. indices = CommandIndices(); + command_options.push_back(Cmd::NewGame); + command_options.push_back(Cmd::ContinueGame); + // Set "Import" based on metadata if (Player::meta->IsImportEnabled()) { - options.push_back(Player::meta->GetExVocabImportSaveTitleText()); - indices.import = indices.exit; - indices.exit++; + command_options.push_back(Cmd::Import); } - // Set "Settings" based on the configuration if (Player::player_config.settings_in_title.Get()) { - // FIXME: Translation? Not shown by default though - options.push_back("Settings"); - indices.settings = indices.exit; - indices.exit++; + command_options.push_back(Cmd::Settings); } - // Set "Translate" based on metadata if (Player::translation.HasTranslations() && Player::player_config.lang_select_in_title.Get()) { - options.push_back(Player::meta->GetExVocabTranslateTitleText()); - indices.translate = indices.exit; - indices.exit++; + command_options.push_back(Cmd::Translate); } - options.push_back(ToString(lcf::Data::terms.exit_game)); + command_options.push_back(Cmd::Exit); + + for (int i = 0; i < command_options.size(); ++i) { + switch (command_options[i]) { + case Cmd::NewGame: + indices.new_game = i; + options.push_back(ToString(lcf::Data::terms.new_game)); + break; + case Cmd::ContinueGame: + indices.continue_game = i; + options.push_back(ToString(lcf::Data::terms.load_game)); + break; + case Cmd::Import: + indices.import = i; + options.push_back(Player::meta->GetExVocabImportSaveTitleText()); + break; + case Cmd::Settings: + indices.settings = i; + // FIXME: Translation? Not shown by default though + options.push_back("Settings"); + break; + case Cmd::Translate: + indices.translate = i; + options.push_back(Player::meta->GetExVocabTranslateTitleText()); + break; + case Cmd::Exit: + indices.exit = i; + options.push_back(ToString(lcf::Data::terms.exit_game)); + break; + } + } command_window.reset(new Window_Command(options)); RepositionWindow(*command_window, Player::hide_title_flag); @@ -267,7 +307,7 @@ void Scene_Title::PlayTitleMusic() { bool Scene_Title::CheckEnableTitleGraphicAndMusic() { return Check2k3ShowTitle() && - !Player::game_config.new_game.Get() && + !CheckStartNewGame() && !Game_Battle::battle_test.enabled && !Player::hide_title_flag; } @@ -280,6 +320,10 @@ bool Scene_Title::CheckValidPlayerLocation() { return (lcf::Data::treemap.start.party_map_id > 0); } +bool Scene_Title::CheckStartNewGame() { + return (!Check2k3ShowTitle() || Player::game_config.new_game.Get()) && !Player::force_make_to_title_flag; +} + void Scene_Title::CommandNewGame() { if (!CheckValidPlayerLocation()) { Output::Warning("The game has no start location set."); @@ -341,4 +385,5 @@ void Scene_Title::OnTitleSpriteReady(FileRequestResult* result) { void Scene_Title::OnGameStart() { restart_title_cache = true; + Player::force_make_to_title_flag = false; } diff --git a/src/scene_title.h b/src/scene_title.h index 3d77ca9253..65f02cb869 100644 --- a/src/scene_title.h +++ b/src/scene_title.h @@ -79,6 +79,13 @@ class Scene_Title : public Scene { */ bool CheckValidPlayerLocation(); + /** + * Checks whether to immediately start into a new game. + * + * @return true if the title screen should be skipped + */ + bool CheckStartNewGame(); + /** * Option New Game. * Starts a new game. @@ -119,6 +126,20 @@ class Scene_Title : public Scene { */ void OnGameStart(); + enum class CommandOptionType { + NewGame = 1, + ContinueGame, + Import, + Settings, + Translate, + Exit + }; + + int GetCommandIndex(CommandOptionType cmd) const; + + static const CommandOptionType CommandOption_None = static_cast(0); + static CommandOptionType force_cursor_index; + /** * Moves a window (typically the New/Continue/Quit menu) to the middle or bottom-center of the screen. * @param window The window to resposition. @@ -142,15 +163,18 @@ class Scene_Title : public Scene { * Stored in a struct for easy resetting, as Scene_Title can be reused. */ struct CommandIndices { - int new_game = 0; - int continue_game = 1; + int new_game = 0; + int continue_game = 1; int import = -1; int settings = -1; int translate = -1; - int exit = 2; + int exit = 2; }; CommandIndices indices; + /** Options available in the menu. */ + std::vector command_options; + /** Contains the state of continue button. */ bool continue_enabled = false;