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; } 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; 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 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); }