ModLoader and hooks

This commit is contained in:
DepressedTWM 2026-06-08 15:13:23 -04:00
parent d978fae6cf
commit 3ed5988375
61 changed files with 3304 additions and 2899 deletions

View file

@ -6,14 +6,15 @@ include(FindPackageHandleStandardArgs)
option(STEAM "Build for Steam" OFF)
option(DEBUG "Debug mode" OFF)
option(FORCE32 "Force 32bit compile on 64bit OS" OFF) # from MKXP
option(NATIVE "Use native instructions(for local use only)" ON)
option(PROFILE "Extra optimization" ON)
option(NATIVE "Use native instructions(for local use only)" OFF)
option(PROFILE "Extra optimization" OFF)
option(EXTRA_SECURITY "Extra security" OFF)
set(CMAKE_DISABLE_IN_SOURCE_BUILD TRUE)
set(VS_WINDOWS_TARGET_PLATFORM_MIN_VERSION "5.1.2600.0")
set(STEAMWORKS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/steamworks" CACHE PATH "Path to Steamworks folder")
set(ZLIB_USE_STATIC_LIBS ON)
set(OPENSSL_USE_STATIC_LIBS ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
# from MKXP
@ -53,12 +54,6 @@ else()
endif()
endif()
if(PROFILE)
if (!MSVC)
add_compile_options(-fprofile-generate -fprofile-use)
endif()
endif()
if(EXTRA_SECURITY)
if (MSVC)
add_compile_options(/guard:cf /GS /sdl /DYNAMICBASE /HIGHENTROPYVA /SAFESEH /guard:ehcont)
@ -128,6 +123,7 @@ set(MAIN_HEADERS
src/pipe.h
src/i18n.h
src/meow.h
src/modloader.h
)
set(MAIN_SOURCE
@ -172,6 +168,7 @@ set(MAIN_SOURCE
src/screen.cpp
src/i18n.cpp
src/meow.cpp
src/modloader.cpp
)
if(WIN32)
@ -333,6 +330,7 @@ set(BINDING_SOURCE
binding-mri/niko-binding.cpp
binding-mri/time-binding.cpp
binding-mri/shader-binging.cpp
binding-mri/modloader-binding.cpp
)
source_group("Binding Source" FILES ${BINDING_SOURCE} ${BINDING_HEADERS})
@ -355,6 +353,7 @@ if(LINUX)
target_link_libraries(${PROJECT_NAME} PRIVATE ${SECCOMP_LIBRARIES})
endif()
find_package(PkgConfig REQUIRED)
find_package(ZLIB REQUIRED)
find_package(SDL3 CONFIG)
find_package(SDL3_image CONFIG)
@ -363,6 +362,7 @@ find_package(SDL3_ttf CONFIG)
find_package(OpenAL CONFIG)
find_package(Boost REQUIRED COMPONENTS program_options chrono)
find_package(PhysFS CONFIG)
find_package(OpenSSL CONFIG REQUIRED COMPONENTS Crypto)
find_path(PIXMAN_INCLUDE_DIR NAMES pixman.h PATH_SUFFIXES pixman-1)
find_library(PIXMAN_LIBRARY NAMES pixman-1 pixman-1_static pixman-1_staticd)
find_package(Ruby 3.0 REQUIRED COMPONENTS Development)
@ -370,6 +370,7 @@ pkg_check_modules(SIGC2 REQUIRED sigc++-2.0)
pkg_check_modules(VORBISFILE REQUIRED vorbisfile)
find_package_handle_standard_args(pixman-1 DEFAULT_MSG PIXMAN_LIBRARY PIXMAN_INCLUDE_DIR)
mark_as_advanced(PIXMAN_INCLUDE_DIR PIXMAN_LIBRARY)
pkg_check_modules(LIBZIP REQUIRED libzip)
target_compile_definitions(${PROJECT_NAME}
PRIVATE
@ -379,7 +380,8 @@ target_include_directories(${PROJECT_NAME}
PRIVATE
src
include
${LIBZIP_INCLUDE_DIRS}
${OPENSSL_INCLUDE_DIR}
${PIXMAN_INCLUDE_DIR}
${SIGC2_INCLUDE_DIRS}
${Ruby_INCLUDE_DIRS}
@ -390,8 +392,10 @@ target_include_directories(${PROJECT_NAME}
target_link_libraries(${PROJECT_NAME}
PRIVATE
SDL3::SDL3 SDL3_image::SDL3_image SDL3_sound::SDL3_sound SDL3_ttf::SDL3_ttf
OpenSSL::Crypto
OpenAL::OpenAL
physfs
${LIBZIP_LIBRARIES}
${PIXMAN_LIBRARY}
${SIGC2_LIBRARIES}
${PLATFORM_LIBRARIES}

24
MODLOADER.md Normal file
View file

@ -0,0 +1,24 @@
# wtf is hook?
Hook - Ruby script executed in other script, with context(binding) of parent script.
# Hooks dirs
For more information you can read scripts/
* hooks/*/init/ - executed in script after initialization (you can change some variables and other)
'*' - class like Scene_Title
* hooks/Window_NameInput/init2 - Window_NameInput init function
* hooks/Scene_Map/main - main function of Map scene
* hooks/Main/at_exit - on game exit
* hooks/Main/start - at game start
# How add custom hooks?
use RPG::Mod.exec_hooks(path, binding)
path - path in game directory like "hooks/Scene_Title/init", you can't load something from other place, only from game direcory.
binding - its a Ruby binding.
# API
Custom API for mods:
work in progress
# how create mod?
First what you need it

View file

@ -11,7 +11,7 @@ Target of sunshine mod - improve original game.
* Cmake
* C/C++ compiler
* xxd
* Ruby (3 or higher)
* Ruby 3+
* Boost
* SDL3
* pixman
@ -21,6 +21,8 @@ Target of sunshine mod - improve original game.
* OpenAL
* PhysFS
* sigc++-2.0
* OpenSSL
* libzip
* GTK3(Linux only!)
* libxfconf(Linux only!)

View file

@ -84,6 +84,7 @@ void oneshotBindingInit();
void SunshineBindingInit();
void steamBindingInit();
void shaderBindingInit();
void ModLoaderBindingInit();
RB_METHOD(mriPrint);
RB_METHOD(mriP);
@ -119,6 +120,7 @@ static void mriBindingInit(){
SunshineBindingInit();
steamBindingInit();
shaderBindingInit();
ModLoaderBindingInit();
_rb_define_module_function(rb_mKernel, "rgss_main", mriRgssMain);
_rb_define_module_function(rb_mKernel, "rgss_stop", mriRgssStop);

View file

@ -0,0 +1,63 @@
//https://silverhammermba.github.io/emberb/c/
//https://docs.ruby-lang.org/capi/en/master/d8/d68/include_2ruby_2internal_2intern_2string_8h.html
#include "binding-util.h"
#include "sharedstate.h"
#include "filesystem.h"
#include "util.h"
#include "debugwriter.h"
#include "ruby/encoding.h"
#include "ruby/intern.h"
#include "ruby/thread.h"
#include <ruby.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <system_error>
#include <vector>
#include <string>
namespace fs = std::filesystem;
VALUE meow(std::vector<std::string> vec){
VALUE ary = rb_ary_new_capa((long)vec.size());
for (const std::string &s : vec) {
VALUE str = rb_str_new_cstr(s.c_str());
rb_ary_push(ary, str);
}
return ary;
}
static VALUE hooks(int argc, VALUE *argv, VALUE self) {
VALUE v_path = Qnil;
rb_scan_args(argc, argv, "01", &v_path);
std::string hook_path = "hooks";
if (v_path != Qnil) {
v_path = StringValue(v_path);
hook_path = StringValueCStr(v_path);
}
std::vector<std::string> files = {};
if(!std::filesystem::exists(hook_path)){
return meow(files);
}
for (const auto &entry : std::filesystem::directory_iterator(hook_path, std::filesystem::directory_options::skip_permission_denied)) {
std::error_code ec;
auto p = entry.path();
if (!std::filesystem::is_regular_file(p, ec) || ec) continue;
auto ext = p.extension().string();
if (ext != ".rb") continue;
std::string full = p.string();
Debug() << "[MODLOADER] Executing hook: " << full;
files.push_back(full);
}
return meow(files);
}
void ModLoaderBindingInit(){
Debug() << "[MODLOADER] initalizing binding...";
VALUE klass = rb_define_module("ModLoader");
rb_define_module_function(klass, "hooks", RUBY_METHOD_FUNC(hooks), -1);
}

View file

@ -64,9 +64,6 @@ module RPG
def self.windowskin(filename)
self.load_bitmap("Graphics/Windowskins/", filename)
end
#def self.minimap(zone, location)
# self.load_bitmap("Graphics/Menus/minimap/", zone + "_" + location)
#end
def self.tile(filename, tile_id, hue)
key = [filename, tile_id, hue]
if not @cache.include?(key) or @cache[key].disposed?
@ -1276,6 +1273,14 @@ module RPG
attr_accessor :timings
end
class Mod
def self.exec_hooks(path, b)
ModLoader.hooks(path).each do |item|
eval(File.read(item), b)
end
end
end
class Tileset
def initialize
@id = 0

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,6 @@
# (default: disabled)
# screenMode=false
# Display current FPS in Window title
# (default: disabled)
# printFPS=false
@ -185,5 +184,12 @@
# [Windows only] Alloc console
# Launch a console window with debug information along with the game.
# (default: false)
#
# Windows_AllocConsole=false
# Mods directory path
# Path to mods directory
# (default: "mods")
#
# ModsDirPath="mods"

View file

@ -32,6 +32,7 @@ class Credits_Message
# Animation flags
@fade_in = false
@fade_out = false
RPG::Mod.exec_hooks("hooks/Credits_Message/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -7,6 +7,7 @@ class FastTravel
@name = name
@maps = maps
@locations = locations;
RPG::Mod.exec_hooks("hooks/FastTravel/init", binding)
end
end
@ -28,6 +29,7 @@ class FastTravel
@next_right = next_right
@next_bottom = next_bottom
@next_left = next_left
RPG::Mod.exec_hooks("hooks/ZoneLocations/init", binding)
end
end

View file

@ -29,6 +29,7 @@ class Desktop_Message
# Animation flags
@fade_in = false
@fade_out = false
RPG::Mod.exec_hooks("hooks/Desktop_Message/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -53,6 +53,7 @@ class Doc_Message
# Animation flags
@fade_in = false
@fade_out = false
RPG::Mod.exec_hooks("hooks/Doc_Message/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -28,6 +28,7 @@ class Ed_Message
@fade_out = false
@fade_in_text = false
@fade_out_text = false
RPG::Mod.exec_hooks("hooks/Ed_Message/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -95,6 +95,7 @@ class FastTravel
@fade_out = false
@transfer_player = nil
RPG::Mod.exec_hooks("hooks/FastTravel/init", binding)
end
#WME

View file

@ -22,6 +22,7 @@ class Game_Actor < Game_Battler
#--------------------------------------------------------------------------
def initialize(actor_id)
super()
RPG::Mod.exec_hooks("hooks/Game_Actor/init", binding)
setup(actor_id)
end
#--------------------------------------------------------------------------

View file

@ -11,6 +11,7 @@ class Game_Actors
#--------------------------------------------------------------------------
def initialize
@data = []
RPG::Mod.exec_hooks("hooks/Game_Actors/init", binding)
end
#--------------------------------------------------------------------------
# * Get Actor

View file

@ -13,6 +13,7 @@ class Game_CommonEvent
def initialize(common_event_id)
@common_event_id = common_event_id
@interpreter = nil
RPG::Mod.exec_hooks("hooks/Game_CommonEvent/init", binding)
refresh
end
#--------------------------------------------------------------------------

View file

@ -41,6 +41,9 @@ class Game_Event < Game_Character
@custom_flags.concat(special.flags)
@collision = special.collision
end
RPG::Mod.exec_hooks("hooks/Game_Event/init", binding)
# Move to starting position
moveto(@event.x, @event.y)
refresh

View file

@ -21,6 +21,7 @@ class Game_FastTravel
@unlocked = {}
@zone = nil
@enabled = false
RPG::Mod.exec_hooks("hooks/Game_FastTravel/init", binding)
end
def unlock(map, id, x, y, dir)

View file

@ -12,6 +12,7 @@ class Game_Follower < Game_Character
@leader = leader
self.actor = actor
moveto(leader.x, leader.y)
RPG::Mod.exec_hooks("hooks/Game_Follower/init", binding)
end
# Overrides

View file

@ -83,6 +83,7 @@ class Game_Map
@map_id = 0
@display_x = 0
@display_y = 0
RPG::Mod.exec_hooks("hooks/Game_Map/init", binding)
end
#--------------------------------------------------------------------------
# * Setup

View file

@ -14,7 +14,9 @@ class Game_Oneshot
@plight_timer = nil
@wallpaper = nil
@bruteforce_start = nil
RPG::Mod.exec_hooks("hooks/Game_Oneshot/init", binding)
end
def self.get_user_name
user_name = (Oneshot::USER_NAME).split(/\s+/)
#user_name = (Steam.enabled? ? Steam::USER_NAME : Oneshot::USER_NAME).split(/\s+/)

View file

@ -25,6 +25,7 @@ class Game_Party
@items = {}
@weapons = {}
@armors = {}
RPG::Mod.exec_hooks("hooks/Game_Party/init", binding)
end
#--------------------------------------------------------------------------
# * Initial Party Setup

View file

@ -45,6 +45,7 @@ class Game_Picture
@tone_duration = 0
@angle = 0
@rotate_speed = 0
RPG::Mod.exec_hooks("hooks/Game_Picture/init", binding)
end
#--------------------------------------------------------------------------
# * Show Picture

View file

@ -38,6 +38,7 @@ class Game_Screen
@weather_type_target = 0
@weather_max_target = 0.0
@weather_duration = 0
RPG::Mod.exec_hooks("hooks/Game_Screen/init", binding)
end
#--------------------------------------------------------------------------
# * Start Changing Color Tone

View file

@ -11,6 +11,7 @@ class Game_SelfSwitches
#--------------------------------------------------------------------------
def initialize
@data = {}
RPG::Mod.exec_hooks("hooks/Game_SelfSwitches/init", binding)
end
#--------------------------------------------------------------------------
# * Get Self Switch

View file

@ -11,6 +11,7 @@ class Game_Switches
#--------------------------------------------------------------------------
def initialize
@data = []
RPG::Mod.exec_hooks("hooks/Game_Switches/init", binding)
end
#--------------------------------------------------------------------------
# * Get Switch
@ -47,4 +48,4 @@ end
# some switches that we know of
# 101 - Use tower footsplashes i think
# 112 - Niko in minecart
# 160 - re-playing game (when Niko is TWM)
# 160 - re-playing game (when Niko is TWM)

View file

@ -35,6 +35,7 @@ class Game_System
@message_frame = 0
@save_count = 0
@magic_number = 0
RPG::Mod.exec_hooks("hooks/Game_System/init", binding)
end
#--------------------------------------------------------------------------
# * Play Background Music

View file

@ -125,6 +125,7 @@ class Game_Temp
@menus_visible = false
@countdown_password = ""
@igt_timer_visible = false
RPG::Mod.exec_hooks("hooks/Game_Temp/init", binding)
end
def bgm_fadein(game_system)

View file

@ -11,6 +11,7 @@ class Game_Variables
#--------------------------------------------------------------------------
def initialize
@data = []
RPG::Mod.exec_hooks("hooks/Game_Variables/init", binding)
end
#--------------------------------------------------------------------------
# * Get Variable

View file

@ -5,11 +5,13 @@
#==============================================================================
at_exit do
RPG::Mod.exec_hooks("hooks/Main/at_exit", binding)
Wallpaper.reset
save unless $game_switches[99] || ($game_system.map_interpreter.running? || !$scene.is_a?(Scene_Map))
end
begin
RPG::Mod.exec_hooks("hooks/Main/start", binding)
$console = Graphics.fullscreen
Graphics.frame_rate = 60
Font.default_size = 20

View file

@ -7,6 +7,7 @@ class Particle
@sprite.oy = bitmap.height / 2
self.x = rand(Graphics.width)
self.y = rand(Graphics.height)
RPG::Mod.exec_hooks("hooks/Particle/main", binding)
end
# Link various things to the sprite

View file

@ -58,6 +58,9 @@ class Scene_Map
@blackfade.bitmap.fill_rect(0, 0, Graphics.width, Graphics.height, Color.new(0, 0, 0))
@blackfade.visible = false
@blackfade.z = 9999
RPG::Mod.exec_hooks("hooks/Scene_Map/main", binding)
# Transition run
Graphics.transition
# Main loop

View file

@ -59,13 +59,15 @@ class Scene_Title
end
end
# check for debug file to add debug items
if Settings[:debug]
$game_party.gain_item(54, 1) # debug save
$game_party.gain_item(82, 1) # plight skip
$game_party.gain_item(81, 1) #George reroler
end
RPG::Mod.exec_hooks("hooks/Scene_Title/init", binding)
# check for debug file to add debug items
if Settings[:debug]
$game_party.gain_item(54, 1) # debug save
$game_party.gain_item(82, 1) # plight skip
$game_party.gain_item(81, 1) # George reroler
end
@sprite.zoom_x = 2.0
@sprite.zoom_y = 2.0
# Create/render menu options
@ -76,7 +78,7 @@ class Scene_Title
@menu.bitmap.draw_text(MENU_X, MENU_Y + 25, 150, 24, tr("Settings"))
@menu.bitmap.draw_text(MENU_X, MENU_Y + 50, 150, 24, tr("Exit"))
#Debug info like in minecraft Forge :P
#Debug info like in minecraft Forge :P
@debug = Sprite.new
@debug.z += @menu.z
@debug.bitmap = Bitmap.new(Graphics.width, Graphics.height)
@ -88,7 +90,7 @@ class Scene_Title
@menu.bitmap.draw_text(MENU_X, MENU_Y + 75, 150, 24, tr("..."))
end
Language.register_text_sprite(self.class.name + "_contents", @menu.bitmap)
Language.register_text_sprite(self.class.name + "_contents", @menu.bitmap)
# Make cursor graphic
@cursor = Sprite.new
@ -98,9 +100,10 @@ class Scene_Title
@cursor.bitmap = RPG::Cache.menu('cursor')
@cursor.x = MENU_X - 12
@cursor.y = MENU_Y + (20 - @cursor.bitmap.height) / 2
# Initialize cursor position
@cursor_pos = 0.0
# Play title BGM
if File.exist?("badend.lock")
@ -138,7 +141,7 @@ class Scene_Title
@menu.dispose
@cursor.bitmap.dispose
@cursor.dispose
@window_settings_title.dispose
@window_settings_title.dispose
Audio.bgm_fade(60)
Graphics.transition(60)
# Run automatic change for BGM and BGS set with map

View file

@ -26,7 +26,8 @@ class Sprite_Character
@text_sprite.bitmap = Bitmap.new(Graphics.width, 24)
@text_sprite.bitmap.font.size = 12
@character = character
RPG::Mod.exec_hooks("hooks/Sprite_Character/init", binding)
update
end
#--------------------------------------------------------------------------

View file

@ -26,6 +26,7 @@ class Sprite_Footsplash < Sprite
self.oy = 80
self.bitmap = RPG::Cache.misc('foot_splash')
self.src_rect.set(0, 0, 80, 80)
RPG::Mod.exec_hooks("hooks/Sprite_Footsplash/init", binding)
update
end

View file

@ -8,6 +8,7 @@ class Light < RPG::Sprite
@map_y = y
self.z = 9999
self.visible = true
RPG::Mod.exec_hooks("hooks/Light/init", binding)
update
end

View file

@ -41,7 +41,7 @@ class Sprite_MapText < Sprite
self.oy = 24
self.ox = width/2
RPG::Mod.exec_hooks("hooks/Sprite_MapText/init", binding)
update
end

View file

@ -14,6 +14,7 @@ class Sprite_Picture < Sprite
def initialize(viewport, picture)
super(viewport)
@picture = picture
RPG::Mod.exec_hooks("hooks/Sprite_Picture/init", binding)
update
end
#--------------------------------------------------------------------------

View file

@ -16,6 +16,7 @@ class Sprite_Timer < Sprite
self.x = 640 - self.bitmap.width
self.y = 0
self.z = 500
RPG::Mod.exec_hooks("hooks/Sprite_Timer/init", binding)
update
end
#--------------------------------------------------------------------------

View file

@ -87,6 +87,7 @@ class Spriteset_Map
@bulb.opacity = has_lightbulb? ? 255 : 0
# Panorama animation timer
@pan_animate_timer = 0
RPG::Mod.exec_hooks("hooks/Spriteset_Map/init", binding)
# Frame update
update
end

View file

@ -21,6 +21,7 @@ class Window_Base < Window
self.width = width
self.height = height
self.z = 100
RPG::Mod.exec_hooks("hooks/Window_Base/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -11,6 +11,7 @@ class Window_DebugLeft < Window_Selectable
def initialize
super(0, 0, 192, Graphics.height)
self.index = 0
RPG::Mod.exec_hooks("hooks/Window_DebugLeft/init", binding)
refresh
end
#--------------------------------------------------------------------------

View file

@ -21,6 +21,7 @@ class Window_DebugRight < Window_Selectable
@item_max = 10
@mode = 0
@top_id = 1
RPG::Mod.exec_hooks("hooks/Window_DebugRight/init", binding)
refresh
end
#--------------------------------------------------------------------------

View file

@ -15,6 +15,7 @@ class Window_Help < Window_Base
self.visible = false
self.back_opacity = 230
self.z = 9998
RPG::Mod.exec_hooks("hooks/Window_Help/init", binding)
end
#--------------------------------------------------------------------------
# * Set Text

View file

@ -25,6 +25,7 @@ class Window_InputNumber < Window_Base
@index = 0
refresh
update_cursor_rect
RPG::Mod.exec_hooks("hooks/Window_InputNumber/init", binding)
end
#--------------------------------------------------------------------------
# * Get Number

View file

@ -25,6 +25,7 @@ class Window_Item < Window_Selectable
@fade_out = false
@help_window = Window_Help.new
RPG::Mod.exec_hooks("hooks/Window_Item/init", binding)
end
#--------------------------------------------------------------------------
# * Get Item

View file

@ -30,6 +30,7 @@ class Window_MainMenu < Window_Selectable
draw_item(i, normal_color)
end
self.z = 9998
RPG::Mod.exec_hooks("hooks/Window_MainMenu/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -42,6 +42,7 @@ class Window_Message < Window_Selectable
# Text blip sound
@blipsound = nil
RPG::Mod.exec_hooks("hooks/Window_Message/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -35,6 +35,7 @@ class Window_NameEdit < Window_Base
@index = name_array.size
refresh
update_cursor_rect
RPG::Mod.exec_hooks("hooks/Window_NameEdit/init", binding)
end
#--------------------------------------------------------------------------
# * Return to Default Name

View file

@ -15,6 +15,7 @@ class NameInputMode
@size = names.map { |n| bitmap.text_size(n).width }.max
@index = 0
@count = 0
RPG::Mod.exec_hooks("hooks/NameInputMode/init", binding)
end
def cycle
@ -47,6 +48,7 @@ class Window_NameInput < Window_Base
@ok_text = "OK"
@char_w = 28
@char_h = 32
RPG::Mod.exec_hooks("hooks/Window_NameInput/init", binding)
end
def init
# Create dimension information
@ -63,6 +65,8 @@ class Window_NameInput < Window_Base
end
end
RPG::Mod.exec_hooks("hooks/Window_NameInput/init2", binding)
refresh
update_cursor_rect
self

View file

@ -22,6 +22,7 @@ class Window_Selectable < Window_Base
@item_max = 1
@column_max = 1
@index = -1
RPG::Mod.exec_hooks("hooks/Window_Selectable/init", binding)
end
#--------------------------------------------------------------------------
# * Set Cursor Position

View file

@ -36,6 +36,7 @@ class Window_Settings
@transfer_player = nil
@visible = false
RPG::Mod.exec_hooks("hooks/Window_Settings/init", binding)
end
def dispose
@ -449,4 +450,4 @@ class Window_Settings
def opacity
@bg.opacity
end
end
end

View file

@ -21,6 +21,7 @@ class Window_TPtL < Window_Selectable
draw_item(i, normal_color)
end
self.z = 9998
RPG::Mod.exec_hooks("hooks/Window_TPtL/init", binding)
end
#--------------------------------------------------------------------------
# * Dispose

View file

@ -79,6 +79,7 @@ void Config::read(int argc, char *argv[]){
PO_DESC(defScreenW, int, 0) \
PO_DESC(defScreenH, int, 0) \
PO_DESC(windowTitle, std::string, "") \
PO_DESC(ModsDirPath, std::string,"mods") \
PO_DESC(fixedFramerate, int, 0) \
PO_DESC(frameSkip, bool, true) \
PO_DESC(syncToRefreshrate, bool, false) \

View file

@ -39,6 +39,7 @@ struct Config{
int defScreenW;
int defScreenH;
std::string windowTitle;
std::string ModsDirPath;
int fixedFramerate;
bool frameSkip;

View file

@ -41,6 +41,9 @@ struct Exception{
SDLError,
MKXPError,
//modloader
ModLoaderError,
// For crash()
MEOW
};

View file

@ -49,6 +49,7 @@
#include "gl-fun.h"
#include "i18n.h"
#include "security.h"
#include "modloader.h"
#include "meow.h"
@ -271,6 +272,7 @@ int main(int argc, char *argv[]){
freopen("CONOUT$", "w", stderr);
}
#endif
if (!conf.gameFolder.empty()){
if (chdir(conf.gameFolder.c_str()) != 0){
@ -279,6 +281,15 @@ int main(int argc, char *argv[]){
}
}
std::string new_path = ModLoader(conf);
if(new_path != ""){
if (chdir(new_path.c_str()) != 0){
crash(Exception::MEOW, "Unable to switch into new gameFolder %s", new_path);
return 0;
}
}
extern int screenMain(Config &conf);
if (conf.screenMode)
return screenMain(conf);

211
src/modloader.cpp Normal file
View file

@ -0,0 +1,211 @@
//https://terminalroot.com/how-to-generate-sha256-hash-with-cpp-and-openssl/
//https://stackoverflow.com/questions/15347123/how-to-construct-a-stdstring-from-a-stdvectorstring
//https://stackoverflow.com/questions/54260184/how-to-sha256-hash-a-text-file-in-chunks-with-openssl-sha-h
//https://stackoverflow.com/questions/1673445/how-to-convert-unsigned-char-to-stdstring-in-c
//https://www.geeksforgeeks.org/cpp/cpp-program-to-read-and-print-all-files-from-a-zip-file/
#include "modloader.h"
#include "debugwriter.h"
#include "meow.h"
#include "config.h"
#include <cctype>
#include <string>
#include <iostream>
#include <filesystem>
#include <vector>
#include <iomanip>
#include <sstream>
#include <fstream>
#include <openssl/sha.h>
#include <SDL3/SDL_stdinc.h>
#include <zip.h>
#include <locale>
#include <filesystem>
#include <string>
#include <vector>
#include <algorithm>
#include <system_error>
namespace fs = std::filesystem;
// Get directory for storing cached builds
//TODO: support for other OS and platforms
std::string getCacheDir(){
#ifdef _WIN32
return SDL_getenv("Temp");
#elif defined(__linux__)
return std::string(SDL_getenv("HOME")) + "/.cache";
#else
return "idk";
#endif
}
//helper
bool ensure_parent_dir(const std::filesystem::path& p){
if (p.has_parent_path()){
std::error_code ec;
std::filesystem::create_directories(p.parent_path(), ec);
return !ec;
}
return true;
}
std::string sha512(const std::string str){
unsigned char hash[SHA512_DIGEST_LENGTH];
SHA512_CTX sha512;
SHA512_Init(&sha512);
SHA512_Update(&sha512, str.c_str(), str.size());
SHA512_Final(hash, &sha512);
std::stringstream ss;
for(int i = 0; i < SHA512_DIGEST_LENGTH; i++){
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>( hash[i] );
}
return ss.str();
}
std::string sha256_file(const std::string &fn) {
FILE *file = fopen(fn.c_str(), "rb");
if (!file) {
crash(Exception::ModLoaderError, "Failed to load mod, filesystem error.");
}
unsigned char buf[1024];
unsigned char hash[SHA256_DIGEST_LENGTH];
size_t len;
SHA256_CTX ctx;
SHA256_Init(&ctx);
while ((len = fread(buf, 1, sizeof buf, file)) != 0){
SHA256_Update(&ctx, buf, len);
}
fclose(file);
SHA256_Final(hash, &ctx);
std::string out;
out.reserve(SHA256_DIGEST_LENGTH * 2);
char hex[3] = {0};
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
snprintf(hex, sizeof hex, "%02x", hash[i]);
out += hex;
}
return out;
}
std::string ModLoader(Config conf){
//"mods" by defailt
std::string path = conf.ModsDirPath;
if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) {
Debug() << "[MODLOADER] Mods directory not found, skip.";
return "";
}
//buildID - unique ID of a certain combination of mods
std::string buildID = "";
std::string buildID_tmp = "";
std::vector<std::string> mod_list = {};
try{
//1.check if any zip(mod) file, 2. calculate sha256 hash of zip(mod) files
for (const auto &entry : std::filesystem::directory_iterator(path, std::filesystem::directory_options::skip_permission_denied)) {
std::error_code ec;
auto p = entry.path();
if (!std::filesystem::is_regular_file(p, ec) || ec) continue;
auto ext = p.extension().string();
if (ext != ".zip") continue;
std::string full = p.string();
Debug() << "[MODLOADER] " << full;
mod_list.push_back(full);
try {
auto h = sha256_file(full);
buildID_tmp.append(h);
}catch (const std::exception &e) {
Debug() << "[MODLOADER] sha256 failed for " << full << ", ex: " << e.what();
continue;
}
}
buildID = sha512(buildID_tmp);
Debug() << "[MODLOADER] BuildID: " << buildID;
std::string path3 = getCacheDir() + "/sunshine-" + buildID;
int err = 0;
if(!std::filesystem::exists(path3)){
std::error_code ec;
fs::create_directory(path3, ec);
if (ec) {
crash(Exception::ModLoaderError, "Failed to create destination: %s", ec.message().c_str());
}
fs::copy(conf.gameFolder, path3, fs::copy_options::recursive | fs::copy_options::overwrite_existing, ec);
if (ec) {
crash(Exception::ModLoaderError, "Copy error: %s", ec.message().c_str());
}
char state2[1024];
//Extracting zipsodpsofspo idk
for (size_t i = 0; i < mod_list.size(); ++i){
zip_t* za = zip_open(mod_list[i].c_str(), ZIP_RDONLY, &err);
if (!za){
crash(Exception::ModLoaderError, "Failed to open zip");
}
zip_int64_t n = zip_get_num_entries(za, 0);
for (zip_uint64_t i2 = 0; i2 < static_cast<zip_uint64_t>(n); ++i2) {
zip_stat_t st;
if (zip_stat_index(za, i2, 0, &st) != 0) {
crash(Exception::ModLoaderError, "zip_stat_index failed");
}
std::string name = st.name;
std::filesystem::path target = path3 / std::filesystem::path(name);
// If entry name ends with '/', treat as directory
if (!name.empty() && name.back() == '/') {
std::error_code ec;
std::filesystem::create_directories(target, ec);
if (ec) crash(Exception::MEOW, "Failed to create dir");
continue;
}
if (!ensure_parent_dir(target)){
crash(Exception::ModLoaderError, "Failed to create parent dirs");
}
zip_file_t* zf = zip_fopen_index(za, i2, 0);
if (!zf) {
crash(Exception::ModLoaderError, "zip_fopen_index failed");
}
std::ofstream out(target, std::ios::binary);
if (!out) {
crash(Exception::ModLoaderError, "Failed to open output file %s", target.c_str());
zip_fclose(zf);
}
const zip_uint64_t bufsize = 4096;
std::vector<char> buf(bufsize);
zip_int64_t bytes_read;
zip_uint64_t remaining = st.size;
while (remaining > 0) {
zip_uint64_t to_read = std::min<zip_uint64_t>(bufsize, remaining);
bytes_read = zip_fread(zf, buf.data(), to_read);
if (bytes_read < 0) {
crash(Exception::ModLoaderError, "zip_fread error for %s", name.c_str());
break;
}
out.write(buf.data(), bytes_read);
remaining -= bytes_read;
}
zip_fclose(zf);
}
zip_close(za);
}
return path3;
}else{
return path3;
}
}catch(const std::exception& e){
crash(Exception::ModLoaderError, "Something is wrong, Exception: %s ", e.what());
}
}

4
src/modloader.h Normal file
View file

@ -0,0 +1,4 @@
//modloader wow
#include <string>
#include "config.h"
std::string ModLoader(Config conf);

View file

@ -17,7 +17,7 @@
SCMP_SYS(move_mount), SCMP_SYS(mount_setattr), SCMP_SYS(mount), SCMP_SYS(lsm_set_self_attr), SCMP_SYS(lsm_list_modules), SCMP_SYS(lsm_get_self_attr),
SCMP_SYS(process_vm_readv), SCMP_SYS(process_vm_writev), SCMP_SYS(ptrace), SCMP_SYS(swapon), SCMP_SYS(swapoff), SCMP_SYS(shutdown), SCMP_SYS(settimeofday),
SCMP_SYS(sethostname), SCMP_SYS(umount), SCMP_SYS(umount2), SCMP_SYS(vm86old), SCMP_SYS(vm86), SCMP_SYS(setgroups), SCMP_SYS(setgid), SCMP_SYS(setfsuid),
SCMP_SYS(setfsgid), SCMP_SYS(setdomainname), SCMP_SYS(setns), SCMP_SYS(setpgid), SCMP_SYS(pciconfig_write), SCMP_SYS(shutdown), SCMP_SYS(shutdown)};
SCMP_SYS(setfsgid), SCMP_SYS(setdomainname), SCMP_SYS(setns), SCMP_SYS(setpgid), SCMP_SYS(pciconfig_write)};
#endif
void SecurityManagerInit(){