2024-11-27 19:54:06 +02:00
# Kawariki MKXP preload infrastructure
module Preload
# Kawariki mkxp resources location
Path = File . dirname __FILE__
# Require common libs
def self . require ( name )
Kernel . require File . join ( Path , " libs " , name )
end
module Common
# In RMXP mode, Kernel.print opens message boxes
def print ( text )
STDOUT . puts ( " [preload] " + text . to_s )
end
end
extend Common
# -------------------------------------------------------------------------
# Patches
# -------------------------------------------------------------------------
class Context
include Common
def initialize ( scripts )
@scripts = scripts
@script_instances = { }
@options = { }
@blacklist = [ ]
@delay = { }
@script_id_digits = Math . log10 ( scripts . size ) . ceil
end
attr_reader :script_id_digits , :script_loc_format
# Scripts
def script ( i )
@script_instances [ i ] || = Script . new self , i , @scripts [ i ]
end
def script_count
@scripts . size
end
def each_script
( 0 ... @scripts . size ) . each { | i | yield script i }
end
def last_script
script ( script_count - 1 )
end
def script_loc ( scriptid )
return script ( scriptid ) . loc
end
def blacklisted? ( script )
@blacklist . include? script . name
end
# def add_script(name, code)
# # Find an empty script slot (if any) to reuse (check for empty string)
# empty_slot = @scripts.find { |script| script[0].is_a?(String) && script[0].empty? }
#
# if empty_slot
# # Reuse the empty slot
# empty_slot[0] = name # Set the script name
# empty_slot[3] = code # Set the script code
# else
# # No empty slot found, so we pop the last script and push a new one
# @scripts.pop
# @scripts.push [name, "", nil, code]
# end
# end
def add_script ( name , code )
# Find the first empty slot (check for empty string) that is not the last slot
empty_slot = @scripts . find { | script | script [ 0 ] . is_a? ( String ) && script [ 0 ] . empty? && @scripts . index ( script ) < @scripts . length - 1 }
if empty_slot
# Reuse the empty slot and replace it with the new script
empty_slot [ 0 ] = name # Set the script name
empty_slot [ 3 ] = code # Set the script code
else
# No empty slot found, so insert the new script 5 positions before the end
insert_index = [ @scripts . length - 5 , 0 ] . max # Ensure the index is not negative
@scripts . insert ( insert_index , [ name , " " , nil , code ] )
end
end
# def add_script(name, code)
# @scripts.pop
# @scripts.push [name, "", nil, code]
# # TODO: Find an empty script to canibalize instead
# end
# Read options from environment
FalseEnvValues = [ nil , " " , " 0 " , " no " ]
def read_env ( env = ENV )
env_bool = - > ( name ) { ! FalseEnvValues . include? ( env [ name ] ) }
env_str = - > ( name ) { e = env [ name ] ; e unless e == " " }
env_list = - > ( name , delim ) { e = env [ name ] ; e . nil? ? [ ] : e . split ( delim ) }
set :dump_scripts_raw , env_str . ( " KAWARIKI_MKXP_DUMP_SCRIPTS " )
set :dump_scripts_patched , env_str . ( " KAWARIKI_MKXP_DUMP_PATCHED_SCRIPTS " )
mark :dont_run_game if env_bool . ( " KAWARIKI_MKXP_DRY_RUN " )
mark :no_font_effects if env_bool . ( " KAWARIKI_MKXP_NO_FONT_EFFECTS " )
@blacklist = env_list . ( " KAWARIKI_MKXP_FILTER_SCRIPTS " , " , " )
end
def read_system ( system = System )
# TODO: Non mkxp-z variants
set :mkxp_version , system :: VERSION
set :mkxp_version_tuple , ( system :: VERSION . split " . " ) . map { | d | d . to_i }
if ( self [ :mkxp_version_tuple ] < = > [ 2 , 4 ] ) > = 0 then
mark :mkxpz_24
_config = CFG
else
_config = system :: CONFIG
end
# set :rgss_version, _config["rgssVersion"].to_i
# puts "jj"+_config["gameFolder"].to_s
# Preload.require "PreloadIni.rb"
# puts "vvv"+ENV["rpgvers"]
set :rgss_version , ENV [ " rpgvers " ] . to_i
# puts self[:zeusrpgver]
# puts vcode
if defined? ( RGSS_VERSION ) && RGSS_VERSION == " 3.0.1 " then
# See mkxp-z/mri-binding.cpp
# puts "vvv"+RGSS_VERSION
set :rgss_version , 3
end
# FIXME: can this be reliably retrieved from MKXP if set to 0 in config?
if self [ :rgss_version ] == 0 then
print " Warning: rgssVersion not set in MKXP config. Are you running mkxp directly? "
if RGSS_VERSION == " 3.0.1 " then
# See mkxp-z/mri-binding.cpp
set :rgss_version , 3
else
print " Warning: Cannot guess RGSS version. Kawariki should automatically set it correctly. "
end
end
if self [ :mkxp_version ] == " MKXPZ_VERSION " then
print " Note: Using mkxp-z with broken System::VERSION reporting. Cannot detect real mkxp-z version "
set :mkxp_version , " mkxp-z "
end
end
# Options
def set ( sym , value = true )
@options . store sym , value unless value . nil?
end
def [] ( sym )
@options [ sym ]
end
def mark ( * flags )
flags . each { | flag | set flag , true }
end
def flag? ( flag )
@options . key? flag
end
# Delay
DelaySlots = [ :after_patches ]
def delay ( slot , & p )
raise " Unknown delay slot #{ slot } " unless DelaySlots . include? slot
@delay [ slot ] = [ ] unless @delay . key? slot
@delay [ slot ] . push p
end
def run_delay_slot ( slot , * args )
raise " Unknown delay slot #{ slot } " unless DelaySlots . include? slot
if @delay [ slot ] then
@delay [ slot ] . each { | p | p . call ( self , * args ) }
@delay . delete slot
end
end
end
class Script
def initialize ( context , i , script )
@context = context
@index = i
@script = script
@log = [ ]
end
attr_reader :context
attr_reader :index
def log ( msg = nil )
@log . push msg unless msg . nil? || @log . last == msg
@log
end
def loc
" # #{ index . to_s . rjust @context . script_id_digits } ' #{ name } ' "
end
def [] ( i )
@script [ i ]
end
def name
@script [ 1 ]
end
def source
@script [ 3 ]
end
def sub! ( * p )
@script [ 3 ] . gsub! ( * p )
end
def source = ( code )
@script [ 3 ] = code
end
def load_file ( path )
log " replaced with #{ File . basename path } "
@script [ 3 ] = File . read ( path )
end
def remove
log " removed "
@script [ 3 ] = " "
end
# Extract $imported key only once
# $imported['Hello'] = 1
# $imported[:Hello] = true
# ($imported ||= {})["Hello"] = true
# Type (String/Symbol) is preserved
ImportedKeyExpr = / ^ \ s*(?: \ $imported| \ ( \ s* \ $imported(?: \ s* \ | \ |= \ s* \ { \ s* \ })? \ s* \ )) \ [(: \ w+|'[^']+'|"[^"]+") \ ] \ s*= \ s*(.+) \ s*$ /
def _extract_imported
if source . nil?
puts " Warning: 'source' is nil at the beginning of _extract_imported "
return
end
# Ensure 'source' is a string, and log the class type if it's not
unless source . is_a? ( String )
puts " Warning: 'source' is not a string, it's a #{ source . class } ! "
return
end
# Log the encoding type for debugging purposes
# puts "Source encoding before: #{source.encoding.name}"
# Force early return if source is unexpectedly nil just before encoding
if source . nil?
puts " Warning: 'source' unexpectedly nil before encoding "
return
end
# Backup the original source value before encoding, to see if it changes unexpectedly
original_source = source . dup
2024-12-03 03:59:12 +02:00
# puts "Before encoding: source = #{source.inspect}" # Debugging log for source
2024-11-27 19:54:06 +02:00
# Force encode to ASCII-8BIT (binary ASCII), replacing non-ASCII characters with '?'
if source . encoding . name != " ASCII-8BIT "
begin
2024-12-03 03:59:12 +02:00
# Ensure source is in UTF-8 first to avoid mix of encodings (UTF-8 and ASCII-8BIT)
# source = source.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
# Now attempt to encode to ASCII-8BIT (ensure no mix of encodings)
# Force both source and encoded_source to be in the same encoding (UTF-8)
2024-11-27 19:54:06 +02:00
encoded_source = source . encode ( " ASCII-8BIT " , invalid : :replace , undef : :replace , replace : " ? " )
# If encoding results in an empty string or nil, restore original source
if encoded_source . nil? || encoded_source . empty?
source = original_source
return
else
# Otherwise, use the encoded result
source = encoded_source
end
rescue Encoding :: UndefinedConversionError , Encoding :: InvalidByteSequenceError = > e
puts " Encoding failed: #{ e . message } "
return
rescue StandardError = > e
2024-12-03 03:59:12 +02:00
# puts "Unexpected error during encoding: #{e.message}"
2024-11-27 19:54:06 +02:00
return
end
end
2024-12-03 03:59:12 +02:00
2024-11-27 19:54:06 +02:00
# Check source after encoding to ensure it isn't nil
if source . nil?
# puts "Warning: 'source' is nil after encoding!"
return
end
# After encoding (or skipping), match the regex pattern
match = ImportedKeyExpr . match ( source )
# Check if a match was found
@imported_entry = ! match . nil?
return unless @imported_entry
# Extract the imported key and value from the match result
@imported_key = match [ 1 ] [ 0 ] == ':' ? match [ 1 ] [ 1 .. ] . to_sym : match [ 1 ] [ 1 ... - 1 ]
@imported_value = match [ 2 ]
end
def imported_entry?
_extract_imported if @imported_entry . nil?
@imported_entry
end
def imported_key
_extract_imported if @imported_entry . nil?
@imported_key
end
def imported_value
_extract_imported if @imported_entry . nil?
@imported_value
end
end
class Patch
include Common
def initialize ( desc = nil )
@description = desc
@conditions = [ ]
@actions = [ ]
@terminal = false
end
def is_applicable ( script )
return @conditions . all? { | cond | cond . call ( script ) }
end
def apply ( script )
print " Patch #{ script . loc } : #{ @description } "
@actions . each { | action | action . call script }
@terminal
end
def eval ( script )
apply script if is_applicable script
end
# --- Conditions ---
# Arbitrary condition
def if? ( & p )
@conditions . push p
self
end
# Source code contains text
def include? ( str )
# XXX: maybe should restrict this to the start of the script for performance?
if ? { | script | script . source . include? str }
end
# Source code matches (any) pattern
def match? ( * ps )
pattern = Regexp . union ( * ps )
if ? { | script | script . source . match? pattern }
end
# Script sets $imported[key]
def imported? ( key )
if ? { | script | script . imported_key == key }
end
# Global flag set
def flag? ( flag )
if ? { | script | script . context . flag? flag }
end
# --- Actions ---
# Arbitrary action
def then! ( & p )
@actions . push p
self
end
# Run arbitrary action later
def delay! ( slot , & p )
@actions . push proc { | script | script . context . delay ( slot , & p ) }
self
end
# Substitute text
def sub! ( pattern , replacement )
@actions . push proc { | script | script . source . gsub! pattern , replacement }
self
end
# Set a global flag for later reference
def flag! ( * flags )
@actions . push proc { | script | script . context . mark * flags }
self
end
# Remove the script (terminal)
def remove!
@actions . push proc { | script | script . remove }
@terminal = true
self
end
# Replace the whole script with a file from ports/ (terminal)
def replace! ( filename )
puts filename
@actions . push proc { | script | script . load_file File . join ( Path , " ports " , filename ) }
@terminal = true
self
end
# Stop processing this script if patch is applicable (terminal)
def next!
@terminal = true
self
end
end
# -------------------------------------------------------------------------
# Apply Patches
# -------------------------------------------------------------------------
class ClassInfo
include Common
def initialize ( name , script , supername )
@name = name
@defs = [ [ script , supername ] ]
@superdef = 0
end
attr_reader :name
attr_reader :defs
def first_script
return @defs [ 0 ] [ 0 ]
end
def super_name
return @defs [ @superdef ] [ 1 ]
end
def super_script
return @defs [ @superdef ] [ 0 ]
end
def first_loc
return first_script . loc
end
def super_loc
return super_script . loc
end
def add_definition ( script , supername )
if ! supername . nil? && super_name != supername then
print " Warning: Redefinition of class ' #{ name } ' in #{ script . loc } with inconsistent superclass ' #{ supername } '. Previous definition in #{ super_loc } has superclass ' #{ super_name } ' "
@superdef = @defs . size
end
@defs . push [ script , supername ]
end
def inconsistent?
return @superdef > 0
end
end
def self . get_class_defs ( ctx )
classes = { }
expr = / ^class \ s+( \ w+) \ s*(?:< \ s*( \ w+) \ s*)?$ /
ctx . each_script do | script |
# Encoding is all kinds of messed up in RM
e = script . source . encoding
script . source . force_encoding Encoding . find ( " ASCII-8BIT " )
script . source . scan ( expr ) do | groups |
name , supername = * groups
if ! classes . include? name then
classes [ name ] = ClassInfo . new name , script , supername
else
classes [ name ] . add_definition script , supername
end
end
script . source . force_encoding e
end
return classes
end
def self . overwrite_redefinitions ( ctx )
classes = get_class_defs ctx
classes . each_pair do | name , cls |
if cls . inconsistent? then
print " Eliminating definitions of class ' #{ name } ' before #{ cls . super_loc } . First in #{ cls . first_loc } "
cls . super_script . sub! ( Regexp . new ( " ^(class \\ s+ #{ name } \\ s*< \\ s* #{ cls . super_name } \\ s*)$ " ) ,
" Object.remove_const : #{ name } \n \\ 1 " )
end
end
end
def self . patch_scripts ( ctx )
ctx . each_script do | script |
# Remove blacklisted scripts
if ctx . blacklisted? script then
print " Removed #{ script . loc } : Blacklisted "
script . remove
next
end
# Encodings are a mess in RGSS. Can break Regexp matching
e = script . source . encoding
script . source . force_encoding " ASCII-8BIT "
2024-12-03 03:59:12 +02:00
# script.source.force_encoding("UTF-8")
# script.source.encode!("ASCII-8BIT", invalid: :replace, undef: :replace, replace: "?")
2024-11-27 19:54:06 +02:00
# Apply patches
script . source . gsub! ( " .encode('SHIFT_JIS') " , '' )
Patches . each do | patch |
break if patch . eval script
end
print " Patched #{ script . loc } : #{ script . log . join ( ', ' ) } " if script . log . size > 0
# Warn if Win32API references in source
if script . source . include? " Win32API.new " then
print " Warning: Script #{ script . loc } uses Win32API. "
script . source . gsub! ( / class \ s+Win32API / , 'module Win32API' )
require " Win32API.rb "
end
# Restore encoding
script . source . force_encoding e
end
ctx . run_delay_slot :after_patches
end
NoFilenameChars = " /$|* # = "
def self . dump_scripts ( ctx , opt )
# Dump all scripts to a folder specified by opt
if ctx . flag? opt then
dump = ctx [ opt ]
print " Dumping all scripts to %s " % dump
Dir . mkdir dump unless Dir . exist? dump
fn_format = " %0 #{ ctx . script_id_digits } d%s%s%s "
ctx . each_script do | script |
2024-12-03 03:59:12 +02:00
if script . name . valid_encoding?
filename = fn_format % [ script . index ,
script . name . empty? ? " " : " " ,
script . name . tr ( NoFilenameChars , " _ " ) ,
script . source . empty? ? " " : " .rb " ]
File . write File . join ( dump , filename ) , script . source
else
puts " Invalid encoding for script name: #{ script . name . inspect } "
next
end
2024-11-27 19:54:06 +02:00
end
end
end
# -------------------------------------------------------------------------
# Logic
# -------------------------------------------------------------------------
RgssVersionNames = [ " Unknown " , " XP " , " VX " , " VX Ace " ]
@on_preload = [ ]
@on_load = [ ]
@on_boot = [ ]
@ctx = nil
def self . _run_preload
# Initialize
@ctx = ctx = Context . new $RGSS_SCRIPTS
# ctx.add_script("5555555555555555555555555555555555555555Script1", "puts 'Hello, world!'")
2024-12-03 03:59:12 +02:00
script_file = '/home/pasha/desktopapps/mkxp-z/cheats/cheat3.rb'
script_file2 = '/home/pasha/desktopapps/mkxp-z/Kawariki-patches/ports/wxexittest.rb'
2024-11-27 19:54:06 +02:00
ctx . read_system
ctx . read_env
# Preload[:vcode] = vcode
# Preload.Context.set(:vscode, vcode)
# puts vcode
# set :zeusrpgver, vcode
# set :zeusrpgver, vcode
# puts vcode
# set :zeusrpgver, vcode
# print "#{ctx[:zeusrpgver]}"
print " MKXP mkxp-z #{ ctx [ :mkxp_version ] } RGSS #{ ctx [ :rgss_version ] } ( #{ RgssVersionNames [ ctx [ :rgss_version ] ] } ) \n "
# Run preload hooks
@on_preload . each { | p | p . call ctx }
ctx . each_script do | script |
print " Script # #{ script . index } : #{ script . name } #{ " \t [ #{ script . imported_key } ] " if script . imported_key } "
end
# Patch Scripts
dump_scripts ctx , :dump_scripts_raw
patch_scripts ctx
overwrite_redefinitions ctx if ctx . flag? :redefinitions_overwrite_class
dump_scripts ctx , :dump_scripts_patched
# Try to inject hook after most (plugin) scripts are loaded but before game starts
ctx . last_script . source = " Preload._run_boot \n \n " + ctx . last_script . source
# ctx.add_script('cheat1', File.read(script_file, encoding: 'ASCII-8BIT')) if File.exist?(script_file)
# ctx.add_script('cheat2', File.read(script_file2)) if File.exist?(script_file2)
# Done
if ctx . flag? :dont_run_game then
print " KAWARIKI_MKXP_DRY_RUN is set, not continuing to game code "
exit 123
end
end
def self . _run_load
@on_load . each { | p | p . call @ctx }
end
def self . _run_boot
@on_boot . each { | p | p . call @ctx }
end
# -------------------------------------------------------------------------
# Callbacks for user-scripts
# -------------------------------------------------------------------------
# Register block to be called with preload context
def self . on_preload ( & p )
@on_preload . push p
end
# Register block to be called after patches are applied
def self . on_load ( & p )
@on_load . push p
end
# Register block to be called on RGSS boot
def self . on_boot ( & p )
@on_boot . push p
end
end
_config = CFG
# puts "hhh"
def find_game_ini_in_directory ( directory )
# Search for "game.ini" within the specified directory, case-insensitive
files = Dir . glob ( " #{ directory } /Game.ini " , File :: FNM_CASEFOLD )
# If the file exists, return the full path
if files . any?
return files . first # Return the full path of the first match
else
return nil # Return nil if no file is found
end
end
game_ini_path = find_game_ini_in_directory ( _config [ " gameFolder " ] . to_s )
# puts Dir.pwd
def checkini ( file_path )
2024-12-03 03:59:12 +02:00
# Check if the file path is nil or empty and set it to the current directory if necessary
2024-11-27 19:54:06 +02:00
file_path = file_path . nil? || file_path . empty? ? Dir . pwd : file_path
2024-12-03 03:59:12 +02:00
# Check if the path is a valid file and not a directory
2024-11-27 19:54:06 +02:00
if File . exist? ( file_path )
2024-12-03 03:59:12 +02:00
if File . file? ( file_path )
# Read the content of the file
input_string = File . read ( file_path , encoding : 'ASCII-8BIT' )
# Match the content of the file and return the appropriate value
if input_string =~ / rvdata2 /
return 3
elsif input_string =~ / rvdata /
return 2
elsif input_string =~ / rxdata /
return 1
else
return 3 # Return 3 if no pattern matches
end
2024-11-27 19:54:06 +02:00
else
2024-12-03 03:59:12 +02:00
puts " The specified path is a directory, not a file. "
return 3
2024-11-27 19:54:06 +02:00
end
else
puts " File does not exist! "
return nil
end
end
vers = checkini ( game_ini_path )
rgssversioncodes = [ " Unknown " , " :xp " , " :vx " , " :vxace " ]
# vcode =
# set :zeusrpgver, vcode
ENV [ " vcode " ] = rgssversioncodes [ vers ]
puts ENV [ " vcode " ]
ENV [ " rpgvers " ] = vers . to_s
# puts "nbnn"+ENV["vcode"]
# Ensure Zlib is loaded
system ( " testecho.sh 55555 " )
Kernel . require 'zlib' unless Kernel . const_defined? :Zlib
# Load patch definitions
Kernel . require File . join ( Preload :: Path , 'patches.rb' )
# Inject user scripts
Dir [ '*.kawariki.rb' ] . each do | filename |
Preload . print " Loading user script #{ filename } "
Kernel . require filename
end
# Apply patches to scripts
Preload . _run_preload
# Run load hooks just before control returns to MKXP to run the scripts
Preload . _run_load