mkxp-z/binding/binding-mri.cpp
Wayward Heart 99ad4fa636 Throw exceptions for Resets and Exits instead of directly raising.
While this does close small memory leaks, this is mostly for threading reasons. We're not supposed to call rb_raise with the gvl released, and calling rb_raise prevents GFX_UNLOCK from being called, which would cause problems for any games that want to call graphical operations in multiple threads should the user reset.

We're also now calling Graphics.__reset__ and Audio.__reset__ via eval instead of directly calling the functions, in case a game wants to hook them.
2024-08-02 09:26:51 -05:00

1269 lines
34 KiB
C++

/*
** binding-mri.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2013 - 2021 Amaryllis Kulla <ancurio@mapleshrine.eu>
**
** mkxp 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 2 of the License, or
** (at your option) any later version.
**
** mkxp 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 mkxp. If not, see <http://www.gnu.org/licenses/>.
*/
#include "audio/audio.h"
#include "filesystem/filesystem.h"
#include "display/graphics.h"
#include "display/font.h"
#include "system/system.h"
#include "util/util.h"
#include "util/sdl-util.h"
#include "util/debugwriter.h"
#include "util/boost-hash.h"
#include "util/exception.h"
#include "util/encoding.h"
#include "config.h"
#include "binding-util.h"
#include "binding.h"
#include "sharedstate.h"
#include "eventthread.h"
#include <vector>
extern "C" {
#include <ruby.h>
#if RAPI_FULL >= 190
#include <ruby/encoding.h>
#endif
}
#ifdef __WIN32__
#include "binding-mri-win32.h"
#endif
#include <assert.h>
#include <string>
#include <zlib.h>
#include <SDL_cpuinfo.h>
#include <SDL_filesystem.h>
#include <SDL_loadso.h>
#include <SDL_power.h>
extern const char module_rpg1[];
extern const char module_rpg2[];
extern const char module_rpg3[];
static void mriBindingExecute();
static void mriBindingTerminate();
static void mriBindingReset();
ScriptBinding scriptBindingImpl = {mriBindingExecute, mriBindingTerminate,
mriBindingReset};
ScriptBinding *scriptBinding = &scriptBindingImpl;
void tableBindingInit();
void etcBindingInit();
void fontBindingInit();
void bitmapBindingInit();
void spriteBindingInit();
void viewportBindingInit();
void planeBindingInit();
void windowBindingInit();
void tilemapBindingInit();
void windowVXBindingInit();
void tilemapVXBindingInit();
void inputBindingInit();
void audioBindingInit();
void graphicsBindingInit();
void fileIntBindingInit();
#ifdef MKXPZ_MINIFFI
void MiniFFIBindingInit();
#endif
#ifdef MKXPZ_STEAM
void CUSLBindingInit();
#endif
void httpBindingInit();
RB_METHOD(mkxpDelta);
RB_METHOD(mriPrint);
RB_METHOD(mriP);
RB_METHOD(mkxpDataDirectory);
RB_METHOD(mkxpSetTitle);
RB_METHOD(mkxpGetTitle);
RB_METHOD(mkxpDesensitize);
RB_METHOD(mkxpPuts);
RB_METHOD(mkxpPlatform);
RB_METHOD(mkxpIsMacHost);
RB_METHOD(mkxpIsWindowsHost);
RB_METHOD(mkxpIsLinuxHost);
RB_METHOD(mkxpIsUsingRosetta);
RB_METHOD(mkxpIsUsingWine);
RB_METHOD(mkxpIsReallyMacHost);
RB_METHOD(mkxpIsReallyLinuxHost);
RB_METHOD(mkxpIsReallyWindowsHost);
RB_METHOD(mkxpUserLanguage);
RB_METHOD(mkxpUserName);
RB_METHOD(mkxpGameTitle);
RB_METHOD(mkxpPowerState);
RB_METHOD(mkxpSettingsMenu);
RB_METHOD(mkxpCpuCount);
RB_METHOD(mkxpSystemMemory);
RB_METHOD(mkxpReloadPathCache);
RB_METHOD(mkxpAddPath);
RB_METHOD(mkxpRemovePath);
RB_METHOD(mkxpFileExists);
RB_METHOD(mkxpLaunch);
RB_METHOD(mkxpGetJSONSetting);
RB_METHOD(mkxpSetJSONSetting);
RB_METHOD(mkxpGetAllJSONSettings);
RB_METHOD(mkxpSetDefaultFontFamily);
RB_METHOD(mriRgssMain);
RB_METHOD(mriRgssStop);
RB_METHOD(_kernelCaller);
RB_METHOD(mkxpStringToUTF8);
RB_METHOD(mkxpStringToUTF8Bang);
VALUE json2rb(json5pp::value const &v);
json5pp::value rb2json(VALUE v);
static void mriBindingInit() {
tableBindingInit();
etcBindingInit();
fontBindingInit();
bitmapBindingInit();
spriteBindingInit();
viewportBindingInit();
planeBindingInit();
if (rgssVer == 1) {
windowBindingInit();
tilemapBindingInit();
} else {
windowVXBindingInit();
tilemapVXBindingInit();
}
inputBindingInit();
audioBindingInit();
graphicsBindingInit();
fileIntBindingInit();
#ifdef MKXPZ_MINIFFI
MiniFFIBindingInit();
#endif
#ifdef MKXPZ_STEAM
CUSLBindingInit();
#endif
httpBindingInit();
if (rgssVer >= 3) {
_rb_define_module_function(rb_mKernel, "rgss_main", mriRgssMain);
_rb_define_module_function(rb_mKernel, "rgss_stop", mriRgssStop);
_rb_define_module_function(rb_mKernel, "msgbox", mriPrint);
_rb_define_module_function(rb_mKernel, "msgbox_p", mriP);
rb_define_global_const("RGSS_VERSION", rb_utf8_str_new_cstr("3.0.1"));
} else {
_rb_define_module_function(rb_mKernel, "print", mriPrint);
_rb_define_module_function(rb_mKernel, "p", mriP);
rb_define_alias(rb_singleton_class(rb_mKernel), "_mkxp_kernel_caller_alias",
"caller");
_rb_define_module_function(rb_mKernel, "caller", _kernelCaller);
}
if (rgssVer == 1)
rb_eval_string(module_rpg1);
else if (rgssVer == 2)
rb_eval_string(module_rpg2);
else if (rgssVer == 3)
rb_eval_string(module_rpg3);
else
assert(!"unreachable");
VALUE mod = rb_define_module("System");
_rb_define_module_function(mod, "delta", mkxpDelta);
_rb_define_module_function(mod, "uptime", mkxpDelta);
_rb_define_module_function(mod, "data_directory", mkxpDataDirectory);
_rb_define_module_function(mod, "set_window_title", mkxpSetTitle);
_rb_define_module_function(mod, "window_title", mkxpGetTitle);
_rb_define_module_function(mod, "window_title=", mkxpSetTitle);
_rb_define_module_function(mod, "show_settings", mkxpSettingsMenu);
_rb_define_module_function(mod, "puts", mkxpPuts);
_rb_define_module_function(mod, "desensitize", mkxpDesensitize);
_rb_define_module_function(mod, "platform", mkxpPlatform);
_rb_define_module_function(mod, "is_mac?", mkxpIsMacHost);
_rb_define_module_function(mod, "is_rosetta?", mkxpIsUsingRosetta);
_rb_define_module_function(mod, "is_linux?", mkxpIsLinuxHost);
_rb_define_module_function(mod, "is_windows?", mkxpIsWindowsHost);
_rb_define_module_function(mod, "is_wine?", mkxpIsUsingWine);
_rb_define_module_function(mod, "is_really_mac?", mkxpIsReallyMacHost);
_rb_define_module_function(mod, "is_really_linux?", mkxpIsReallyLinuxHost);
_rb_define_module_function(mod, "is_really_windows?", mkxpIsReallyWindowsHost);
_rb_define_module_function(mod, "user_language", mkxpUserLanguage);
_rb_define_module_function(mod, "user_name", mkxpUserName);
_rb_define_module_function(mod, "game_title", mkxpGameTitle);
_rb_define_module_function(mod, "power_state", mkxpPowerState);
_rb_define_module_function(mod, "nproc", mkxpCpuCount);
_rb_define_module_function(mod, "memory", mkxpSystemMemory);
_rb_define_module_function(mod, "reload_cache", mkxpReloadPathCache);
_rb_define_module_function(mod, "mount", mkxpAddPath);
_rb_define_module_function(mod, "unmount", mkxpRemovePath);
_rb_define_module_function(mod, "file_exist?", mkxpFileExists);
_rb_define_module_function(mod, "launch", mkxpLaunch);
_rb_define_module_function(mod, "default_font_family=", mkxpSetDefaultFontFamily);
_rb_define_method(rb_cString, "to_utf8", mkxpStringToUTF8);
_rb_define_method(rb_cString, "to_utf8!", mkxpStringToUTF8Bang);
VALUE cmod = rb_define_module("CFG");
_rb_define_module_function(cmod, "[]", mkxpGetJSONSetting);
_rb_define_module_function(cmod, "[]=", mkxpSetJSONSetting);
_rb_define_module_function(cmod, "to_hash", mkxpGetAllJSONSettings);
/* Load global constants */
rb_gv_set("MKXP", Qtrue);
VALUE debug = rb_bool_new(shState->config().editor.debug);
if (rgssVer == 1)
rb_gv_set("DEBUG", debug);
else if (rgssVer >= 2)
rb_gv_set("TEST", debug);
rb_gv_set("BTEST", rb_bool_new(shState->config().editor.battleTest));
#ifdef MKXPZ_BUILD_XCODE
std::string version = std::string(MKXPZ_VERSION "/") + getPlistValue("GIT_COMMIT_HASH");
VALUE vers = rb_utf8_str_new_cstr(version.c_str());
#else
VALUE vers = rb_utf8_str_new_cstr(MKXPZ_VERSION "/" MKXPZ_GIT_HASH);
#endif
rb_str_freeze(vers);
rb_define_const(mod, "VERSION", vers);
// Automatically load zlib if it's present -- the correct way this time
int state;
rb_eval_string_protect("require('zlib') if !Kernel.const_defined?(:Zlib)", &state);
if (state) {
Debug() << "Could not load Zlib. If this is important, make sure Ruby was built with static extensions, or that"
<< ((MKXPZ_PLATFORM == MKXPZ_PLATFORM_MACOS) ? "zlib.bundle" : "zlib.so")
<< "is present and reachable by Ruby's loadpath.";
}
// Set $stdout and its ilk accordingly on Windows
// I regret teaching you that word
#ifdef __WIN32__
if (shState->config().winConsole)
configureWindowsStreams();
#endif
}
static void showMsg(const std::string &msg) {
shState->eThread().showMessageBox(msg.c_str());
}
static void printP(int argc, VALUE *argv, const char *convMethod,
const char *sep) {
VALUE dispString = rb_str_buf_new(128);
ID conv = rb_intern(convMethod);
for (int i = 0; i < argc; ++i) {
VALUE str = rb_funcall2(argv[i], conv, 0, NULL);
rb_str_buf_append(dispString, str);
if (i < argc)
rb_str_buf_cat2(dispString, sep);
}
showMsg(RSTRING_PTR(dispString));
}
RB_METHOD_GUARD(mriPrint) {
RB_UNUSED_PARAM;
printP(argc, argv, "to_s", "");
shState->checkShutdown();
shState->checkReset();
return Qnil;
}
RB_METHOD_GUARD_END
RB_METHOD_GUARD(mriP) {
RB_UNUSED_PARAM;
printP(argc, argv, "inspect", "\n");
shState->checkShutdown();
shState->checkReset();
return Qnil;
}
RB_METHOD_GUARD_END
RB_METHOD(mkxpDelta) {
RB_UNUSED_PARAM;
return rb_float_new(shState->runTime());
}
RB_METHOD(mkxpDataDirectory) {
RB_UNUSED_PARAM;
const std::string &path = shState->config().customDataPath;
const char *s = path.empty() ? "." : path.c_str();
std::string s_nml = shState->fileSystem().normalize(s, 1, 1);
VALUE ret = rb_utf8_str_new_cstr(s_nml.c_str());
return ret;
}
RB_METHOD(mkxpSetTitle) {
RB_UNUSED_PARAM;
VALUE s;
rb_scan_args(argc, argv, "1", &s);
SafeStringValue(s);
shState->eThread().requestWindowRename(RSTRING_PTR(s));
return s;
}
RB_METHOD(mkxpGetTitle) {
RB_UNUSED_PARAM;
rb_check_argc(argc, 0);
return rb_utf8_str_new_cstr(SDL_GetWindowTitle(shState->sdlWindow()));
}
RB_METHOD(mkxpDesensitize) {
RB_UNUSED_PARAM;
VALUE filename;
rb_scan_args(argc, argv, "1", &filename);
SafeStringValue(filename);
return rb_utf8_str_new_cstr(
shState->fileSystem().desensitize(RSTRING_PTR(filename)));
}
RB_METHOD(mkxpPuts) {
RB_UNUSED_PARAM;
const char *str;
rb_get_args(argc, argv, "z", &str RB_ARG_END);
Debug() << str;
return Qnil;
}
RB_METHOD(mkxpPlatform) {
RB_UNUSED_PARAM;
#if MKXPZ_PLATFORM == MKXPZ_PLATFORM_MACOS
std::string platform("macOS");
if (mkxp_sys::isRosetta())
platform += " (Rosetta)";
#elif MKXPZ_PLATFORM == MKXPZ_PLATFORM_WINDOWS
std::string platform("Windows");
if (mkxp_sys::isWine()) {
platform += " (Wine - ";
switch (mkxp_sys::getRealHostType()) {
case mkxp_sys::WineHostType::Mac:
platform += "macOS)";
break;
default:
platform += "Linux)";
break;
}
}
#else
std::string platform("Linux");
#endif
return rb_utf8_str_new_cstr(platform.c_str());
}
RB_METHOD(mkxpIsMacHost) {
RB_UNUSED_PARAM;
return rb_bool_new(MKXPZ_PLATFORM == MKXPZ_PLATFORM_MACOS);
}
RB_METHOD(mkxpIsUsingRosetta) {
RB_UNUSED_PARAM;
return rb_bool_new(mkxp_sys::isRosetta());
}
RB_METHOD(mkxpIsLinuxHost) {
RB_UNUSED_PARAM;
return rb_bool_new(MKXPZ_PLATFORM == MKXPZ_PLATFORM_LINUX);
}
RB_METHOD(mkxpIsWindowsHost) {
RB_UNUSED_PARAM;
return rb_bool_new(MKXPZ_PLATFORM == MKXPZ_PLATFORM_WINDOWS);
}
RB_METHOD(mkxpIsUsingWine) {
RB_UNUSED_PARAM;
return rb_bool_new(mkxp_sys::isWine());
}
RB_METHOD(mkxpIsReallyMacHost) {
RB_UNUSED_PARAM;
return rb_bool_new(mkxp_sys::getRealHostType() == mkxp_sys::WineHostType::Mac);
}
RB_METHOD(mkxpIsReallyLinuxHost) {
RB_UNUSED_PARAM;
return rb_bool_new(mkxp_sys::getRealHostType() == mkxp_sys::WineHostType::Linux);
}
RB_METHOD(mkxpIsReallyWindowsHost) {
RB_UNUSED_PARAM;
return rb_bool_new(mkxp_sys::getRealHostType() == mkxp_sys::WineHostType::Windows);
}
RB_METHOD(mkxpUserLanguage) {
RB_UNUSED_PARAM;
return rb_utf8_str_new_cstr(mkxp_sys::getSystemLanguage().c_str());
}
RB_METHOD(mkxpUserName) {
RB_UNUSED_PARAM;
// Using the Windows API isn't working with usernames that involve Unicode
// characters for some dumb reason
#ifdef __WIN32__
VALUE env = rb_const_get(rb_mKernel, rb_intern("ENV"));
return rb_funcall(env, rb_intern("[]"), 1, rb_str_new_cstr("USERNAME"));
#else
return rb_utf8_str_new_cstr(mkxp_sys::getUserName().c_str());
#endif
}
RB_METHOD(mkxpGameTitle) {
RB_UNUSED_PARAM;
return rb_utf8_str_new_cstr(shState->config().game.title.c_str());
}
RB_METHOD(mkxpPowerState) {
RB_UNUSED_PARAM;
int secs, pct;
SDL_PowerState ps = SDL_GetPowerInfo(&secs, &pct);
VALUE hash = rb_hash_new();
rb_hash_aset(hash, ID2SYM(rb_intern("seconds")),
(secs > -1) ? INT2NUM(secs) : RUBY_Qnil);
rb_hash_aset(hash, ID2SYM(rb_intern("percent")),
(pct > -1) ? INT2NUM(pct) : RUBY_Qnil);
rb_hash_aset(hash, ID2SYM(rb_intern("discharging")),
rb_bool_new(ps == SDL_POWERSTATE_ON_BATTERY));
return hash;
}
RB_METHOD(mkxpSettingsMenu) {
RB_UNUSED_PARAM;
shState->eThread().requestSettingsMenu();
return Qnil;
}
RB_METHOD(mkxpCpuCount) {
RB_UNUSED_PARAM;
return INT2NUM(SDL_GetCPUCount());
}
RB_METHOD(mkxpSystemMemory) {
RB_UNUSED_PARAM;
return INT2NUM(SDL_GetSystemRAM());
}
RB_METHOD_GUARD(mkxpReloadPathCache) {
RB_UNUSED_PARAM;
shState->fileSystem().reloadPathCache();
return Qnil;
}
RB_METHOD_GUARD_END
RB_METHOD_GUARD(mkxpAddPath) {
RB_UNUSED_PARAM;
VALUE path, mountpoint, reload;
rb_scan_args(argc, argv, "12", &path, &mountpoint, &reload);
SafeStringValue(path);
if (mountpoint != Qnil) SafeStringValue(mountpoint);
const char *mp = (mountpoint == Qnil) ? 0 : RSTRING_PTR(mountpoint);
bool rl = true;
if (reload != Qnil)
rb_bool_arg(reload, &rl);
shState->fileSystem().addPath(RSTRING_PTR(path), mp, rl);
return path;
}
RB_METHOD_GUARD_END
RB_METHOD_GUARD(mkxpRemovePath) {
RB_UNUSED_PARAM;
VALUE path, reload;
rb_scan_args(argc, argv, "11", &path, &reload);
SafeStringValue(path);
bool rl = true;
if (reload != Qnil)
rb_bool_arg(reload, &rl);
shState->fileSystem().removePath(RSTRING_PTR(path), rl);
return path;
}
RB_METHOD_GUARD_END
RB_METHOD(mkxpFileExists) {
RB_UNUSED_PARAM;
VALUE path;
rb_scan_args(argc, argv, "1", &path);
SafeStringValue(path);
if (shState->fileSystem().exists(RSTRING_PTR(path)))
return Qtrue;
return Qfalse;
}
RB_METHOD(mkxpSetDefaultFontFamily) {
RB_UNUSED_PARAM;
VALUE familyV;
rb_scan_args(argc, argv, "1", &familyV);
SafeStringValue(familyV);
std::string family(RSTRING_PTR(familyV));
shState->fontState().setDefaultFontFamily(family);
return Qnil;
}
RB_METHOD_GUARD(mkxpStringToUTF8) {
RB_UNUSED_PARAM;
rb_check_argc(argc, 0);
std::string ret(RSTRING_PTR(self), RSTRING_LEN(self));
ret = Encoding::convertString(ret);
return rb_utf8_str_new(ret.c_str(), ret.length());
}
RB_METHOD_GUARD_END
RB_METHOD_GUARD(mkxpStringToUTF8Bang) {
RB_UNUSED_PARAM;
rb_check_argc(argc, 0);
std::string ret(RSTRING_PTR(self), RSTRING_LEN(self));
ret = Encoding::convertString(ret);
rb_str_resize(self, ret.length());
memcpy(RSTRING_PTR(self), ret.c_str(), RSTRING_LEN(self));
#if RAPI_FULL >= 190
rb_funcall(self, rb_intern("force_encoding"), 1, rb_enc_from_encoding(rb_utf8_encoding()));
#endif
return self;
}
RB_METHOD_GUARD_END
#ifdef __APPLE__
#define OPENCMD "open "
#define OPENARGS "--args"
#elif defined(__linux__)
#define OPENCMD "xdg-open "
#define OPENARGS ""
#else
#define OPENCMD "start /b \"launch\" "
#define OPENARGS ""
#endif
RB_METHOD_GUARD(mkxpLaunch) {
RB_UNUSED_PARAM;
VALUE cmdname, args;
rb_scan_args(argc, argv, "11", &cmdname, &args);
SafeStringValue(cmdname);
std::string command(OPENCMD);
command += "\""; command += RSTRING_PTR(cmdname); command += "\"";
if (args != RUBY_Qnil) {
#ifndef __linux__
command += " ";
command += OPENARGS;
Check_Type(args, T_ARRAY);
for (int i = 0; i < RARRAY_LEN(args); i++) {
VALUE arg = rb_ary_entry(args, i);
SafeStringValue(arg);
if (RSTRING_LEN(arg) <= 0)
continue;
command += " ";
command += RSTRING_PTR(arg);
}
#else
Debug() << command << ":" << "Arguments are not supported with xdg-open. Ignoring.";
#endif
}
if (std::system(command.c_str()) != 0) {
throw Exception(Exception::MKXPError, "Failed to launch \"%s\"", RSTRING_PTR(cmdname));
}
return RUBY_Qnil;
}
RB_METHOD_GUARD_END
json5pp::value loadUserSettings() {
json5pp::value ret;
VALUE cpath = rb_utf8_str_new_cstr(shState->config().userConfPath.c_str());
if (rb_funcall(rb_cFile, rb_intern("exists?"), 1, cpath) == Qtrue) {
VALUE f = rb_funcall(rb_cFile, rb_intern("open"), 2, cpath, rb_str_new("r", 1));
VALUE data = rb_funcall(f, rb_intern("read"), 0);
rb_funcall(f, rb_intern("close"), 0);
ret = json5pp::parse5(RSTRING_PTR(data));
}
if (!ret.is_object())
ret = json5pp::object({});
return ret;
}
void saveUserSettings(json5pp::value &settings) {
VALUE cpath = rb_utf8_str_new_cstr(shState->config().userConfPath.c_str());
VALUE f = rb_funcall(rb_cFile, rb_intern("open"), 2, cpath, rb_str_new("w", 1));
rb_funcall(f, rb_intern("write"), 1, rb_utf8_str_new_cstr(settings.stringify5(json5pp::rule::space_indent<>()).c_str()));
rb_funcall(f, rb_intern("close"), 0);
}
RB_METHOD(mkxpGetJSONSetting) {
RB_UNUSED_PARAM;
VALUE sname;
rb_scan_args(argc, argv, "1", &sname);
SafeStringValue(sname);
auto settings = loadUserSettings();
auto &s = settings.as_object();
if (s[RSTRING_PTR(sname)].is_null()) {
return json2rb(shState->config().raw.as_object()[RSTRING_PTR(sname)]);
}
return json2rb(s[RSTRING_PTR(sname)]);
}
RB_METHOD_GUARD(mkxpSetJSONSetting) {
RB_UNUSED_PARAM;
VALUE sname, svalue;
rb_scan_args(argc, argv, "2", &sname, &svalue);
SafeStringValue(sname);
auto settings = loadUserSettings();
auto &s = settings.as_object();
s[RSTRING_PTR(sname)] = rb2json(svalue);
saveUserSettings(settings);
return Qnil;
}
RB_METHOD_GUARD_END
RB_METHOD(mkxpGetAllJSONSettings) {
RB_UNUSED_PARAM;
return json2rb(shState->config().raw);
}
static VALUE rgssMainCb(VALUE block) {
rb_funcall2(block, rb_intern("call"), 0, 0);
return Qnil;
}
static VALUE rgssMainRescue(VALUE arg, VALUE exc) {
VALUE *excRet = (VALUE *)arg;
*excRet = exc;
return Qnil;
}
static bool processReset(bool rubyExc) {
const char *str = "Audio.__reset__; Graphics.__reset__;";
if (rubyExc) {
rb_eval_string(str);
} else {
int state;
rb_eval_string_protect(str, &state);
return state;
}
return 0;
}
RB_METHOD_GUARD(mriRgssMain) {
RB_UNUSED_PARAM;
while (true) {
VALUE exc = Qnil;
#if RAPI_FULL < 270
rb_rescue2((VALUE(*)(ANYARGS))rgssMainCb, rb_block_proc(),
(VALUE(*)(ANYARGS))rgssMainRescue, (VALUE)&exc, rb_eException,
(VALUE)0);
#else
rb_rescue2(rgssMainCb, rb_block_proc(), rgssMainRescue, (VALUE)&exc,
rb_eException, (VALUE)0);
#endif
if (NIL_P(exc))
break;
if (rb_obj_class(exc) == getRbData()->exc[Reset])
processReset(true);
else
rb_exc_raise(exc);
}
return Qnil;
}
RB_METHOD_GUARD_END
RB_METHOD_GUARD(mriRgssStop) {
RB_UNUSED_PARAM;
while (true)
shState->graphics().update();
return Qnil;
}
RB_METHOD_GUARD_END
RB_METHOD(_kernelCaller) {
RB_UNUSED_PARAM;
VALUE trace =
rb_funcall2(rb_mKernel, rb_intern("_mkxp_kernel_caller_alias"), 0, 0);
if (!RB_TYPE_P(trace, RUBY_T_ARRAY))
return trace;
long len = RARRAY_LEN(trace);
if (len < 2)
return trace;
/* Remove useless "ruby:1:in 'eval'" */
rb_ary_pop(trace);
/* Also remove trace of this helper function */
rb_ary_shift(trace);
len -= 2;
if (len == 0)
return trace;
/* RMXP does this, not sure if specific or 1.8 related */
VALUE args[] = {rb_utf8_str_new_cstr(":in `<main>'"), rb_utf8_str_new_cstr("")};
rb_funcall2(rb_ary_entry(trace, len - 1), rb_intern("gsub!"), 2, args);
return trace;
}
#if RAPI_FULL > 187
static VALUE newStringUTF8(const char *string, long length) {
return rb_enc_str_new(string, length, rb_utf8_encoding());
}
#else
#define newStringUTF8 rb_str_new
#endif
struct evalArg {
VALUE string;
VALUE filename;
};
static VALUE evalHelper(evalArg *arg) {
VALUE argv[] = {arg->string, Qnil, arg->filename};
return rb_funcall2(Qnil, rb_intern("eval"), ARRAY_SIZE(argv), argv);
}
static VALUE evalString(VALUE string, VALUE filename, int *state) {
evalArg arg = {string, filename};
return rb_protect((VALUE(*)(VALUE))evalHelper, (VALUE)&arg, state);
}
static void runCustomScript(const std::string &filename) {
std::string scriptData;
if (!readFileSDL(filename.c_str(), scriptData)) {
showMsg(std::string("Unable to open '") + filename + "'");
return;
}
evalString(newStringUTF8(scriptData.c_str(), scriptData.size()),
newStringUTF8(filename.c_str(), filename.size()), NULL);
}
VALUE kernelLoadDataInt(const char *filename, bool rubyExc, bool raw);
struct BacktraceData {
/* Maps: Ruby visible filename, To: Actual script name */
BoostHash<std::string, std::string> scriptNames;
};
bool evalScript(VALUE string, const char *filename)
{
int state;
evalString(string, rb_utf8_str_new_cstr(filename), &state);
if (state) return false;
return true;
}
#define SCRIPT_SECTION_FMT (rgssVer >= 3 ? "{%04ld}" : "Section%03ld")
static void runRMXPScripts(BacktraceData &btData) {
const Config &conf = shState->rtData().config;
const std::string &scriptPack = conf.game.scripts;
if (scriptPack.empty()) {
showMsg("No script file has been specified. Check the game's INI and try again.");
return;
}
if (!shState->fileSystem().exists(scriptPack.c_str())) {
showMsg("Unable to load scripts from '" + scriptPack + "'");
return;
}
VALUE scriptArray;
/* We checked if Scripts.rxdata exists, but something might
* still go wrong */
try {
scriptArray = kernelLoadDataInt(scriptPack.c_str(), false, false);
} catch (const Exception &e) {
showMsg(std::string("Failed to read script data: ") + e.msg);
return;
}
if (!RB_TYPE_P(scriptArray, RUBY_T_ARRAY)) {
showMsg("Failed to read script data");
return;
}
rb_gv_set("$RGSS_SCRIPTS", scriptArray);
long scriptCount = RARRAY_LEN(scriptArray);
std::string decodeBuffer;
decodeBuffer.resize(0x1000);
for (long i = 0; i < scriptCount; ++i) {
VALUE script = rb_ary_entry(scriptArray, i);
if (!RB_TYPE_P(script, RUBY_T_ARRAY))
continue;
VALUE scriptName = rb_ary_entry(script, 1);
VALUE scriptString = rb_ary_entry(script, 2);
int result = Z_OK;
unsigned long bufferLen;
while (true) {
unsigned char *bufferPtr = reinterpret_cast<unsigned char *>(
const_cast<char *>(decodeBuffer.c_str()));
const unsigned char *sourcePtr =
reinterpret_cast<const unsigned char *>(RSTRING_PTR(scriptString));
bufferLen = decodeBuffer.length();
result = uncompress(bufferPtr, &bufferLen, sourcePtr,
RSTRING_LEN(scriptString));
bufferPtr[bufferLen] = '\0';
if (result != Z_BUF_ERROR)
break;
decodeBuffer.resize(decodeBuffer.size() * 2);
}
if (result != Z_OK) {
static char buffer[256];
snprintf(buffer, sizeof(buffer), "Error decoding script %ld: '%s'", i,
RSTRING_PTR(scriptName));
showMsg(buffer);
break;
}
rb_ary_store(script, 3, rb_utf8_str_new_cstr(decodeBuffer.c_str()));
}
/* Execute preloaded scripts */
for (std::vector<std::string>::const_iterator i = conf.preloadScripts.begin();
i != conf.preloadScripts.end(); ++i)
{
if (shState->rtData().rqTerm)
break;
runCustomScript(*i);
}
VALUE exc = rb_gv_get("$!");
if (exc != Qnil)
return;
while (true) {
for (long i = 0; i < scriptCount; ++i) {
if (shState->rtData().rqTerm)
break;
VALUE script = rb_ary_entry(scriptArray, i);
VALUE scriptDecoded = rb_ary_entry(script, 3);
VALUE string =
newStringUTF8(RSTRING_PTR(scriptDecoded), RSTRING_LEN(scriptDecoded));
VALUE fname;
const char *scriptName = RSTRING_PTR(rb_ary_entry(script, 1));
char buf[512];
int len;
if (conf.useScriptNames)
len = snprintf(buf, sizeof(buf), "%03ld:%s", i, scriptName);
else
len = snprintf(buf, sizeof(buf), SCRIPT_SECTION_FMT, i);
fname = newStringUTF8(buf, len);
btData.scriptNames.insert(buf, scriptName);
// if the script name starts with |s|, only execute
// it if "s" is the same first letter as the platform
// we're running on
// |W| - Windows, |M| - Mac OS X, |L| - Linux
// Adding a 'not' symbol means it WON'T run on that
// platform (i.e. |!W| won't run on Windows)
/*
if (scriptName[0] == '|') {
int len = strlen(scriptName);
if (len > 2) {
if (scriptName[1] == '!' && len > 3 &&
scriptName[3] == scriptName[0]) {
if (toupper(scriptName[2]) == platform[0])
continue;
}
if (scriptName[2] == scriptName[0] &&
toupper(scriptName[1]) != platform[0])
continue;
}
}
*/
int state;
evalString(string, fname, &state);
if (state)
break;
}
VALUE exc = rb_gv_get("$!");
if (rb_obj_class(exc) != getRbData()->exc[Reset])
break;
if (processReset(false))
break;
}
}
static void showExc(VALUE exc, const BacktraceData &btData) {
VALUE bt = rb_funcall2(exc, rb_intern("backtrace"), 0, NULL);
VALUE msg = rb_funcall2(exc, rb_intern("message"), 0, NULL);
VALUE bt0 = rb_ary_entry(bt, 0);
VALUE name = rb_class_path(rb_obj_class(exc));
VALUE ds = rb_sprintf("%" PRIsVALUE ": %" PRIsVALUE " (%" PRIsVALUE ")",
#if RAPI_MAJOR >= 2
bt0, exc, name);
#else
// Ruby 1.9's version of this function needs char*
RSTRING_PTR(bt0), RSTRING_PTR(exc), RSTRING_PTR(name));
#endif
/* omit "useless" last entry (from ruby:1:in `eval') */
for (long i = 1, btlen = RARRAY_LEN(bt) - 1; i < btlen; ++i)
rb_str_catf(ds, "\n\tfrom %" PRIsVALUE,
#if RAPI_MAJOR >= 2
rb_ary_entry(bt, i));
#else
RSTRING_PTR(rb_ary_entry(bt, i)));
#endif
Debug() << StringValueCStr(ds);
char *s = RSTRING_PTR(bt0);
char line[16];
std::string file(512, '\0');
char *p = s + strlen(s);
char *e;
while (p != s)
if (*--p == ':')
break;
e = p;
while (p != s)
if (*--p == ':')
break;
/* s p e
* SectionXXX:YY: in 'blabla' */
*e = '\0';
strncpy(line, *p ? p + 1 : p, sizeof(line));
line[sizeof(line) - 1] = '\0';
*e = ':';
e = p;
/* s e
* SectionXXX:YY: in 'blabla' */
*e = '\0';
strncpy(&file[0], s, file.size());
*e = ':';
/* Shrink to fit */
file.resize(strlen(file.c_str()));
file = btData.scriptNames.value(file, file);
std::string ms(640, '\0');
snprintf(&ms[0], ms.size(), "Script '%s' line %s: %s occured.\n\n%s",
file.c_str(), line, RSTRING_PTR(name), RSTRING_PTR(msg));
showMsg(ms);
}
static void mriBindingExecute() {
Config &conf = shState->rtData().config;
#if RAPI_MAJOR >= 2
/* Normally only a ruby executable would do a sysinit,
* but not doing it will lead to crashes due to closed
* stdio streams on some platforms (eg. Windows) */
int argc = 0;
char **argv = 0;
ruby_sysinit(&argc, &argv);
RUBY_INIT_STACK;
ruby_init();
std::vector<const char*> rubyArgsC{"mkxp-z"};
rubyArgsC.push_back("-e ");
void *node;
if (conf.jit.enabled) {
#if RAPI_FULL >= 310
// Ruby v3.1.0 renamed the --jit options to --mjit.
std::string verboseLevel("--mjit-verbose=");
std::string maxCache("--mjit-max-cache=");
std::string minCalls("--mjit-min-calls=");
rubyArgsC.push_back("--mjit");
#else
std::string verboseLevel("--jit-verbose=");
std::string maxCache("--jit-max-cache=");
std::string minCalls("--jit-min-calls=");
rubyArgsC.push_back("--jit");
#endif
verboseLevel += std::to_string(conf.jit.verboseLevel);
maxCache += std::to_string(conf.jit.maxCache);
minCalls += std::to_string(conf.jit.minCalls);
rubyArgsC.push_back(verboseLevel.c_str());
rubyArgsC.push_back(maxCache.c_str());
rubyArgsC.push_back(minCalls.c_str());
node = ruby_options(rubyArgsC.size(), const_cast<char**>(rubyArgsC.data()));
} else if (conf.yjit.enabled) {
rubyArgsC.push_back("--yjit");
// TODO: Maybe support --yjit-exec-mem-size, --yjit-call-threshold
node = ruby_options(rubyArgsC.size(), const_cast<char**>(rubyArgsC.data()));
} else {
node = ruby_options(rubyArgsC.size(), const_cast<char**>(rubyArgsC.data()));
}
int state;
bool valid = ruby_executable_node(node, &state);
if (valid)
state = ruby_exec_node(node);
if (state || !valid) {
// The message is formatted for and automatically spits
// out to the terminal, so let's leave it that way for now
/*
VALUE exc = rb_errinfo();
#if RAPI_FULL >= 250
VALUE msg = rb_funcall(exc, rb_intern("full_message"), 0);
#else
VALUE msg = rb_funcall(exc, rb_intern("message"), 0);
#endif
*/
showMsg("An error occurred while initializing Ruby. (Invalid JIT settings?)");
ruby_cleanup(state);
shState->rtData().rqTermAck.set();
return;
}
rb_enc_set_default_internal(rb_enc_from_encoding(rb_utf8_encoding()));
rb_enc_set_default_external(rb_enc_from_encoding(rb_utf8_encoding()));
#else
ruby_init();
rb_eval_string("$KCODE='U'");
#ifdef __WIN32__
if (!conf.winConsole) {
VALUE iostr = rb_str_new2("NUL");
// Sysinit isn't a thing yet, so send io to /dev/null instead
rb_funcall(rb_gv_get("$stderr"), rb_intern("reopen"), 1, iostr);
rb_funcall(rb_gv_get("$stdout"), rb_intern("reopen"), 1, iostr);
}
#endif
#endif
VALUE rbArgv = rb_get_argv();
for (const auto &str : conf.launchArgs)
rb_ary_push(rbArgv, rb_utf8_str_new_cstr(str.c_str()));
// Duplicates get pushed for some reason
rb_funcall(rbArgv, rb_intern("uniq!"), 0);
VALUE lpaths = rb_gv_get(":");
rb_ary_clear(lpaths);
#if defined(MKXPZ_BUILD_XCODE) && RAPI_MAJOR >= 2
std::string resPath = mkxp_fs::getResourcePath();
resPath += "/Ruby/" + std::to_string(RAPI_MAJOR) + "." + std::to_string(RAPI_MINOR) + ".0";
rb_ary_push(lpaths, rb_str_new(resPath.c_str(), resPath.size()));
#endif
if (!conf.rubyLoadpaths.empty()) {
/* Setup custom load paths */
for (size_t i = 0; i < conf.rubyLoadpaths.size(); ++i) {
std::string &path = conf.rubyLoadpaths[i];
VALUE pathv = rb_str_new(path.c_str(), path.size());
rb_ary_push(lpaths, pathv);
}
}
#ifndef WORKDIR_CURRENT
else {
rb_ary_push(lpaths, rb_utf8_str_new_cstr(mkxp_fs::getCurrentDirectory().c_str()));
}
#endif
RbData rbData;
shState->setBindingData(&rbData);
BacktraceData btData;
mriBindingInit();
std::string &customScript = conf.customScript;
if (!customScript.empty())
runCustomScript(customScript);
else
runRMXPScripts(btData);
#if RAPI_FULL > 187
VALUE exc = rb_errinfo();
#else
VALUE exc = rb_gv_get("$!");
#endif
if (!NIL_P(exc) && !rb_obj_is_kind_of(exc, rb_eSystemExit))
showExc(exc, btData);
ruby_cleanup(0);
shState->rtData().rqTermAck.set();
}
static void mriBindingTerminate() { throw Exception(Exception::SystemExit, " "); }
static void mriBindingReset() { throw Exception(Exception::Reset, " "); }