mirror of
https://github.com/bakustarver/rpgmakermlinux-cicpoffs.git
synced 2025-04-21 21:52:04 +02:00
676 lines
20 KiB
Ruby
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
|