From a63fdba58e1467f18cfe68ed1637b17632c8627e Mon Sep 17 00:00:00 2001 From: florianessl Date: Tue, 4 Mar 2025 13:15:08 +0100 Subject: [PATCH 01/18] Refactor & fix: Moved several instances of safe file name creation to one call in FileFinder; Moved constant for save file count to "Player::Constants::MaxSaveFiles" (Many instances did not check the overridden count) --- src/filefinder.cpp | 22 ++++++++++++++++++---- src/filefinder.h | 3 +++ src/game_interpreter.cpp | 4 ++-- src/meta.cpp | 9 ++++----- src/player.cpp | 4 ++++ src/player.h | 4 ++++ src/scene_file.cpp | 8 ++------ src/scene_import.cpp | 7 +++---- src/scene_load.cpp | 3 +-- src/scene_logo.cpp | 9 ++------- src/scene_map.cpp | 2 +- src/scene_save.cpp | 13 +------------ src/scene_save.h | 1 - 13 files changed, 45 insertions(+), 44 deletions(-) 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_interpreter.cpp b/src/game_interpreter.cpp index c4ae07e809..8fa7cde775 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4277,7 +4277,7 @@ bool Game_Interpreter::CommandManiacGetSaveInfo(lcf::rpg::EventCommand const& co } auto savefs = FileFinder::Save(); - std::string save_name = Scene_Save::GetSaveFilename(savefs, save_number); + std::string save_name = FileFinder::GetSaveFilename(savefs, save_number, false); auto save_stream = FileFinder::Save().OpenInputStream(save_name); if (!save_stream) { @@ -4373,7 +4373,7 @@ bool Game_Interpreter::CommandManiacLoad(lcf::rpg::EventCommand const& com) { // 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); + std::string save_name = FileFinder::GetSaveFilename(savefs, slot, false); auto save_stream = FileFinder::Save().OpenInputStream(save_name); std::unique_ptr save = lcf::LSD_Reader::Load(save_stream, Player::encoding); 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/player.cpp b/src/player.cpp index 5557b24539..b2d3489d9a 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -1613,3 +1613,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..f3db96f22f 100644 --- a/src/player.h +++ b/src/player.h @@ -435,6 +435,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..f8996a39fd 100644 --- a/src/scene_map.cpp +++ b/src/scene_map.cpp @@ -410,7 +410,7 @@ 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()); } 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); }; From a1979acded33ccbbf3a921169015ee367ca4b906 Mon Sep 17 00:00:00 2001 From: florianessl Date: Tue, 4 Mar 2025 13:25:46 +0100 Subject: [PATCH 02/18] Refactor: Moved some parts of the custom "Load/Save " code from Maniac commands into "Game_Interpreter_Shared" (Many other patches implemented something similar before MP) --- src/game_interpreter.cpp | 33 ++++++---------------- src/game_interpreter_shared.cpp | 50 +++++++++++++++++++++++++++++++++ src/game_interpreter_shared.h | 7 +++++ 3 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 8fa7cde775..d8ef38ac83 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4271,25 +4271,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; - } - - auto savefs = FileFinder::Save(); - std::string save_name = FileFinder::GetSaveFilename(savefs, save_number, false); - auto save_stream = FileFinder::Save().OpenInputStream(save_name); + bool save_corrupted = false; + auto save = ValidateAndLoadSave("ManiacGetSaveInfo", FileFinder::Save(), save_number, save_corrupted); - 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 +4355,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 = FileFinder::GetSaveFilename(savefs, slot, false); - 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_shared.cpp b/src/game_interpreter_shared.cpp index ee923ada5b..7d3f43452b 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,55 @@ 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; +} + +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); +} + //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..c099263212 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,10 @@ 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); + AsyncOp MakeLoadOp(const char* caller, int save_slot); } inline bool Game_Interpreter_Shared::CheckOperator(int val, int val2, int op) { From b79fbcc344e037a61ee443c2c8b4422ed3dcaf9a Mon Sep 17 00:00:00 2001 From: florianessl Date: Tue, 4 Mar 2025 16:07:03 +0100 Subject: [PATCH 03/18] Refactor: Make execution of Ineluki scripts behave synchronously so emulation support for some external programs can be properly handled. --- src/game_ineluki.cpp | 46 ++++++++++++++++++++++++++-------------- src/game_ineluki.h | 5 ++++- src/game_interpreter.cpp | 21 +++++++++++++++++- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/game_ineluki.cpp b/src/game_ineluki.cpp index 7a8d660453..eabd773e5b 100644 --- a/src/game_ineluki.cpp +++ b/src/game_ineluki.cpp @@ -66,19 +66,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 +97,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 +157,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 +177,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 +195,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 +211,7 @@ bool Game_Ineluki::Execute(std::string_view ini_file) { } } - return true; + return {}; } bool Game_Ineluki::ExecuteScriptList(std::string_view list_file) { @@ -403,6 +404,19 @@ 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 { + 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 d8ef38ac83..cf7f8d3f1d 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -1991,7 +1991,26 @@ 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 (Player::IsPatchKeyPatch() && EndsWith(sound_name, ".script")) { + // Is a Ineluki Script File + FileRequestAsync* request = AsyncHandler::RequestFile("Sound", sound_name); + request->SetImportantFile(true); + request->Start(); + + if (!request->IsReady()) { + _async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + auto op = Main_Data::game_ineluki->Execute(FileFinder::FindSound(sound_name)); + if (op.IsActive()) { + _async_op = op; + } + return true; + } + + sound.name = ToString(sound_name); sound.volume = ValueOrVariableBitfield(com, 3, 1, 0); sound.tempo = ValueOrVariableBitfield(com, 3, 2, 1); From 1747a81714197ff2c00135bcc5e5549e1800d02a Mon Sep 17 00:00:00 2001 From: florianessl Date: Tue, 4 Mar 2025 16:14:55 +0100 Subject: [PATCH 04/18] Fix: Ineluki scripts could also be handled via PlayBGM (Fixes usage in game "Earthcraft") --- src/game_interpreter.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index cf7f8d3f1d..5bd1914bf2 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -1972,7 +1972,26 @@ bool Game_Interpreter::CommandWait(lcf::rpg::EventCommand const& com) { // code 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 (Player::IsPatchKeyPatch() && EndsWith(music_name, ".script")) { + // Is a Ineluki Script File + FileRequestAsync* request = AsyncHandler::RequestFile("Music", music_name); + request->SetImportantFile(true); + request->Start(); + + if (!request->IsReady()) { + _async_op = AsyncOp::MakeYieldRepeat(); + return true; + } + auto op = Main_Data::game_ineluki->Execute(FileFinder::FindMusic(music_name)); + if (op.IsActive()) { + _async_op = op; + } + return true; + } + + music.name = ToString(music_name); music.fadein = ValueOrVariableBitfield(com, 4, 1, 0); music.volume = ValueOrVariableBitfield(com, 4, 2, 1); From 44430020c6d7228e4345ecb73f12e68fe7783ed7 Mon Sep 17 00:00:00 2001 From: florianessl Date: Tue, 4 Mar 2025 16:18:57 +0100 Subject: [PATCH 05/18] Implemented some commonly used "ppcomp" commands and added a simple heuristic check for the "full" version of PowerPatch (unsupported) --- CMakeLists.txt | 2 + Makefile.am | 2 + src/game_ineluki.cpp | 6 + src/game_interpreter_shared.cpp | 27 ++++ src/game_interpreter_shared.h | 1 + src/game_powerpatch.cpp | 240 ++++++++++++++++++++++++++++++++ src/game_powerpatch.h | 99 +++++++++++++ src/player.cpp | 4 + 8 files changed, 381 insertions(+) create mode 100644 src/game_powerpatch.cpp create mode 100644 src/game_powerpatch.h 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/game_ineluki.cpp b/src/game_ineluki.cpp index eabd773e5b..12e0440864 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" @@ -410,6 +411,11 @@ AsyncOp Game_Ineluki::ExecProgram(std::string_view command) { Player::exit_flag = true; } else if (StartsWith(command, "savecount.dat")) { // no-op, detected through saves.script access + } 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); } diff --git a/src/game_interpreter_shared.cpp b/src/game_interpreter_shared.cpp index 7d3f43452b..d9579d8645 100644 --- a/src/game_interpreter_shared.cpp +++ b/src/game_interpreter_shared.cpp @@ -269,6 +269,33 @@ std::unique_ptr Game_Interpreter_Shared::ValidateAndLoadSave(con 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); diff --git a/src/game_interpreter_shared.h b/src/game_interpreter_shared.h index c099263212..eaea781394 100644 --- a/src/game_interpreter_shared.h +++ b/src/game_interpreter_shared.h @@ -109,6 +109,7 @@ namespace Game_Interpreter_Shared { 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); } diff --git a/src/game_powerpatch.cpp b/src/game_powerpatch.cpp new file mode 100644 index 0000000000..934ed98571 --- /dev/null +++ b/src/game_powerpatch.cpp @@ -0,0 +1,240 @@ +/* + * 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 "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); + } +} + +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; + } + // FIXME: 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::MakeLoadOp("PowerPatch Load", slot); + break; + } + case Type::CheckSave: { + int slot = atoi(args[0].c_str()); + int dest_sw = atoi(args[1].c_str()); + + auto exists = !FileFinder::GetSaveFilename(FileFinder::Save(), 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: { + int cursor = args.size() >= 1 ? atoi(args[0].c_str()) : 0; + Game_Map::GetInterpreter().RequestMainMenuScene(); + break; + } + case Type::CallTitleScreen: { + int cursor = args.size() >= 1 ? atoi(args[0].c_str()) : 0; + 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; + } + default: + return false; + } + + return true; +} diff --git a/src/game_powerpatch.h b/src/game_powerpatch.h new file mode 100644 index 0000000000..b693dafee5 --- /dev/null +++ b/src/game_powerpatch.h @@ -0,0 +1,99 @@ +/* + * 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 "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, 0, "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" } + }}; +}; + +#endif diff --git a/src/player.cpp b/src/player.cpp index b2d3489d9a..df38f408d2 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -827,6 +827,10 @@ 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."); + } } game_config.PrintActivePatches(); From ababdf6d9dae2383f27af68fe07a32793c5e2988 Mon Sep 17 00:00:00 2001 From: florianessl Date: Tue, 4 Mar 2025 17:54:57 +0100 Subject: [PATCH 06/18] KeyPatch: Only force synchronous mode when one of a few known external tools is found in the game directory --- src/game_config_game.h | 1 + src/game_interpreter.cpp | 56 ++++++++++++++++++++-------------------- src/game_system.cpp | 10 +++++++ src/player.cpp | 13 ++++++++++ 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/game_config_game.h b/src/game_config_game.h index fe9e3ec0c0..f96bb63f11 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -45,6 +45,7 @@ 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 }; diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 5bd1914bf2..14d8348de4 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -1970,24 +1970,37 @@ 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; auto music_name = CommandStringOrVariableBitfield(com, 4, 0, 5); - if (Player::IsPatchKeyPatch() && EndsWith(music_name, ".script")) { - // Is a Ineluki Script File - FileRequestAsync* request = AsyncHandler::RequestFile("Music", music_name); - request->SetImportantFile(true); - request->Start(); - - if (!request->IsReady()) { - _async_op = AsyncOp::MakeYieldRepeat(); - return true; - } - auto op = Main_Data::game_ineluki->Execute(FileFinder::FindMusic(music_name)); - if (op.IsActive()) { - _async_op = op; - } + if (InelukiKeyPatch::HandleScriptFile(music_name, true, _async_op)) { return true; } @@ -2012,20 +2025,7 @@ bool Game_Interpreter::CommandPlaySound(lcf::rpg::EventCommand const& com) { // lcf::rpg::Sound sound; auto sound_name = CommandStringOrVariableBitfield(com, 3, 0, 4); - if (Player::IsPatchKeyPatch() && EndsWith(sound_name, ".script")) { - // Is a Ineluki Script File - FileRequestAsync* request = AsyncHandler::RequestFile("Sound", sound_name); - request->SetImportantFile(true); - request->Start(); - - if (!request->IsReady()) { - _async_op = AsyncOp::MakeYieldRepeat(); - return true; - } - auto op = Main_Data::game_ineluki->Execute(FileFinder::FindSound(sound_name)); - if (op.IsActive()) { - _async_op = op; - } + if (InelukiKeyPatch::HandleScriptFile(sound_name, false, _async_op)) { return true; } 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/player.cpp b/src/player.cpp index df38f408d2..f0e3c6d173 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -831,6 +831,19 @@ void Player::CreateGameObjects() { if (!FileFinder::Game().FindFile("powerp.oex").empty()) { Output::Warning("This game uses Power Patch 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", "savecount", "LS"); + + 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; + } + } + } } game_config.PrintActivePatches(); From 2e77ba0a77d46fc6193e25e4574d0e9cc9192c9c Mon Sep 17 00:00:00 2001 From: florianessl Date: Wed, 5 Mar 2025 14:07:17 +0100 Subject: [PATCH 07/18] Added new AsyncOp "eLoadParallel" for emulating PowerPatch "LOAD" behavior. (Could maybe also be used for improving compatibility the Maniac Load command) --- src/async_op.h | 12 +++++++++-- src/game_interpreter_shared.cpp | 19 +++++++++++++++++ src/game_interpreter_shared.h | 11 ++++++++++ src/game_powerpatch.cpp | 6 ++++-- src/player.cpp | 25 ++++++++++++++++------- src/player.h | 2 +- src/scene_map.cpp | 36 ++++++++++++++++++++++++++++++++- src/scene_map.h | 3 +++ 8 files changed, 101 insertions(+), 13 deletions(-) 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/game_interpreter_shared.cpp b/src/game_interpreter_shared.cpp index d9579d8645..532b2a3e15 100644 --- a/src/game_interpreter_shared.cpp +++ b/src/game_interpreter_shared.cpp @@ -315,6 +315,25 @@ AsyncOp Game_Interpreter_Shared::MakeLoadOp(const char* caller, int save_slot) { 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 eaea781394..6f1bfd371b 100644 --- a/src/game_interpreter_shared.h +++ b/src/game_interpreter_shared.h @@ -110,7 +110,18 @@ namespace Game_Interpreter_Shared { 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 index 934ed98571..165bd6cbdf 100644 --- a/src/game_powerpatch.cpp +++ b/src/game_powerpatch.cpp @@ -124,7 +124,7 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a if (!save) { return true; } - // FIXME: In RPG_RT the loading operation happens asynchronously while the + // 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" @@ -133,7 +133,7 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a // 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::MakeLoadOp("PowerPatch Load", slot); + async_op = Game_Interpreter_Shared::MakeLoadParallel("PowerPatch Load", slot); break; } case Type::CheckSave: { @@ -207,11 +207,13 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a break; case Type::CallGameMenu: { int cursor = args.size() >= 1 ? atoi(args[0].c_str()) : 0; + //TODO: implement cursor Game_Map::GetInterpreter().RequestMainMenuScene(); break; } case Type::CallTitleScreen: { int cursor = args.size() >= 1 ? atoi(args[0].c_str()) : 0; + //TODO: implement cursor async_op = AsyncOp::MakeToTitle(); break; } diff --git a/src/player.cpp b/src/player.cpp index f0e3c6d173..2b6b83dfc5 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -834,7 +834,9 @@ void Player::CreateGameObjects() { if (game_config.patch_key_patch.Get()) { auto exe_util_types = Utils::MakeSvArray(".exe", ".dll", ".dat"); - auto exe_util_names = Utils::MakeSvArray("ppcomp", "sfx", "savecount", "LS"); + 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()) { @@ -843,6 +845,15 @@ void Player::CreateGameObjects() { 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; + } + } + } } } @@ -1138,12 +1149,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 @@ -1201,7 +1212,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); } @@ -1223,12 +1234,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); @@ -1240,7 +1251,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)); } } diff --git a/src/player.h b/src/player.h index f3db96f22f..b304e18b17 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 diff --git a/src/scene_map.cpp b/src/scene_map.cpp index f8996a39fd..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; } @@ -414,6 +439,15 @@ void Scene_Map::OnAsyncSuspend(F&& f, AsyncOp aop, bool is_preupdate) { 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 From 842ab86d5e1343110eb814ea405ba908d82a28e3 Mon Sep 17 00:00:00 2001 From: florianessl Date: Wed, 5 Mar 2025 14:08:49 +0100 Subject: [PATCH 08/18] KeyPatch: Implemented commonly encountered command "LS.DAT" (Calling the Load scene) --- src/game_ineluki.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/game_ineluki.cpp b/src/game_ineluki.cpp index 12e0440864..87c674d324 100644 --- a/src/game_ineluki.cpp +++ b/src/game_ineluki.cpp @@ -28,6 +28,7 @@ #include "system.h" #include +#include namespace { #if defined(SUPPORT_KEYBOARD) @@ -411,6 +412,10 @@ AsyncOp Game_Ineluki::ExecProgram(std::string_view command) { 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) { From 892c6926ac8f2ad7650d0cc102ea0b25196ba01b Mon Sep 17 00:00:00 2001 From: florianessl Date: Wed, 5 Mar 2025 14:25:18 +0100 Subject: [PATCH 09/18] Implemented support for the "BetterAEP" patch (Very common among games that also make use of "ppcomp") --- src/game_config_game.cpp | 18 ++++++++++++++++++ src/game_config_game.h | 1 + src/game_interpreter.cpp | 12 ++++++++++++ src/player.cpp | 7 +++++++ 4 files changed, 38 insertions(+) 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 f96bb63f11..8d510dbb43 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -49,6 +49,7 @@ struct Game_ConfigGame { 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 }; // Command line only BoolConfigParam patch_support{ "Support patches", "When OFF all patch support is disabled", "", "", true }; diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 14d8348de4..c973b8d965 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; @@ -2040,6 +2041,17 @@ 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: + Scene::instance->SetRequestedScene(std::make_shared()); + return true; + case 2: + Player::exit_flag = true; + return true; + } + } + EndEventProcessing(); return true; } diff --git a/src/player.cpp b/src/player.cpp index 2b6b83dfc5..b0137b6a52 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -855,6 +855,10 @@ void Player::CreateGameObjects() { } } } + + if (Player::game_config.patch_better_aep.Get()) { + Player::game_config.new_game.Set(true); + } } game_config.PrintActivePatches(); @@ -1481,6 +1485,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. From e88cc18080a7ea0d76ac4544f5cb9de1fc24af55 Mon Sep 17 00:00:00 2001 From: florianessl Date: Wed, 5 Mar 2025 14:37:45 +0100 Subject: [PATCH 10/18] Fixing some GCC build issues that MSVC doesn't care about. --- src/game_powerpatch.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/game_powerpatch.cpp b/src/game_powerpatch.cpp index 165bd6cbdf..7d63772159 100644 --- a/src/game_powerpatch.cpp +++ b/src/game_powerpatch.cpp @@ -58,7 +58,7 @@ namespace { } 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) { + 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()) { @@ -101,7 +101,7 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a 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(); + auto fs = FileFinder::Save(); if (slot == 0) { slot = Game_Interpreter_Shared::GetLatestSaveSlot(fs); } @@ -116,7 +116,7 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a case Type::Load: { int slot = args.size() >= 1 ? atoi(args[0].c_str()) : 0; - auto& fs = FileFinder::Save(); + auto fs = FileFinder::Save(); if (slot == 0) { slot = Game_Interpreter_Shared::GetLatestSaveSlot(fs); } @@ -140,7 +140,8 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a int slot = atoi(args[0].c_str()); int dest_sw = atoi(args[1].c_str()); - auto exists = !FileFinder::GetSaveFilename(FileFinder::Save(), slot, true).empty(); + auto fs = FileFinder::Save(); + auto exists = !FileFinder::GetSaveFilename(fs, slot, true).empty(); Main_Data::game_switches->Set(dest_sw, exists); break; } @@ -148,7 +149,7 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a int slot = atoi(args[0].c_str()); int dest_slot = atoi(args[1].c_str()); - auto& fs = FileFinder::Save(); + auto fs = FileFinder::Save(); auto save = Game_Interpreter_Shared::ValidateAndLoadSave("PowerPatch CopySave", fs, slot); if (!save) { return true; @@ -174,7 +175,7 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a int slot = atoi(args[0].c_str()); int dest_v = atoi(args[1].c_str()); - auto& fs = FileFinder::Save(); + auto fs = FileFinder::Save(); auto save = Game_Interpreter_Shared::ValidateAndLoadSave("PowerPatch GetSaveDateTime", fs, slot); if (!save) { return true; From 1648060f042df6c7297a6f0dbd2511c75ddbb6c5 Mon Sep 17 00:00:00 2001 From: florianessl Date: Wed, 5 Mar 2025 14:39:53 +0100 Subject: [PATCH 11/18] Commented out all the code related to checking for the new Ineluki-Script handling (Re-enabling it by default, for easier testing) --- src/game_config_game.h | 2 +- src/game_interpreter.cpp | 8 ++++---- src/player.cpp | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/game_config_game.h b/src/game_config_game.h index 8d510dbb43..120c6b8d81 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -45,7 +45,7 @@ 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_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 }; diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index c973b8d965..6c5db62f8c 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -1974,10 +1974,10 @@ bool Game_Interpreter::CommandWait(lcf::rpg::EventCommand const& com) { // code 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; - } + //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); diff --git a/src/player.cpp b/src/player.cpp index b0137b6a52..986c48340e 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -832,7 +832,7 @@ void Player::CreateGameObjects() { Output::Warning("This game uses Power Patch and might not run properly."); } - if (game_config.patch_key_patch.Get()) { + /*if (game_config.patch_key_patch.Get()) { auto exe_util_types = Utils::MakeSvArray(".exe", ".dll", ".dat"); auto exe_util_names = Utils::MakeSvArray("ppcomp", "sfx"); @@ -854,7 +854,7 @@ void Player::CreateGameObjects() { } } } - } + }*/ if (Player::game_config.patch_better_aep.Get()) { Player::game_config.new_game.Set(true); From 558d6f3911449a5bc45b9ebb9088c2405a8dc376 Mon Sep 17 00:00:00 2001 From: florianessl Date: Wed, 5 Mar 2025 19:27:35 +0100 Subject: [PATCH 12/18] Fix Emscripten build --- src/platform/emscripten/interface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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))); From a9573b50400a51ba2f063de0205df0b2233ce410 Mon Sep 17 00:00:00 2001 From: florianessl Date: Wed, 5 Mar 2025 20:22:59 +0100 Subject: [PATCH 13/18] Minor: Also print a compatility warning if PowerMode2003 is encountered. --- src/player.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/player.cpp b/src/player.cpp index 986c48340e..7f60812668 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -832,6 +832,12 @@ void Player::CreateGameObjects() { 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"); From 0fb6ebeb43b14adde5b6899f63c5fdd759c8f05a Mon Sep 17 00:00:00 2001 From: florianessl Date: Mon, 10 Mar 2025 16:08:51 +0100 Subject: [PATCH 14/18] BetterAEP: Emulate patch "CustomSaveLoad" which depends on BetterAEP --- src/game_config_game.h | 1 + src/game_interpreter.cpp | 8 ++++++++ src/game_interpreter_map.cpp | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/src/game_config_game.h b/src/game_config_game.h index 120c6b8d81..8828cdfd94 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -50,6 +50,7 @@ struct Game_ConfigGame { 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_interpreter.cpp b/src/game_interpreter.cpp index 6c5db62f8c..3797a836b6 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -2044,11 +2044,19 @@ bool Game_Interpreter::CommandEndEventProcessing(lcf::rpg::EventCommand const& / 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; } } 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; From 70579c2807e2f2e24e49c7a021bfae2c61bbdea8 Mon Sep 17 00:00:00 2001 From: florianessl Date: Mon, 10 Mar 2025 16:15:24 +0100 Subject: [PATCH 15/18] Scene_Title: Refactor handling of command options: - Make options code more dynamic, similar to Scene_Menu, so the option positions could theoretically be swapped or removed entirely - Added static field "force_cursor_index" to allow overriding the default cursor position --- src/scene_title.cpp | 82 ++++++++++++++++++++++++++++++++++----------- src/scene_title.h | 30 +++++++++++++++-- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/scene_title.cpp b/src/scene_title.cpp index d43c8d9835..7fce799e5c 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(); +} + void Scene_Title::CommandNewGame() { if (!CheckValidPlayerLocation()) { Output::Warning("The game has no start location set."); 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; From fa01f3aed2dbac86b4da17231668f9f137681f14 Mon Sep 17 00:00:00 2001 From: florianessl Date: Mon, 10 Mar 2025 16:16:30 +0100 Subject: [PATCH 16/18] PPCOMP: Implemented setting of cursor position for subcommands "CallGameMenu" & "CallTitleScreen" --- src/game_powerpatch.cpp | 23 +++++++-- src/player.cpp | 1 + src/player.h | 3 ++ src/scene_menu.cpp | 110 ++++++++++++++++++++++++---------------- src/scene_menu.h | 7 ++- src/scene_title.cpp | 3 +- 6 files changed, 96 insertions(+), 51 deletions(-) diff --git a/src/game_powerpatch.cpp b/src/game_powerpatch.cpp index 7d63772159..ec3a78a683 100644 --- a/src/game_powerpatch.cpp +++ b/src/game_powerpatch.cpp @@ -25,6 +25,8 @@ #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" @@ -207,14 +209,25 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a Scene::instance->SetRequestedScene(std::make_shared()); break; case Type::CallGameMenu: { - int cursor = args.size() >= 1 ? atoi(args[0].c_str()) : 0; - //TODO: implement cursor - Game_Map::GetInterpreter().RequestMainMenuScene(); + 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: { - int cursor = args.size() >= 1 ? atoi(args[0].c_str()) : 0; - //TODO: implement cursor + 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; } diff --git a/src/player.cpp b/src/player.cpp index 7f60812668..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 diff --git a/src/player.h b/src/player.h index b304e18b17..e78e6118be 100644 --- a/src/player.h +++ b/src/player.h @@ -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 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_title.cpp b/src/scene_title.cpp index 7fce799e5c..ce53d048b8 100644 --- a/src/scene_title.cpp +++ b/src/scene_title.cpp @@ -321,7 +321,7 @@ bool Scene_Title::CheckValidPlayerLocation() { } bool Scene_Title::CheckStartNewGame() { - return !Check2k3ShowTitle() || Player::game_config.new_game.Get(); + return (!Check2k3ShowTitle() || Player::game_config.new_game.Get()) && !Player::force_make_to_title_flag; } void Scene_Title::CommandNewGame() { @@ -385,4 +385,5 @@ void Scene_Title::OnTitleSpriteReady(FileRequestResult* result) { void Scene_Title::OnGameStart() { restart_title_cache = true; + Player::force_make_to_title_flag = false; } From ff8731131e1c062740d62738858d553d79c39da7 Mon Sep 17 00:00:00 2001 From: florianessl Date: Sun, 23 Mar 2025 14:31:12 +0100 Subject: [PATCH 17/18] PPCOMP: Implemented command "SimulateKeyPress" (Needs testing) --- src/game_ineluki.cpp | 7 +++ src/game_powerpatch.cpp | 136 ++++++++++++++++++++++++++++++++++++++++ src/game_powerpatch.h | 9 ++- 3 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/game_ineluki.cpp b/src/game_ineluki.cpp index 87c674d324..7848050d6c 100644 --- a/src/game_ineluki.cpp +++ b/src/game_ineluki.cpp @@ -341,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() { diff --git a/src/game_powerpatch.cpp b/src/game_powerpatch.cpp index ec3a78a683..45cd324d0f 100644 --- a/src/game_powerpatch.cpp +++ b/src/game_powerpatch.cpp @@ -57,8 +57,115 @@ namespace { 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; @@ -248,9 +355,38 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a 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 index b693dafee5..bbb44a4b6e 100644 --- a/src/game_powerpatch.h +++ b/src/game_powerpatch.h @@ -22,6 +22,7 @@ #include #include "async_op.h" +#include "game_ineluki.h" #include "string_view.h" #include "span.h" @@ -84,7 +85,7 @@ namespace Game_PowerPatch { { PPC_CommandType::CallSaveMenu, 0, "CALLSAVEMENU" }, { PPC_CommandType::CallGameMenu, 0, "CALLGAMEMENU" }, { PPC_CommandType::CallTitleScreen, 0, "CALLTITLESCREEN" }, - { PPC_CommandType::SimulateKeyPress, 0, "SIMULATEKEYPRESS" }, + { PPC_CommandType::SimulateKeyPress, 2, "SIMULATEKEYPRESS" }, { PPC_CommandType::ChangeFunctionKey, 0, "CHANGEFUNCTIONKEY" }, { PPC_CommandType::SetTitleBGM, 1, "SETTITLEBGM" }, { PPC_CommandType::SetTitleScreen, 1, "SETTITLESCREEN" }, @@ -94,6 +95,12 @@ namespace Game_PowerPatch { { 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 From 99c559d073bc59c8e3e0e225086f640ac8426d0f Mon Sep 17 00:00:00 2001 From: florianessl Date: Mon, 24 Mar 2025 16:42:06 +0100 Subject: [PATCH 18/18] PPCOMP "CallTitleScreen": Comment out the title force flag --- src/game_powerpatch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game_powerpatch.cpp b/src/game_powerpatch.cpp index 45cd324d0f..c81b80e393 100644 --- a/src/game_powerpatch.cpp +++ b/src/game_powerpatch.cpp @@ -334,7 +334,7 @@ bool Game_PowerPatch::Execute(PPC_CommandType command, Span a Scene_Title::force_cursor_index = Scene_Title::CommandOptionType::NewGame; } } - Player::force_make_to_title_flag = true; + //Player::force_make_to_title_flag = true; async_op = AsyncOp::MakeToTitle(); break; }