Files
florianessl 51e022ccec Refactored debug views related to in-game variable display:
- Extended & refactored window_varlist to support displaying data over multiple lines
  This is neccessary to have proper space for extended information on some special data types
  (for now StringVars; -> and at a later time also ScopedVars, which need more lines too)
- Removed the unneccessary copying of in-game strings
- Refactored part of the Range_Window setup code to be able to display more digits when viewing Switch/Variable ranges above 9999
- Maniacs 241028: Implemented the display for StringVar names
- Added a special function for button "Shift" in the debug scene:
  Pressing this button changes display modes for the current window
  (Currently only implemented for strings, to toggle between the old single-line display & the newer detailed one, that also included the name)
- Some other minor refactoring & moving of code parts
2025-05-24 15:32:29 +02:00

537 lines
14 KiB
C++

/*
* 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 <http://www.gnu.org/licenses/>.
*/
// Headers
#include <regex>
#include <lcf/encoder.h>
#include <lcf/reader_util.h>
#include "async_handler.h"
#include "game_map.h"
#include "game_message.h"
#include "game_strings.h"
#include "game_switches.h"
#include "game_variables.h"
#include "output.h"
#include "player.h"
#include "utils.h"
#ifdef HAVE_NLOHMANN_JSON
#include "json_helper.h"
#endif
void Game_Strings::WarnGet(int id) const {
Output::Debug("Invalid read strvar[{}]!", id);
--_warnings;
}
#ifdef HAVE_NLOHMANN_JSON
nlohmann::ordered_json* Game_Strings::ParseJson(int id) {
auto it = _json_cache.find(id);
if (it != _json_cache.end()) {
return &(it->second);
}
auto str = ToString(Get(id));
auto res = Json_Helper::Parse(str);
if (!res) {
return nullptr;
} else {
_json_cache[id] = *res;
return &_json_cache[id];
}
}
#endif
std::string_view Game_Strings::Asg(Str_Params params, std::string_view string) {
Set(params, string);
return Get(params.string_id);
}
std::string_view Game_Strings::Cat(Str_Params params, std::string_view string) {
if (params.string_id <= 0) {
return {};
}
auto it = _strings.find(params.string_id);
if (it == _strings.end()) {
Set(params, string);
return Get(params.string_id);
}
it->second += ToString(string);
return it->second;
}
int Game_Strings::ToNum(Str_Params params, int var_id, Game_Variables& variables) {
if (params.string_id <= 0) {
return -1;
}
auto it = _strings.find(params.string_id);
if (it == _strings.end()) {
return 0;
}
int num;
if (params.hex)
num = static_cast<int>(std::strtol(it->second.c_str(), nullptr, 16));
else
num = static_cast<int>(std::strtol(it->second.c_str(), nullptr, 0));
variables.Set(var_id, num);
Game_Map::SetNeedRefreshForVarChange(var_id);
return num;
}
int Game_Strings::GetLen(Str_Params params, int var_id, Game_Variables& variables) const {
if (params.string_id <= 0) {
return -1;
}
int len = Utils::UTF8Length(Get(params.string_id));
variables.Set(var_id, len);
Game_Map::SetNeedRefreshForVarChange(var_id);
return len;
}
int Game_Strings::InStr(Str_Params params, std::string search, int var_id, int begin, Game_Variables& variables) const {
if (params.string_id <= 0) {
return -1;
}
if (params.extract) {
search = Extract(search, params.hex);
}
auto search32 = Utils::DecodeUTF32(search);
auto string32 = Utils::DecodeUTF32(Get(params.string_id));
int index = string32.find(search32, begin);
variables.Set(var_id, index);
Game_Map::SetNeedRefreshForVarChange(var_id);
return index;
}
int Game_Strings::Split(Str_Params params, const std::string& delimiter, int string_out_id, int var_id, Game_Variables& variables) {
if (params.string_id <= 0) {
return -1;
}
std::string str = ToString(Get(params.string_id));
params.string_id = string_out_id;
int components = 0;
if (delimiter.empty()) {
// Count the characters (or the codepoints in our case)
components = 0;
const char* iter = str.data();
const auto end = str.data() + str.size();
while (iter != end) {
const char* start_copy = iter;
auto ret = Utils::UTF8Next(iter, end);
iter = ret.next;
if (iter == end) {
break;
}
Set(params, std::string(start_copy, iter - start_copy));
params.string_id++;
components++;
}
} else {
components = 1;
if (str.find(delimiter) == std::string::npos) {
// token not found
} else {
// This works for UTF-8
std::string token;
for (auto index = str.find(delimiter); index != std::string::npos; index = str.find(delimiter)) {
token = str.substr(0, index);
Set(params, token);
params.string_id++;
components++;
str.erase(0, index + delimiter.length());
}
}
}
// set the remaining string
Set(params, str);
variables.Set(var_id, components);
Game_Map::SetNeedRefreshForVarChange(var_id);
return components;
}
std::string Game_Strings::FromFile(std::string_view filename, int encoding, bool& do_yield) {
do_yield = false;
Filesystem_Stream::InputStream is = FileFinder::OpenText(filename);
if (!is) {
// Emscripten: Try to async fetch the file
auto* request = AsyncHandler::RequestFile("Text", filename);
request->SetImportantFile(true);
request->Start();
do_yield = !request->IsReady();
return {};
}
auto vec = Utils::ReadStream(is);
std::string file_content(vec.begin(), vec.end());
if (encoding == 0) {
lcf::Encoder enc(Player::encoding);
enc.Encode(file_content);
} else {
// UTF-8: Remove Byte Order Mask
if (file_content.size() >= 3 && file_content[0] == '\xEF' && file_content[1] == '\xBB' && file_content[2] == '\xBF') {
file_content.erase(0, 3);
}
}
return file_content;
}
bool Game_Strings::ToFile(Str_Params params, std::string filename, int encoding) {
std::string str = ToString(Get(params.string_id));
if (params.extract) {
filename = Extract(filename, params.hex);
}
// Maniacs forces the File in Text/ folder with .txt extension
filename = "Text/" + filename;
// EasyRPG Extension: When "*" is at the end of filename, ".txt" is not appended
if (Player::HasEasyRpgExtensions() && filename.back() == '*') {
filename.pop_back();
} else {
filename += ".txt";
}
auto txt_out = FileFinder::Save().OpenOutputStream(filename);
auto txt_dir = FileFinder::GetPathAndFilename(filename).first;
if (!txt_out) {
if (!FileFinder::Save().MakeDirectory(txt_dir, false)) {
Output::Warning("Maniac String Op ToFile failed. Cannot create directory {}", txt_dir);
return false;
}
txt_out = FileFinder::Save().OpenOutputStream(filename);
if (!txt_out) {
Output::Warning("Maniac String Op ToFile failed. Cannot write to {}", filename);
return false;
}
}
if (encoding == 0) {
lcf::Encoder enc(Player::encoding);
enc.Decode(str);
}
txt_out << str;
txt_out.Close();
AsyncHandler::SaveFilesystem();
return true;
}
std::string_view Game_Strings::PopLine(Str_Params params, int offset, int string_out_id) {
// FIXME: consideration needed around encoding -- what mode are files read in?
if (params.string_id <= 0) {
return {};
}
std::string result;
std::string_view str = Get(params.string_id);
std::stringstream ss(ToString(str));
while (offset >= 0 && Utils::ReadLine(ss, result)) { offset--; }
offset = ss.rdbuf()->in_avail();
Set(params, ss.str().substr(str.length() - offset));
// the maniacs implementation is to always preserve the mutated base string
// so in the case where the out_id matches the base string id, the popped line is discarded.
if (string_out_id != params.string_id) {
params.string_id = string_out_id;
Set(params, result);
}
return Get(params.string_id);
}
std::string_view Game_Strings::ExMatch(Str_Params params, std::string expr, int var_id, int begin, int string_out_id, Game_Variables& variables) {
// std::regex only works with char and wchar, not char32
// For full Unicode support requires the w-API, even on non-Windows systems
int var_result;
std::string str_result;
if (params.extract) {
expr = Extract(expr, params.hex);
}
auto source = Get(params.string_id);
std::string base = Substring(source, begin, Utils::UTF8Length(source));
std::wsmatch match;
auto wbase = Utils::ToWideString(base);
auto wexpr = Utils::ToWideString(expr);
std::wregex r(wexpr);
std::regex_search(wbase, match, r);
str_result = Utils::FromWideString(match.str());
var_result = match.position() + begin;
variables.Set(var_id, var_result);
Game_Map::SetNeedRefreshForVarChange(var_id);
if (string_out_id > 0) {
params.string_id = string_out_id;
Set(params, str_result);
return Get(params.string_id);
}
return {};
}
const Game_Strings::Strings_t& Game_Strings::RangeOp(Str_Params params, int string_id_1, std::string string, int op, int args[], Game_Variables& variables) {
if (EP_UNLIKELY(ShouldWarn(params.string_id))) {
WarnGet(params.string_id);
}
if (EP_UNLIKELY(ShouldWarn(string_id_1))) {
WarnGet(string_id_1);
}
if (params.string_id <= 0 && string_id_1 <= 0) { return GetData(); }
// maniacs just ignores if only one of the params is <= 0
if (params.string_id <= 0) { params.string_id = 1; }
if (string_id_1 <= 0) { string_id_1 = 1; }
// swap so that id_0 is < id_1
if (params.string_id > string_id_1) {
std::swap(params.string_id, string_id_1);
}
for (int start = params.string_id; params.string_id <= string_id_1; params.string_id++) {
switch (op) {
case 0: Asg(params, string); break;
case 1: Cat(params, string); break;
case 2: ToNum(params, args[0] + (params.string_id - start), variables); break;
case 3: GetLen(params, args[0] + (params.string_id - start), variables); break;
case 4: InStr(params, string, args[1], args[2], variables); break;
case 5: params.string_id += Split(params, string, args[1], args[2], variables); break;
case 8: break; // range case not applicable for popLine; see case in game_interpreter.cpp
case 9: ExMatch(params, string, args[1] + (params.string_id - start), args[2], -1, variables); break;
case 10: ExMatch(params, string, args[1] + (params.string_id - start), args[2], args[3], variables); break;
}
}
return GetData();
}
std::string Game_Strings::PrependMin(std::string_view string, int min_size, char c) {
int len = Utils::UTF8Length(string);
if (min_size < 0) {
// Left adjust
min_size = abs(min_size);
if (len < min_size) {
int s = min_size - len;
return ToString(string) + std::string(s, c);
}
} else if (len < min_size) {
// Right adjust
int s = min_size - len;
return std::string(s, c) + ToString(string);
}
return ToString(string);
}
std::string Game_Strings::Extract(std::string_view string, bool as_hex) {
PendingMessage::CommandInserter cmd_fn;
if (as_hex) {
cmd_fn = ManiacsCommandInserterHex;
} else {
cmd_fn = ManiacsCommandInserter;
}
return PendingMessage::ApplyTextInsertingCommands(ToString(string), Player::escape_char, cmd_fn);
}
std::string Game_Strings::Substring(std::string_view source, int begin, int length) {
const char* iter = source.data();
const auto end = source.data() + source.size();
begin = AdjustIndex(source, begin);
if (length < 0) {
length = 0;
}
// Points at start of the substring
auto left = Utils::UTF8Skip(iter, end, begin);
// Points at end of the substring
auto right = Utils::UTF8Skip(left.next, end, length);
if (right.next == nullptr) {
return std::string(left.next, end);
} else {
return std::string(left.next, right.next);
}
}
std::string Game_Strings::Insert(std::string_view source, std::string_view what, int where) {
const char* iter = source.data();
const auto end = source.data() + source.size();
where = AdjustIndex(source, where);
// Points at insertion location
auto ret = Utils::UTF8Skip(iter, end, where);
return std::string(source.data(), ret.next) + ToString(what) + std::string(ret.next, end);
}
std::string Game_Strings::Erase(std::string_view source, int begin, int length) {
const char* iter = source.data();
const auto end = source.data() + source.size();
begin = AdjustIndex(source, begin);
if (length < 0) {
length = 0;
}
// Points at start of deletion
auto left = Utils::UTF8Skip(iter, end, begin);
if (left.next == nullptr) {
return ToString(source);
}
// Points at end of deletion
auto right = Utils::UTF8Skip(left.next, end, length);
std::string ret = std::string(source.data(), left.next);
if (right.next != nullptr) {
ret += std::string(right.next, end);
}
return ret;
}
std::string Game_Strings::RegExReplace(std::string_view str, std::string_view search, std::string_view replace, std::regex_constants::match_flag_type flags) {
// std::regex only works with char and wchar, not char32
// For full Unicode support requires the w-API, even on non-Windows systems
auto wstr = Utils::ToWideString(str);
auto wsearch = Utils::ToWideString(search);
auto wreplace = Utils::ToWideString(replace);
std::wregex rexp(wsearch);
auto result = std::regex_replace(wstr, rexp, wreplace, flags);
return Utils::FromWideString(result);
}
int Game_Strings::AdjustIndex(std::string_view str, int index) {
if (index >= 0) {
return index;
}
return std::max(Utils::UTF8Length(str) - abs(index), 0);
}
std::optional<std::string> Game_Strings::ManiacsCommandInserter(char ch, const char** iter, const char* end, uint32_t escape_char) {
if (ch == 'S' || ch == 's') {
// \s in a normal message is the speed modifier
// parsing a switch within an extracted string var command will parse \s[N] as a switch (ON/OFF)
auto parse_ret = Game_Message::ParseSpeed(*iter, end, escape_char, true);
*iter = parse_ret.next;
int value = parse_ret.value;
return Main_Data::game_switches->Get(value) ? "ON" : "OFF";
} else if (ch == 'T' || ch == 't') {
auto parse_ret = Game_Message::ParseString(*iter, end, escape_char, true);
*iter = parse_ret.next;
int value = parse_ret.value;
// Contrary to Messages, the content of \t[]-strings is not evaluated
return ToString(Main_Data::game_strings->Get(value));
}
return Game_Message::CommandCodeInserter(ch, iter, end, escape_char);
};
std::optional<std::string> Game_Strings::ManiacsCommandInserterHex(char ch, const char** iter, const char* end, uint32_t escape_char) {
if (ch == 'V' || ch == 'v') {
auto parse_ret = Game_Message::ParseVariable(*iter, end, escape_char, true);
*iter = parse_ret.next;
int value = parse_ret.value;
int variable_value = Main_Data::game_variables->Get(value);
std::ostringstream ss;
ss << std::hex << variable_value;
return ss.str();
}
return ManiacsCommandInserter(ch, iter, end, escape_char);
};
int Game_Strings::GetSizeWithLimit() {
if (_size < 0) {
_size = 0;
for (auto& [index, value] : _strings) {
assert(index > 0);
if (index > _size) {
_size = index;
}
}
}
return std::max(_size, static_cast<int>(lcf::Data::maniac_string_variables.size()));
}
std::string_view Game_Strings::GetName(int id) const {
const auto* strvar = lcf::ReaderUtil::GetElement(lcf::Data::maniac_string_variables, id);
if (!strvar) {
return {};
} else {
return strvar->name;
}
}