From c6673196b206a540c4ecfc8ab2a71b06a9d9ae1b Mon Sep 17 00:00:00 2001 From: Saikari Date: Sun, 17 May 2026 17:22:03 +0300 Subject: [PATCH 1/4] Maniac Patch: implement self-variables (negative variable IDs -1..-64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RPG Maker games using the Maniac Patch can use negative variable IDs (-1 to -64) as "self-variables" — per-context scratch storage that doesn't pollute the global variable table. Add a 64-element `_self_vars` array to Game_Variables. Get() returns from this array for IDs in [-64..-1], SetOp() writes to it, and ShouldWarn() suppresses spurious out-of-range warnings for valid negative IDs. Co-Authored-By: Claude Sonnet 4.6 --- src/game_variables.cpp | 7 +++++++ src/game_variables.h | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/game_variables.cpp b/src/game_variables.cpp index 4f77f700bb..0d11929646 100644 --- a/src/game_variables.cpp +++ b/src/game_variables.cpp @@ -145,6 +145,13 @@ void Game_Variables::WarnGet(int variable_id) const { template Game_Variables::Var_t Game_Variables::SetOp(int variable_id, Var_t value, F&& op, const char* warn) { + // Maniac Patch self-variables: -1..-kSelfVarCount map to local storage + if (variable_id < 0 && variable_id >= -kSelfVarCount) { + auto& v = _self_vars[(-variable_id) - 1]; + value = op(v, value); + v = Utils::Clamp(value, _min, _max); + return v; + } if (EP_UNLIKELY(ShouldWarn(variable_id, variable_id))) { Output::Debug(warn, variable_id, value); --_warnings; diff --git a/src/game_variables.h b/src/game_variables.h index bd4d5d5211..8ed31e64a2 100644 --- a/src/game_variables.h +++ b/src/game_variables.h @@ -158,6 +158,9 @@ class Game_Variables { void WriteArray(const int first_id_a, const int last_id_a, const int first_id_b, F&& op); Variables_t _variables; + // Maniac Patch self-variables: negative IDs (-1..-N) map to self_vars[0..N-1] + static constexpr int kSelfVarCount = 64; + Var_t _self_vars[kSelfVarCount] = {}; Var_t _min = 0; Var_t _max = 0; size_t lower_limit = 0; @@ -189,10 +192,18 @@ inline bool Game_Variables::IsValid(int variable_id) const { } inline bool Game_Variables::ShouldWarn(int first_id, int last_id) const { + // Self-variables (negative IDs in range) are valid, don't warn for them + if (first_id < 0 && first_id >= -kSelfVarCount && last_id < 0 && last_id >= -kSelfVarCount) { + return false; + } return (first_id <= 0 || last_id > GetSizeWithLimit()) && _warnings > 0; } inline Game_Variables::Var_t Game_Variables::Get(int variable_id) const { + // Maniac Patch self-variables: -1..-kSelfVarCount map to local storage + if (variable_id < 0 && variable_id >= -kSelfVarCount) { + return _self_vars[(-variable_id) - 1]; + } if (EP_UNLIKELY(ShouldWarn(variable_id, variable_id))) { WarnGet(variable_id); } From 7b6be6492f600fd8f18723f46b2d7605f8844dd1 Mon Sep 17 00:00:00 2001 From: Saikari Date: Sun, 17 May 2026 17:22:12 +0300 Subject: [PATCH 2/4] Maniac Patch: add scoped variable modes 5/6 in DecodeTargetEvaluationMode Mode 5 (ScopedSingle) and mode 6 (ScopedRange) address self-variables by a 1-based positive index that maps to negative IDs: params[1]=1 becomes variable ID -1, etc. Co-Authored-By: Claude Sonnet 4.6 --- src/game_interpreter_shared.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/game_interpreter_shared.cpp b/src/game_interpreter_shared.cpp index c2bf508cd0..28e58b0905 100644 --- a/src/game_interpreter_shared.cpp +++ b/src/game_interpreter_shared.cpp @@ -78,6 +78,24 @@ inline bool Game_Interpreter_Shared::DecodeTargetEvaluationMode(lcf::rpg::EventC return true; } break; + case 5: // Maniac Patch ScopedSingle: params[1] is positive self-var index → negative ID + if constexpr (validate_patches) { + if (!Player::IsPatchManiac()) { + return false; + } + } + id_0 = -com.parameters[1]; + id_1 = id_0; + break; + case 6: // Maniac Patch ScopedRange + if constexpr (validate_patches) { + if (!Player::IsPatchManiac()) { + return false; + } + } + id_0 = -com.parameters[1]; + id_1 = -com.parameters[2]; + break; default: id_0 = 0; id_1 = 0; From 817bb2b5111c08460c06e902ef9937d503c1c606 Mon Sep 17 00:00:00 2001 From: Saikari Date: Sun, 17 May 2026 17:22:22 +0300 Subject: [PATCH 3/4] Maniac Patch: fix BreakLoop to scan backward for the enclosing loop The vanilla RPG_RT emulation for BreakLoop unconditionally jumps to the next EndLoop command in the list, which is correct for vanilla 2003 but wrong under Maniac Patch where BreakLoop can appear inside conditionals nested within a loop. Example: BreakLoop at indent=2 (inside an if at indent=1 inside an infinite loop at indent=0) must break the outer loop, not whatever inner loop happens to have its EndLoop appear next in the bytecode. For Maniac Patch, scan backward from the BreakLoop to find the innermost enclosing Loop command, tracking EndLoop/Loop pairs to handle already-closed nested loops correctly, then skip forward to its EndLoop. Co-Authored-By: Claude Sonnet 4.6 --- src/game_interpreter.cpp | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index aca814c645..45358e2799 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -3882,9 +3882,43 @@ bool Game_Interpreter::CommandBreakLoop(lcf::rpg::EventCommand const& /* com */) // BreakLoop will jump to the end of the event if there is no loop. - bool has_bug = !Player::IsPatchManiac(); - if (!has_bug) { - SkipToNextConditional({ Cmd::EndLoop }, list[index].indent - 1); + if (Player::IsPatchManiac()) { + // Maniac Patch: find the actual innermost enclosing loop by scanning backward. + // This correctly handles BreakLoop inside conditionals nested within a loop — + // e.g. BreakLoop at indent=2 inside an if (indent=1) inside an infinite loop + // (indent=0) must break the outer loop, not some unrelated inner loop that + // happens to appear later in the same loop body. + int break_indent = list[index].indent; + int target_indent = -1; + + // endloop_depth[M] counts how many EndLoops at indent=M we have seen going + // backward; a matching Loop cancels one out rather than being the enclosing loop. + constexpr int kMaxIndent = 20; + int endloop_depth[kMaxIndent] = {}; + + for (int i = index - 1; i >= 0; --i) { + const auto& c = list[i]; + if (c.indent >= break_indent) continue; + auto cc = static_cast(c.code); + if (cc == Cmd::EndLoop) { + if (c.indent < kMaxIndent) endloop_depth[c.indent]++; + } else if (cc == Cmd::Loop) { + int d = (c.indent < kMaxIndent) ? endloop_depth[c.indent] : 0; + if (d > 0) { + endloop_depth[c.indent]--; + } else { + target_indent = c.indent; + break; + } + } + } + + if (target_indent < 0) { + index = static_cast(list.size()); + return true; + } + + SkipToNextConditional({ Cmd::EndLoop }, target_indent); ++index; return true; } From 749ae2a954fa89ba211a925310e50c61343a1965 Mon Sep 17 00:00:00 2001 From: Saikari Date: Sun, 17 May 2026 17:22:29 +0300 Subject: [PATCH 4/4] Maniac Patch: support negative variable IDs in message escape sequences ParseParam() treats a leading '-' inside brackets as stopping digit parsing, so \v[-1] was returned as 0 instead of reading self-variable -1. Add an is_negative flag that consumes the '-' and negates the parsed value, matching the Maniac Patch behaviour for \v[-N], \t[-N], \c[-N], etc. Co-Authored-By: Claude Sonnet 4.6 --- src/game_message.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/game_message.cpp b/src/game_message.cpp index 0404cbd713..1214254bda 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -222,6 +222,13 @@ Game_Message::ParseParamResult Game_Message::ParseParam( ++iter; bool stop_parsing = false; bool got_valid_number = false; + bool is_negative = false; + + // Maniac Patch: support negative variable IDs like \v[-1] + if (iter != end && *iter == '-') { + is_negative = true; + ++iter; + } while (iter != end && *iter != ']') { if (stop_parsing) { @@ -283,6 +290,7 @@ Game_Message::ParseParamResult Game_Message::ParseParam( ++iter; } + if (is_negative) { value = -value; } values.emplace_back(value); // Actor 0 references the first party member