mirror of
https://github.com/infinitefusion/infinitefusion-e18.git
synced 2025-12-06 06:01:46 +00:00
686 lines
32 KiB
Ruby
686 lines
32 KiB
Ruby
#==============================================================================#
|
|
# Plugin Manager #
|
|
# by Marin #
|
|
# Support for external plugin scripts by Luka S.J. #
|
|
# Tweaked by Maruno #
|
|
#------------------------------------------------------------------------------#
|
|
# Provides a simple interface that allows plugins to require dependencies #
|
|
# at specific versions, and to specify incompatibilities between plugins. #
|
|
# #
|
|
# Supports external scripts that are in .rb files in folders within the #
|
|
# Plugins folder. #
|
|
#------------------------------------------------------------------------------#
|
|
# Usage: #
|
|
# #
|
|
# Each plugin should have its own folder in the "Plugins" folder found in the #
|
|
# main directory. The "Plugins" folder is similar in concept to the "PBS" #
|
|
# folder, in that its contents are compiled and recorded as existing. The #
|
|
# plugin's script file(s) are placed in its folder - they must be .rb files. #
|
|
# #
|
|
# A plugin's folder must also contain a "meta.txt" file. This file is what #
|
|
# makes Essentials recognise that the plugin exists, and contains important #
|
|
# information about the plugin; if this file does not exist, the folder's #
|
|
# contents are ignored. Each line in this file is a property. #
|
|
# #
|
|
# Required lines: #
|
|
# #
|
|
# Name = Simple Extension The plugin's name #
|
|
# Version = 1.0 The plugin's version #
|
|
# Essentials = 19.1,20 Compatible version(s) of Essentials #
|
|
# Link = https://reliccastle.com/link-to-the-plugin/ #
|
|
# Credits = Luka S.J.,Maruno,Marin One or more names #
|
|
# #
|
|
# A plugin's version should be in the format X or X.Y or X.Y.Z, where X/Y/Z #
|
|
# are numbers. You can also use Xa, Xb, Xc, Ya, etc. What matters is that you #
|
|
# use version numbers consistently for your plugin. A later version will be #
|
|
# alphanumerically higher than an older version. #
|
|
# #
|
|
# Plugins can interact with each other in several ways, such as requiring #
|
|
# another one to exist or by clashing with each other. These interactions are #
|
|
# known as dependencies and conflicts. The lines below are all optional, and #
|
|
# go in "meta.txt" to define how your plugin works (or doesn't work) with #
|
|
# others. You can have multiples of each of these lines. #
|
|
# #
|
|
# Requires = Basic Plugin Must have this plugin (any version) #
|
|
# Requires = Useful Utils,1.1 Must have this plugin/min. version #
|
|
# Exact = Scene Tweaks,2 Must have this plugin/version #
|
|
# Optional = Extended Windows,1.2 If this plugin exists, load it first #
|
|
# Conflicts = Complex Extension Incompatible plugin #
|
|
# #
|
|
# A plugin that depends on another one ("Requires"/"Exact"/"Optional") will #
|
|
# make that other plugin be loaded first. The "Optional" line is for a plugin #
|
|
# which isn't necessary, but if it does exist in the same project, it must be #
|
|
# at the given version or higher. #
|
|
# #
|
|
# When plugins are compiled, their scripts are stored in the file #
|
|
# "PluginScripts.rxdata" in the "Data" folder. Dependencies defined above will #
|
|
# ensure that they are loaded in a suitable order. Scripts within a plugin are #
|
|
# loaded alphanumerically, going through subfolders depth-first. #
|
|
# #
|
|
# The "Plugins" folder should be deleted when the game is released. Scripts in #
|
|
# there are compiled, but any other files used by a plugin (graphics/audio) #
|
|
# should go into other folders and not the plugin's folder. #
|
|
# #
|
|
#------------------------------------------------------------------------------#
|
|
# The code behind plugins: #
|
|
# #
|
|
# When a plugin's "meta.txt" file is read, its contents are registered in the #
|
|
# PluginManager. A simple example of registering a plugin is as follows: #
|
|
# #
|
|
# PluginManager.register({ #
|
|
# :name => "Basic Plugin", #
|
|
# :version => "1.0", #
|
|
# :essentials => "20", #
|
|
# :link => "https://reliccastle.com/link-to-the-plugin/", #
|
|
# :credits => ["Marin"] #
|
|
# }) #
|
|
# #
|
|
# The :link value is optional, but recommended. This will be shown in the #
|
|
# message if the PluginManager detects that this plugin needs to be updated. #
|
|
# #
|
|
# Here is the same example but also with dependencies and conflicts: #
|
|
# #
|
|
# PluginManager.register({ #
|
|
# :name => "Basic Plugin", #
|
|
# :version => "1.0", #
|
|
# :essentials => "20", #
|
|
# :link => "https://reliccastle.com/link-to-the-plugin/", #
|
|
# :credits => ["Marin"], #
|
|
# :dependencies => ["Basic Plugin", #
|
|
# ["Useful Utils", "1.1"], #
|
|
# [:exact, "Scene Tweaks", "2"], #
|
|
# [:optional, "Extended Windows", "1.2"], #
|
|
# ], #
|
|
# :incompatibilities => ["Simple Extension"] #
|
|
# }) #
|
|
# #
|
|
# The example dependencies/conflict are the same as the examples shown above #
|
|
# for lines in "meta.txt". :optional_exact is a combination of :exact and #
|
|
# :optional, and there is no way to make use of its combined functionality via #
|
|
# "meta.txt". #
|
|
# #
|
|
#------------------------------------------------------------------------------#
|
|
# Please give credit when using this. #
|
|
#==============================================================================#
|
|
|
|
module PluginManager
|
|
# Holds all registered plugin data.
|
|
@@Plugins = {}
|
|
#-----------------------------------------------------------------------------
|
|
# Registers a plugin and tests its dependencies and incompatibilities.
|
|
#-----------------------------------------------------------------------------
|
|
def self.register(options)
|
|
name = nil
|
|
version = nil
|
|
essentials = nil
|
|
link = nil
|
|
dependencies = nil
|
|
incompats = nil
|
|
credits = []
|
|
order = [:name, :version, :essentials, :link, :dependencies, :incompatibilities, :credits]
|
|
# Ensure it first reads the plugin's name, which is used in error reporting,
|
|
# by sorting the keys
|
|
keys = options.keys.sort do |a, b|
|
|
idx_a = order.index(a) || order.size
|
|
idx_b = order.index(b) || order.size
|
|
next idx_a <=> idx_b
|
|
end
|
|
keys.each do |key|
|
|
value = options[key]
|
|
case key
|
|
when :name # Plugin name
|
|
if nil_or_empty?(value)
|
|
self.error("Plugin name must be a non-empty string.")
|
|
end
|
|
if !@@Plugins[value].nil?
|
|
self.error("A plugin called '#{value}' already exists.")
|
|
end
|
|
name = value
|
|
when :version # Plugin version
|
|
if nil_or_empty?(value)
|
|
self.error("Plugin version must be a string.")
|
|
end
|
|
version = value
|
|
when :essentials
|
|
essentials = value
|
|
when :link # Plugin website
|
|
if nil_or_empty?(value)
|
|
self.error("Plugin link must be a non-empty string.")
|
|
end
|
|
link = value
|
|
when :dependencies # Plugin dependencies
|
|
dependencies = value
|
|
dependencies = [dependencies] if !dependencies.is_a?(Array) || !dependencies[0].is_a?(Array)
|
|
value.each do |dep|
|
|
case dep
|
|
when String # "plugin name"
|
|
if !self.installed?(dep)
|
|
self.error("Plugin '#{name}' requires plugin '#{dep}' to be installed above it.")
|
|
end
|
|
when Array
|
|
case dep.size
|
|
when 1 # ["plugin name"]
|
|
if dep[0].is_a?(String)
|
|
dep_name = dep[0]
|
|
if !self.installed?(dep_name)
|
|
self.error("Plugin '#{name}' requires plugin '#{dep_name}' to be installed above it.")
|
|
end
|
|
else
|
|
self.error("Expected the plugin name as a string, but got #{dep[0].inspect}.")
|
|
end
|
|
when 2 # ["plugin name", "version"]
|
|
if dep[0].is_a?(Symbol)
|
|
self.error("A plugin version comparator symbol was given but no version was given.")
|
|
elsif dep[0].is_a?(String) && dep[1].is_a?(String)
|
|
dep_name = dep[0]
|
|
dep_version = dep[1]
|
|
next if self.installed?(dep_name, dep_version)
|
|
if self.installed?(dep_name) # Have plugin but lower version
|
|
msg = "Plugin '#{name}' requires plugin '#{dep_name}' version #{dep_version} or higher, " +
|
|
"but the installed version is #{self.version(dep_name)}."
|
|
dep_link = self.link(dep_name)
|
|
if dep_link
|
|
msg += "\r\nCheck #{dep_link} for an update to plugin '#{dep_name}'."
|
|
end
|
|
self.error(msg)
|
|
else # Don't have plugin
|
|
self.error("Plugin '#{name}' requires plugin '#{dep_name}' version #{dep_version} " +
|
|
"or higher to be installed above it.")
|
|
end
|
|
end
|
|
when 3 # [:optional/:exact/:optional_exact, "plugin name", "version"]
|
|
if !dep[0].is_a?(Symbol)
|
|
self.error("Expected first dependency argument to be a symbol, but got #{dep[0].inspect}.")
|
|
end
|
|
if !dep[1].is_a?(String)
|
|
self.error("Expected second dependency argument to be a plugin name, but got #{dep[1].inspect}.")
|
|
end
|
|
if !dep[2].is_a?(String)
|
|
self.error("Expected third dependency argument to be the plugin version, but got #{dep[2].inspect}.")
|
|
end
|
|
dep_arg = dep[0]
|
|
dep_name = dep[1]
|
|
dep_version = dep[2]
|
|
optional = false
|
|
exact = false
|
|
case dep_arg
|
|
when :optional
|
|
optional = true
|
|
when :exact
|
|
exact = true
|
|
when :optional_exact
|
|
optional = true
|
|
exact = true
|
|
else
|
|
self.error("Expected first dependency argument to be one of " +
|
|
":optional, :exact or :optional_exact, but got #{dep_arg.inspect}.")
|
|
end
|
|
if optional
|
|
if self.installed?(dep_name) && # Have plugin but lower version
|
|
!self.installed?(dep_name, dep_version, exact)
|
|
msg = "Plugin '#{name}' requires plugin '#{dep_name}', if installed, to be version #{dep_version}"
|
|
msg << " or higher" if !exact
|
|
msg << ", but the installed version was #{self.version(dep_name)}."
|
|
dep_link = self.link(dep_name)
|
|
if dep_link
|
|
msg << "\r\nCheck #{dep_link} for an update to plugin '#{dep_name}'."
|
|
end
|
|
self.error(msg)
|
|
end
|
|
elsif !self.installed?(dep_name, dep_version, exact)
|
|
if self.installed?(dep_name) # Have plugin but lower version
|
|
msg = "Plugin '#{name}' requires plugin '#{dep_name}' to be version #{dep_version}"
|
|
msg << " or later" if !exact
|
|
msg << ", but the installed version was #{self.version(dep_name)}."
|
|
dep_link = self.link(dep_name)
|
|
if dep_link
|
|
msg << "\r\nCheck #{dep_link} for an update to plugin '#{dep_name}'."
|
|
end
|
|
else # Don't have plugin
|
|
msg = "Plugin '#{name}' requires plugin '#{dep_name}' version #{dep_version} "
|
|
msg << "or later " if !exact
|
|
msg << "to be installed above it."
|
|
end
|
|
self.error(msg)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
when :incompatibilities # Plugin incompatibilities
|
|
incompats = value
|
|
incompats = [incompats] if !incompats.is_a?(Array)
|
|
incompats.each do |incompat|
|
|
if self.installed?(incompat)
|
|
self.error("Plugin '#{name}' is incompatible with '#{incompat}'. " +
|
|
"They cannot both be used at the same time.")
|
|
end
|
|
end
|
|
when :credits # Plugin credits
|
|
value = [value] if value.is_a?(String)
|
|
if value.is_a?(Array)
|
|
value.each do |entry|
|
|
if entry.is_a?(String)
|
|
credits << entry
|
|
else
|
|
self.error("Plugin '#{name}'s credits array contains a non-string value.")
|
|
end
|
|
end
|
|
else
|
|
self.error("Plugin '#{name}'s credits field must contain a string, or a string array.")
|
|
end
|
|
else
|
|
self.error("Invalid plugin registry key '#{key}'.")
|
|
end
|
|
end
|
|
@@Plugins.each_value do |plugin|
|
|
if plugin[:incompatibilities]&.include?(name)
|
|
self.error("Plugin '#{plugin[:name]}' is incompatible with '#{name}'. " +
|
|
"They cannot both be used at the same time.")
|
|
end
|
|
end
|
|
# Add plugin to class variable
|
|
@@Plugins[name] = {
|
|
:name => name,
|
|
:version => version,
|
|
:essentials => essentials,
|
|
:link => link,
|
|
:dependencies => dependencies,
|
|
:incompatibilities => incompats,
|
|
:credits => credits
|
|
}
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Throws a pure error message without stack trace or any other useless info.
|
|
#-----------------------------------------------------------------------------
|
|
def self.error(msg)
|
|
Graphics.update
|
|
t = Thread.new do
|
|
Console.echo_error "Plugin Error:\r\n#{msg}"
|
|
print("Plugin Error:\r\n#{msg}")
|
|
Thread.exit
|
|
end
|
|
while t.status
|
|
Graphics.update
|
|
end
|
|
Kernel.exit! true
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Returns true if the specified plugin is installed.
|
|
# If the version is specified, this version is taken into account.
|
|
# If mustequal is true, the version must be a match with the specified version.
|
|
#-----------------------------------------------------------------------------
|
|
def self.installed?(plugin_name, plugin_version = nil, mustequal = false)
|
|
plugin = @@Plugins[plugin_name]
|
|
return false if plugin.nil?
|
|
return true if plugin_version.nil?
|
|
comparison = compare_versions(plugin[:version], plugin_version)
|
|
return true if !mustequal && comparison >= 0
|
|
return true if mustequal && comparison == 0
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Returns the string names of all installed plugins.
|
|
#-----------------------------------------------------------------------------
|
|
def self.plugins
|
|
return @@Plugins.keys
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Returns the installed version of the specified plugin.
|
|
#-----------------------------------------------------------------------------
|
|
def self.version(plugin_name)
|
|
return if !installed?(plugin_name)
|
|
return @@Plugins[plugin_name][:version]
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Returns the link of the specified plugin.
|
|
#-----------------------------------------------------------------------------
|
|
def self.link(plugin_name)
|
|
return if !installed?(plugin_name)
|
|
return @@Plugins[plugin_name][:link]
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Returns the credits of the specified plugin.
|
|
#-----------------------------------------------------------------------------
|
|
def self.credits(plugin_name)
|
|
return if !installed?(plugin_name)
|
|
return @@Plugins[plugin_name][:credits]
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Compares two versions given in string form. v1 should be the plugin version
|
|
# you actually have, and v2 should be the minimum/desired plugin version.
|
|
# Return values:
|
|
# 1 if v1 is higher than v2
|
|
# 0 if v1 is equal to v2
|
|
# -1 if v1 is lower than v2
|
|
#-----------------------------------------------------------------------------
|
|
def self.compare_versions(v1, v2)
|
|
d1 = v1.chars
|
|
d1.insert(0, "0") if d1[0] == "." # Turn ".123" into "0.123"
|
|
while d1[-1] == "." # Turn "123." into "123"
|
|
d1 = d1[0..-2]
|
|
end
|
|
d2 = v2.chars
|
|
d2.insert(0, "0") if d2[0] == "." # Turn ".123" into "0.123"
|
|
while d2[-1] == "." # Turn "123." into "123"
|
|
d2 = d2[0..-2]
|
|
end
|
|
[d1.size, d2.size].max.times do |i| # Compare each digit in turn
|
|
c1 = d1[i]
|
|
c2 = d2[i]
|
|
if c1
|
|
return 1 if !c2
|
|
return 1 if c1.to_i(16) > c2.to_i(16)
|
|
return -1 if c1.to_i(16) < c2.to_i(16)
|
|
elsif c2
|
|
return -1
|
|
end
|
|
end
|
|
return 0
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# formats the error message
|
|
#-----------------------------------------------------------------------------
|
|
def self.pluginErrorMsg(name, script)
|
|
e = $!
|
|
# begin message formatting
|
|
message = "[Pokémon Essentials version #{Essentials::VERSION}]\r\n"
|
|
message += "#{Essentials::ERROR_TEXT}\r\n" # For third party scripts to add to
|
|
message += "Error in Plugin: [#{name}]\r\n"
|
|
message += "Exception: #{e.class}\r\n"
|
|
message += "Message: "
|
|
message += e.message
|
|
# show last 10 lines of backtrace
|
|
message += "\r\n\r\nBacktrace:\r\n"
|
|
e.backtrace[0, 10].each { |i| message += "#{i}\r\n" }
|
|
# output to log
|
|
errorlog = "errorlog.txt"
|
|
errorlog = RTP.getSaveFileName("errorlog.txt") if (Object.const_defined?(:RTP) rescue false)
|
|
File.open(errorlog, "ab") do |f|
|
|
f.write("\r\n=================\r\n\r\n[#{Time.now}]\r\n")
|
|
f.write(message)
|
|
end
|
|
# format/censor the error log directory
|
|
errorlogline = errorlog.gsub("/", "\\")
|
|
errorlogline.sub!(Dir.pwd + "\\", "")
|
|
errorlogline.sub!(pbGetUserName, "USERNAME")
|
|
errorlogline = "\r\n" + errorlogline if errorlogline.length > 20
|
|
# output message
|
|
print("#{message}\r\nThis exception was logged in #{errorlogline}.\r\nHold Ctrl when closing this message to copy it to the clipboard.")
|
|
# Give a ~500ms coyote time to start holding Control
|
|
t = System.delta
|
|
until (System.delta - t) >= 500_000
|
|
Input.update
|
|
if Input.press?(Input::CTRL)
|
|
Input.clipboard = message
|
|
break
|
|
end
|
|
end
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Used to read the metadata file
|
|
#-----------------------------------------------------------------------------
|
|
def self.readMeta(dir, file)
|
|
filename = "#{dir}/#{file}"
|
|
meta = {}
|
|
# read file
|
|
Compiler.pbCompilerEachPreppedLine(filename) { |line, line_no|
|
|
# split line up into property name and values
|
|
if !line[/^\s*(\w+)\s*=\s*(.*)$/]
|
|
raise _INTL("Bad line syntax (expected syntax like XXX=YYY)\r\n{1}", FileLineData.linereport)
|
|
end
|
|
property = $~[1].upcase
|
|
data = $~[2].split(",")
|
|
data.each_with_index { |value, i| data[i] = value.strip }
|
|
# begin formatting data hash
|
|
case property
|
|
when "ESSENTIALS"
|
|
meta[:essentials] = [] if !meta[:essentials]
|
|
data.each { |ver| meta[:essentials].push(ver) }
|
|
when "REQUIRES"
|
|
meta[:dependencies] = [] if !meta[:dependencies]
|
|
if data.length < 2 # No version given, just push name of plugin dependency
|
|
meta[:dependencies].push(data[0])
|
|
next
|
|
elsif data.length == 2 # Push name and version of plugin dependency
|
|
meta[:dependencies].push([data[0], data[1]])
|
|
else # Push dependency type, name and version of plugin dependency
|
|
meta[:dependencies].push([data[2].downcase.to_sym, data[0], data[1]])
|
|
end
|
|
when "EXACT"
|
|
next if data.length < 2 # Exact dependencies must have a version given; ignore if not
|
|
meta[:dependencies] = [] if !meta[:dependencies]
|
|
meta[:dependencies].push([:exact, data[0], data[1]])
|
|
when "OPTIONAL"
|
|
next if data.length < 2 # Optional dependencies must have a version given; ignore if not
|
|
meta[:dependencies] = [] if !meta[:dependencies]
|
|
meta[:dependencies].push([:optional, data[0], data[1]])
|
|
when "CONFLICTS"
|
|
meta[:incompatibilities] = [] if !meta[:incompatibilities]
|
|
data.each { |value| meta[:incompatibilities].push(value) if value && !value.empty? }
|
|
when "SCRIPTS"
|
|
meta[:scripts] = [] if !meta[:scripts]
|
|
data.each { |scr| meta[:scripts].push(scr) }
|
|
when "CREDITS"
|
|
meta[:credits] = data
|
|
when "LINK", "WEBSITE"
|
|
meta[:link] = data[0]
|
|
else
|
|
meta[property.downcase.to_sym] = data[0]
|
|
end
|
|
}
|
|
# generate a list of all script files to be loaded, in the order they are to
|
|
# be loaded (files listed in the meta file are loaded first)
|
|
meta[:scripts] = [] if !meta[:scripts]
|
|
# get all script files from plugin Dir
|
|
Dir.all(dir).each do |fl|
|
|
next if !fl.include?(".rb")
|
|
meta[:scripts].push(fl.gsub("#{dir}/", ""))
|
|
end
|
|
# ensure no duplicate script files are queued
|
|
meta[:scripts].uniq!
|
|
# return meta hash
|
|
return meta
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Get a list of all the plugin directories to inspect
|
|
#-----------------------------------------------------------------------------
|
|
def self.listAll
|
|
return [] if !$DEBUG || safeExists?("Game.rgssad") || !Dir.safe?("Plugins")
|
|
# get a list of all directories in the `Plugins/` folder
|
|
dirs = []
|
|
Dir.get("Plugins").each { |d| dirs.push(d) if Dir.safe?(d) }
|
|
# return all plugins
|
|
return dirs
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Catch any potential loop with dependencies and raise an error
|
|
#-----------------------------------------------------------------------------
|
|
def self.validateDependencies(name, meta, og = nil)
|
|
# exit if no registered dependency
|
|
return nil if !meta[name] || !meta[name][:dependencies]
|
|
og = [name] if !og
|
|
# go through all dependencies
|
|
meta[name][:dependencies].each do |dname|
|
|
# clean the name to a simple string
|
|
dname = dname[0] if dname.is_a?(Array) && dname.length == 2
|
|
dname = dname[1] if dname.is_a?(Array) && dname.length == 3
|
|
# catch looping dependency issue
|
|
self.error("Plugin '#{og[0]}' has looping dependencies which cannot be resolved automatically.") if !og.nil? && og.include?(dname)
|
|
new_og = og.clone
|
|
new_og.push(dname)
|
|
self.validateDependencies(dname, meta, new_og)
|
|
end
|
|
return name
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Sort load order based on dependencies (this ends up in reverse order)
|
|
#-----------------------------------------------------------------------------
|
|
def self.sortLoadOrder(order, plugins)
|
|
# go through the load order
|
|
order.each do |o|
|
|
next if !plugins[o] || !plugins[o][:dependencies]
|
|
# go through all dependencies
|
|
plugins[o][:dependencies].each do |dname|
|
|
optional = false
|
|
# clean the name to a simple string
|
|
if dname.is_a?(Array)
|
|
optional = [:optional, :optional_exact].include?(dname[0])
|
|
dname = dname[dname.length - 2]
|
|
end
|
|
# catch missing dependency
|
|
if !order.include?(dname)
|
|
next if optional
|
|
self.error("Plugin '#{o}' requires plugin '#{dname}' to work properly.")
|
|
end
|
|
# skip if already sorted
|
|
next if order.index(dname) > order.index(o)
|
|
# catch looping dependency issue
|
|
order.swap(o, dname)
|
|
order = self.sortLoadOrder(order, plugins)
|
|
end
|
|
end
|
|
return order
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Get the order in which to load plugins
|
|
#-----------------------------------------------------------------------------
|
|
def self.getPluginOrder
|
|
plugins = {}
|
|
order = []
|
|
# Find all plugin folders that have a meta.txt and add them to the list of
|
|
# plugins.
|
|
self.listAll.each do |dir|
|
|
# skip if there is no meta file
|
|
next if !safeExists?(dir + "/meta.txt")
|
|
ndx = order.length
|
|
meta = self.readMeta(dir, "meta.txt")
|
|
meta[:dir] = dir
|
|
# raise error if no name defined for plugin
|
|
self.error("No 'Name' metadata defined for plugin located at '#{dir}'.") if !meta[:name]
|
|
# raise error if no script defined for plugin
|
|
self.error("No 'Scripts' metadata defined for plugin located at '#{dir}'.") if !meta[:scripts]
|
|
plugins[meta[:name]] = meta
|
|
# raise error if a plugin with the same name already exists
|
|
self.error("A plugin called '#{meta[:name]}' already exists in the load order.") if order.include?(meta[:name])
|
|
order.insert(ndx, meta[:name])
|
|
end
|
|
# validate all dependencies
|
|
order.each { |o| self.validateDependencies(o, plugins) }
|
|
# sort the load order
|
|
return self.sortLoadOrder(order, plugins).reverse, plugins
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Check if plugins need compiling
|
|
#-----------------------------------------------------------------------------
|
|
def self.needCompiling?(order, plugins)
|
|
# fixed actions
|
|
return false if !$DEBUG || safeExists?("Game.rgssad")
|
|
return true if !safeExists?("Data/PluginScripts.rxdata")
|
|
Input.update
|
|
return true if Input.press?(Input::SHIFT) || Input.press?(Input::CTRL)
|
|
# analyze whether or not to push recompile
|
|
mtime = File.mtime("Data/PluginScripts.rxdata")
|
|
order.each do |o|
|
|
# go through all the registered plugin scripts
|
|
scr = plugins[o][:scripts]
|
|
dir = plugins[o][:dir]
|
|
scr.each do |sc|
|
|
return true if File.mtime("#{dir}/#{sc}") > mtime
|
|
end
|
|
return true if File.mtime("#{dir}/meta.txt") > mtime
|
|
end
|
|
return false
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Check if plugins need compiling
|
|
#-----------------------------------------------------------------------------
|
|
def self.compilePlugins(order, plugins)
|
|
Console.echo_li "Compiling plugin scripts..."
|
|
scripts = []
|
|
# go through the entire order one by one
|
|
order.each do |o|
|
|
# save name, metadata and scripts array
|
|
meta = plugins[o].clone
|
|
meta.delete(:scripts)
|
|
meta.delete(:dir)
|
|
dat = [o, meta, []]
|
|
# iterate through each file to deflate
|
|
plugins[o][:scripts].each do |file|
|
|
File.open("#{plugins[o][:dir]}/#{file}", "rb") do |f|
|
|
dat[2].push([file, Zlib::Deflate.deflate(f.read)])
|
|
end
|
|
end
|
|
# push to the main scripts array
|
|
scripts.push(dat)
|
|
end
|
|
# save to main `PluginScripts.rxdata` file
|
|
File.open("Data/PluginScripts.rxdata", "wb") { |f| Marshal.dump(scripts, f) }
|
|
# collect garbage
|
|
GC.start
|
|
Console.echo_done(true)
|
|
echoln "" if scripts.length == 0
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Check if plugins need compiling
|
|
#-----------------------------------------------------------------------------
|
|
def self.runPlugins
|
|
Console.echo_h1 "Checking plugins"
|
|
# get the order of plugins to interpret
|
|
order, plugins = self.getPluginOrder
|
|
# compile if necessary
|
|
self.compilePlugins(order, plugins) if self.needCompiling?(order, plugins)
|
|
# load plugins
|
|
scripts = load_data("Data/PluginScripts.rxdata")
|
|
echoed_plugins = []
|
|
scripts.each do |plugin|
|
|
# get the required data
|
|
name, meta, script = plugin
|
|
if !meta[:essentials] || !meta[:essentials].include?(Essentials::VERSION)
|
|
Console.echo_warn "Plugin '#{name}' may not be compatible with Essentials v#{Essentials::VERSION}. Trying to load anyway."
|
|
end
|
|
# register plugin
|
|
self.register(meta)
|
|
# go through each script and interpret
|
|
script.each do |scr|
|
|
# turn code into plaintext
|
|
code = Zlib::Inflate.inflate(scr[1]).force_encoding(Encoding::UTF_8)
|
|
# get rid of tabs
|
|
code.gsub!("\t", " ")
|
|
# construct filename
|
|
sname = scr[0].gsub("\\", "/").split("/")[-1]
|
|
fname = "[#{name}] #{sname}"
|
|
# try to run the code
|
|
begin
|
|
eval(code, TOPLEVEL_BINDING, fname)
|
|
Console.echoln_li "Loaded plugin: ==#{name}== (ver. #{meta[:version]})" if !echoed_plugins.include?(name)
|
|
echoed_plugins.push(name)
|
|
rescue Exception # format error message to display
|
|
self.pluginErrorMsg(name, sname)
|
|
Kernel.exit! true
|
|
end
|
|
end
|
|
end
|
|
if scripts.length > 0
|
|
echoln ""
|
|
Console.echo_h2("Successfully loaded #{scripts.length} plugin(s)", text: :green)
|
|
else
|
|
Console.echo_h2("No plugins found", text: :green)
|
|
end
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
# Get plugin dir from name based on meta entries
|
|
#-----------------------------------------------------------------------------
|
|
def self.findDirectory(name)
|
|
# go through the plugins folder
|
|
Dir.get("Plugins").each do |dir|
|
|
next if !Dir.safe?(dir)
|
|
next if !safeExists?(dir + "/meta.txt")
|
|
# read meta
|
|
meta = self.readMeta(dir, "meta.txt")
|
|
return dir if meta[:name] == name
|
|
end
|
|
# return nil if no plugin dir found
|
|
return nil
|
|
end
|
|
#-----------------------------------------------------------------------------
|
|
end
|