rpgmaker-linux/mkxp-z/Kawariki-patches/preload.rb
2024-11-24 13:42:25 +02:00

676 lines
20 KiB
Ruby

# 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)
@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
# Force encode to ASCII-8BIT (binary ASCII), replacing non-ASCII characters with '?'
if source.encoding.name != "ASCII-8BIT"
begin
# puts "Attempting to encode source..."
# Try encoding, but ensure that source remains unchanged if encoding fails
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?
# puts "Warning: 'source' became nil or empty after encoding! Reverting to original source."
source = original_source
return
else
# Otherwise, use the encoded result
source = encoded_source
end
# puts "Encoding successful, source encoding is now: #{source.encoding.name}"
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
puts "Encoding failed: #{e.message}"
return
rescue StandardError => e
puts "Unexpected error during encoding: #{e.message}"
return
end
end
# 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"
# 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|
filename = fn_format % [script.index,
script.name.empty? ? "" : " ",
script.name.tr(NoFilenameChars, "_"),
script.source.empty? ? "" : ".rb"]
File.write File.join(dump, filename), script.source
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.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
# 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)
# Check if the file exists
file_path = file_path.nil? || file_path.empty? ? Dir.pwd : file_path
if File.exist?(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
# Match the pattern in the input string and return corresponding values
if input_string =~ /rvdata2/
return 3
elsif input_string =~ /rvdata/
return 2
elsif input_string =~ /rxdata/
return 1
else
return 3 # Return nil if none of the patterns match
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
puts "bbbb------------"
puts $data_system
# Run load hooks just before control returns to MKXP to run the scripts
Preload._run_load