From e89f80dce66b17704b2e722b5964d0a00792409e Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:59:28 -0300 Subject: [PATCH] Implement Partial Maniac Patch game option commands Added support for various Maniac Patch runtime engine settings via a new command handler in Game_Interpreter. Introduced new Game_System methods for pause screen when focus lost, fatal mix settings, fast-forward text, frame skip mode and message mouse disabling. Updated Player and Window_Message to respect these options, including frame skipping and fast-forwarding message text. missing features are: battle origin, and message face size. --- src/game_interpreter.cpp | 92 +++++++++++++++++++++++++++++++++++++--- src/game_system.cpp | 45 ++++++++++++++++++++ src/game_system.h | 18 ++++++++ src/player.cpp | 25 ++++++++++- src/player.h | 9 ++++ src/window_message.cpp | 21 ++++++++- 6 files changed, 201 insertions(+), 9 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 3e4842ba67..901a2d2754 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -4981,17 +4981,95 @@ bool Game_Interpreter::CommandManiacSetGameOption(lcf::rpg::EventCommand const& return true; } - int operation = com.parameters[1]; - //int value = ValueOrVariable(com.parameters[0], com.parameters[2]); + // This command changes various engine settings at runtime, specific to the Maniac Patch. + // The specific option is determined by `parameters[1]`. + enum class SetGameOptionType { + RunWhenInactive = 0, + FatalMix = 1, + PicturesLimit = 2, + SkipFrames = 3, + MouseTextMessage = 4, + BattleScreenOrigin = 5, + AnimationLimit = 6, + WindowFaceSize = 7 + }; - switch (operation) { - case 2: // Change Picture Limit (noop, we support arbitrary amount of pictures) + const auto option_type = static_cast(com.parameters[1]); + + switch (option_type) { + case SetGameOptionType::RunWhenInactive: { // .runWhenInactive or .pauseWhenInactive + const bool run_when_inactive = (ValueOrVariable(com.parameters[0], com.parameters[2]) != 0); + + if (DisplayUi) { + DisplayUi->SetPauseWhenFocusLost(!run_when_inactive); + } + return true; + } + case SetGameOptionType::FatalMix: { // .fatal fps, testPlayMode, fastForwardText + const int fps = ValueOrVariableBitfield(com, 0, 0, 2); + const int test_play_mode = com.parameters[3] & 3; // 0: From Game, 1: Off, 2: On + const bool enable_fast_forward_text = (com.parameters[3] & 16) != 0; + + if (fps > 0) { + Game_Clock::SetGameSpeedFactor(static_cast(fps) / 60.0f); + } + + switch (test_play_mode) { + case 0: + Player::debug_flag = Player::program_test_play; break; - default: - Output::Warning("Maniac SetGameOption: Operation {} not supported", operation); + case 1: + Player::debug_flag = false; + break; + case 2: + Player::debug_flag = true; + break; + } + + Main_Data::game_system->SetFastForwardText(enable_fast_forward_text); + return true; + } + case SetGameOptionType::PicturesLimit: { // .picLimit limit + // No-op by design. EasyRPG Player dynamically allocates pictures. + return true; + } + case SetGameOptionType::SkipFrames: { // .fullFrame, .oneFifth, .oneThird, .oneHalf + const int skip_mode = com.parameters[2]; + Main_Data::game_system->SetFrameSkipMode(skip_mode); + return true; } + case SetGameOptionType::MouseTextMessage: { // .mouse.disableMsgProcession value + const bool disable_mouse_for_messages = (ValueOrVariableBitfield(com, 0, 0, 2) != 0); + Main_Data::game_system->SetMessageMouseDisabled(disable_mouse_for_messages); + return true; + } + case SetGameOptionType::BattleScreenOrigin: { // .btlOrigin position + // 0: center, 1: topLeft, 2: bottomLeft, 3: topRight, 4: bottomRight, + // 5: top, 6: bottom, 7: left, 8: right + const int battle_origin = com.parameters[2]; - return true; + // TODO: Store this value, likely in `Game_System`, and have `Scene_Battle` use it + // to adjust the layout of its windows and sprites during initialization. + Output::Warning("Maniac Patch - Command SetGameOption: Reposition Battle UI (position: {}) is not yet implemented.", battle_origin); + return true; + } + case SetGameOptionType::AnimationLimit: { // .animLimit limit + // No-op by design. EasyRPG Player handles battle animations dynamically. + return true; + } + case SetGameOptionType::WindowFaceSize: { // .winFaceSize width, height + const int width = ValueOrVariableBitfield(com, 0, 0, 2); + const int height = ValueOrVariableBitfield(com, 0, 1, 3); + + // TODO: Store these values in Game_System (e.g., `message_face_width`/`height`). + // `Window_Message::Refresh` must then use these values when drawing the face graphic. + Output::Warning("Maniac Patch - Command SetGameOption: Custom WinFaceSize ({}x{}) is not yet implemented.", width, height); + return true; + } + default: + Output::Warning("Maniac SetGameOption: Unknown Operation {}", static_cast(option_type)); + return true; + } } bool Game_Interpreter::CommandManiacControlStrings(lcf::rpg::EventCommand const& com) { diff --git a/src/game_system.cpp b/src/game_system.cpp index 3482f7f07c..ff108ec686 100644 --- a/src/game_system.cpp +++ b/src/game_system.cpp @@ -649,3 +649,48 @@ bool Game_System::IsMessageTransparent() { return data.message_transparent != 0; } +void Game_System::SetFastForwardText(bool enabled) { + data.maniac_fast_forward_text = enabled; +} + +bool Game_System::GetFastForwardText() const { + return data.maniac_fast_forward_text; +} + +void Game_System::SetFrameSkipMode(int mode) { + data.maniac_frame_skip_mode = mode; + Player::SetFrameSkip(mode); +} + +int Game_System::GetFrameSkipMode() const { + return data.maniac_frame_skip_mode; +} + +void Game_System::SetMessageMouseDisabled(bool disabled) { + message_mouse_disabled = disabled; +} + +bool Game_System::IsMessageMouseDisabled() const { + return message_mouse_disabled; +} + +void Game_System::SetBattleOrigin(int origin) { + data.maniac_battle_origin = origin; +} + +int Game_System::GetBattleOrigin() const { + return data.maniac_battle_origin; +} + +void Game_System::SetMessageFaceSize(int width, int height) { + data.message_face_width = width; + data.message_face_height = height; +} + +int Game_System::GetMessageFaceWidth() const { + return data.message_face_width; +} + +int Game_System::GetMessageFaceHeight() const { + return data.message_face_height; +} diff --git a/src/game_system.h b/src/game_system.h index a15e006c83..1dbbedd3e9 100644 --- a/src/game_system.h +++ b/src/game_system.h @@ -436,6 +436,24 @@ class Game_System { /** @return Whether the game was loaded from a savegame in the current frame */ bool IsLoadedThisFrame() const; + void SetFastForwardText(bool enabled); + bool GetFastForwardText() const; + + void SetFrameSkipMode(int mode); + int GetFrameSkipMode() const; + + void SetMessageMouseDisabled(bool disabled); + bool IsMessageMouseDisabled() const; + + void SetBattleOrigin(int origin); + int GetBattleOrigin() const; + + void SetMessageFaceSize(int width, int height); + int GetMessageFaceWidth() const; + int GetMessageFaceHeight() const; + + bool message_mouse_disabled = false; + private: std::string InelukiReadLink(Filesystem_Stream::InputStream& stream); diff --git a/src/player.cpp b/src/player.cpp index bb4799c4d7..1332b55f0c 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -133,6 +133,13 @@ namespace Player { int rng_seed = -1; Game_ConfigPlayer player_config; Game_ConfigGame game_config; + bool program_test_play = false; + + namespace { + int frame_skip_counter = 0; + int frame_skip_rate = 1; + } + #ifdef EMSCRIPTEN std::string emscripten_game_name; #endif @@ -263,7 +270,11 @@ void Player::MainLoop() { Input::UpdateSystem(); } - Player::Draw(); + frame_skip_counter = (frame_skip_counter + 1) % frame_skip_rate; + if (frame_skip_counter == 0) { + Player::Draw(); + } + Scene::old_instances.clear(); @@ -294,6 +305,17 @@ void Player::MainLoop() { } } +void Player::SetFrameSkip(int mode) { + switch (mode) { + case 0: frame_skip_rate = 1; break; // Full + case 1: frame_skip_rate = 5; break; // 1/5 + case 2: frame_skip_rate = 3; break; // 1/3 + case 3: frame_skip_rate = 2; break; // 1/2 + default: frame_skip_rate = 1; break; + } + frame_skip_counter = 0; +} + void Player::Pause() { Audio().BGM_Pause(); } @@ -687,6 +709,7 @@ Game_Config Player::ParseCommandLine() { cp.SkipNext(); } + program_test_play = debug_flag; return cfg; } diff --git a/src/player.h b/src/player.h index ece1b465e1..52329348b4 100644 --- a/src/player.h +++ b/src/player.h @@ -432,6 +432,15 @@ namespace Player { /** game specific configuration */ extern Game_ConfigGame game_config; + /** test play flag at a game startup */ + extern bool program_test_play; + + /** + * Sets the frame skipping rate for the main loop. + * @param mode 0: Full, 1: 1/5, 2: 1/3, 3: 1/2 + */ + void SetFrameSkip(int mode); + #ifdef EMSCRIPTEN /** Name of game emscripten uses */ extern std::string emscripten_game_name; diff --git a/src/window_message.cpp b/src/window_message.cpp index 4d797a29de..1f7f8bb5cb 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -200,6 +200,15 @@ void Window_Message::StartMessageProcessing(PendingMessage pm) { void Window_Message::OnFinishPage() { DebugLog("{}: FINISH PAGE"); + if (Player::IsPatchManiac() && Main_Data::game_system->GetFastForwardText()) { + if ((text.data() + text.size() - text_index) < 3 && !pending_message.HasNumberInput() && pending_message.GetNumChoices() <= 0) { + line_char_counter = 0; + SetWait(5); + FinishMessageProcessing(); + return; + } + } + if (pending_message.GetNumChoices() > 0) { StartChoiceProcessing(); } else if (pending_message.HasNumberInput()) { @@ -312,6 +321,7 @@ void Window_Message::InsertNewPage() { } if (IsFaceEnabled()) { + int face_width = Main_Data::game_system->GetMessageFaceWidth(); if (!Main_Data::game_system->IsMessageFaceRightPosition()) { contents_x = LeftMargin + FaceSize + RightFaceMargin; DrawFace(Main_Data::game_system->GetMessageFaceName(), Main_Data::game_system->GetMessageFaceIndex(), LeftMargin, TopMargin, Main_Data::game_system->IsMessageFaceFlipped()); @@ -478,7 +488,8 @@ void Window_Message::UpdateMessage() { // Message Box Show Message rendering loop bool instant_speed_forced = false; - if (Player::debug_flag && Input::IsPressed(Input::SHIFT)) { + if ((Player::debug_flag && Input::IsPressed(Input::SHIFT)) || + (Main_Data::game_system->GetFastForwardText() && Input::IsRawKeyPressed(Input::Keys::RSHIFT))) { instant_speed = true; instant_speed_forced = true; } @@ -832,6 +843,14 @@ void Window_Message::InputChoice() { } void Window_Message::InputNumber() { + + if (Main_Data::game_system->IsMessageMouseDisabled()) { + // When mouse is disabled, it shouldn't trigger number input either + if (Input::IsRawKeyTriggered(Input::Keys::MOUSE_LEFT) || Input::IsRawKeyTriggered(Input::Keys::MOUSE_RIGHT)) { + return; + } + } + number_input_window->SetVisible(true); if (Input::IsTriggered(Input::DECISION)) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));