Added extension to Plugin Manager that supports scripts in a Plugin folder

This commit is contained in:
Maruno17
2021-04-25 19:09:56 +01:00
parent 484813c592
commit 0b757d3863
4 changed files with 415 additions and 60 deletions

View File

@@ -1,3 +1,63 @@
#===============================================================================
# Reads files of certain format from a directory
#===============================================================================
class Dir
#-----------------------------------------------------------------------------
# Reads all files in a directory
#-----------------------------------------------------------------------------
def self.get(dir, filters = "*", full = true)
files = []
filters = [filters] if !filters.is_a?(Array)
self.chdir(dir) do
for filter in filters
self.glob(filter){ |f| files.push(full ? (dir + "/" + f) : f) }
end
end
return files.sort
end
#-----------------------------------------------------------------------------
# Generates entire file/folder tree from a certain directory
#-----------------------------------------------------------------------------
def self.all(dir, filters = "*", full = true)
# sets variables for starting
files = []
for file in self.get(dir, filters, full)
# engages in recursion to read the entire file tree
files += self.safe?(file) ? self.get(file, filters, full) : [file]
end
# returns all found files
return files
end
#-----------------------------------------------------------------------------
# Checks for existing directory, gets around accents
#-----------------------------------------------------------------------------
def self.safe?(dir)
ret = false
self.chdir(dir) { ret = true } rescue nil
return ret
end
#-----------------------------------------------------------------------------
end
#===============================================================================
# extensions for file class
#===============================================================================
class File
#-----------------------------------------------------------------------------
# Checks for existing file, gets around accents
#-----------------------------------------------------------------------------
def self.safe?(file)
ret = false
self.open(file, 'rb') { ret = true } rescue nil
return ret
end
#-----------------------------------------------------------------------------
end
#===============================================================================
# Checking for files and directories
#===============================================================================

View File

@@ -76,6 +76,13 @@ class Array
def ^(other) # xor of two arrays
return (self|other) - (self&other)
end
def swap(val1, val2)
index1 = self.index(val1)
index2 = self.index(val2)
self[index1] = val2
self[index2] = val1
end
end
#===============================================================================

View File

@@ -1,9 +1,14 @@
#==============================================================================#
# Plugin Manager #
# by Marin #
# 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: #
# #
@@ -21,14 +26,23 @@
# the error message if the PluginManager detects that this plugin needs to be #
# updated. #
# #
# A plugin's version is typically in the format X.Y.Z, but the number of #
# digits does not matter. You can also use Xa, Xb, Xc, Ya, etc. #
# A plugin's version should be in the format X.Y.Z, but the number of digits #
# you use does not matter. You can also use Xa, Xb, Xc, Ya, etc. #
# What matters is that you use it consistently, so that it can be compared. #
# #
# IF there are multiple people to credit, their names should be in an array. #
# If there is only one credit, it does not need an array: #
# #
# :credits => "Marin" #
# :credits => ["Marin", "Maruno"], #
# #
# #
# Now let's say we create a new plugin titled "Simple Extension", which #
# requires our previously created "Basic Plugin" to work. #
# #
# Dependency: #
# #
# A plugin can require another plugin to be installed in order to work. For #
# example, the "Simple Extension" plugin depends on the above "Basic Plugin" #
# like so: #
# #
# PluginManager.register({ #
# :name => "Simple Extension", #
@@ -38,48 +52,45 @@
# :dependencies => ["Basic Plugin"] #
# }) #
# #
# This plugin has two credits as an array, instead of one string. Furthermore, #
# this code will ensure that "Basic Plugin" is installed, ignoring its #
# version. If you have only one dependency, you can omit the array brackets #
# like so: #
# If there are multiple dependencies, they should be listed in an array. If #
# there is only one dependency, it does not need an array: #
# #
# :dependencies => "Basic Plugin" #
# #
# #
# #
# To require a minimum version of a dependency plugin, you should turn the #
# dependency's name into an array which contains the name and the version #
# (both as strings). For example, to require "Basic Plugin" version 1.2 or #
# higher, you would write: #
# #
# PluginManager.register({ #
# :name => "Simple Extension", #
# :version => "1.0", #
# :link => "https://reliccastle.com/link-to-the-plugin/", #
# :credits => "Marin", #
# :dependencies => [ #
# ["Basic Plugin", "1.2"] #
# ] #
# }) #
# #
# #
# :dependencies => [ #
# ["Basic Plugin", "1.2"] #
# ] #
# #
# To require a specific version (no higher and no lower) of a dependency #
# plugin, you should add the :exact flag as the first thing in the array for #
# that dependency: #
# #
# PluginManager.register({ #
# :name => "Simple Extension", #
# :version => "1.0", #
# :link => "https://reliccastle.com/link-to-the-plugin/", #
# :credits => "Marin", #
# :dependencies => [ #
# [:exact, "Basic Plugin", "1.2"] #
# ] #
# }) #
# :dependencies => [ #
# [:exact, "Basic Plugin", "1.2"] #
# ] #
# #
# If your plugin can work without another plugin, but it is incompatible with #
# an old version of that other plugin, you should list it as an optional #
# dependency. If that other plugin is present in a game, then this optional #
# dependency will check whether it meets the minimum version required for your #
# plugin. Write it in the same way as any other dependency as described above, #
# but use the :optional flag instead. #
# #
# :dependencies => [ #
# [:optional, "QoL Improvements", "1.1"] #
# ] #
# #
# The :optional_exact flag is a combination of :optional and :exact. #
# #
# #
# #
# Incompatibility: #
# #
# If your plugin is known to be incompatible with another plugin, you should #
# list that other plugin as such. Only one of the two plugins needs to list #
# that it is incompatible with the other. #
@@ -94,39 +105,70 @@
# ] #
# }) #
# #
#------------------------------------------------------------------------------#
# Plugin folder: #
# #
# The Plugin folder is treated like the PBS folder, but for script files for #
# plugins. Each plugin has its own folder within the Plugin folder. Each #
# plugin must have a meta.txt file in its folder, which contains information #
# about that plugin. Folders without this meta.txt file are ignored. #
# #
# If your plugin can work without another plugin, but is known to be #
# incompatible with an old version of that other plugin, you should list it as #
# an optional dependency. If that other plugin is present in a game, then this #
# optional dependency will ensure it meets the minimum version required for #
# your plugin. Write it in the same way as any other dependency as described #
# above, but use the :optional flag instead. #
# You do not need to list a plugin as an optional dependency at all if all #
# versions of that other plugin are compatible with your plugin. #
# Scripts must be in .rb files. You should not put any other files into a #
# plugin's folder except for script files and meta.txt. #
# #
# PluginManager.register({ #
# :name => "Other Plugin", #
# :version => "1.0", #
# :link => "https://reliccastle.com/link-to-the-plugin/", #
# :credits => "Marin", #
# :dependencies => [ #
# [:optional, "QoL Improvements", "1.1"] #
# ] #
# }) #
# When the game is compiled, scripts in these folders are read and converted #
# into a usable format, and saved in the file Data/PluginScripts.rxdata. #
# Script files are loaded in order of their name and subfolder, so it is wise #
# to name script files "001_first script.rb", "002_second script.rb", etc. to #
# ensure they are loaded in the correct order. #
# #
# When the game is compressed for distribution, the Plugin folder and all its #
# contents should be deleted (like the PBS folder), because its contents will #
# be unused (they will have been compiled into the PluginScripts.rxdata file). #
# #
# The contents of meta.txt are as follows: #
# #
# Name = Simple Extension #
# Version = 1.0 #
# Requires = Basic Plugin #
# Requires = Useful Utilities,1.1 #
# Conflicts = Complex Extension #
# Conflicts = Extended Windows #
# Link = https://reliccastle.com/link-to-the-plugin/ #
# Credits = Luka S.J.,Maruno,Marin #
# #
# These lines are related to what is described above. You can have multiple #
# "Requires" and "Conflicts" lines, each listing a single other plugin that is #
# either a dependency or a conflict respectively. #
# #
# Examples of the "Requires" line: #
# #
# Requires = Basic Plugin #
# Requires = Basic Plugin,1.1 #
# Requires = Basic Plugin,1.1,exact #
# Requires = Basic Plugin,1.1,optional #
# Exact = Basic Plugin,1.1 #
# Optional = Basic Plugin,1.1 #
# #
# The "Exact" and "Optional" lines are equivalent to the "Requires" lines #
# that contain those keywords. #
# #
# There is also a "Scripts" line, which lists one or more script files that #
# should be loaded first. You can have multiple "Scripts" lines. However, you #
# can achieve the same effect by simply naming your script files in #
# alphanumeric order to make them load in a particular order, so the "Scripts" #
# line should not be necessary. #
# #
# The :optional_exact flag is a combination of :optional and :exact. #
#------------------------------------------------------------------------------#
# Please give credit when using this. #
#==============================================================================#
module PluginManager
# Win32API MessageBox function for custom errors.
# MBOX = Win32API.new('user32', 'MessageBox', ['I','P','P','I'], 'I')
# Holds all registered plugin data.
@@Plugins = {}
#-----------------------------------------------------------------------------
# Registers a plugin and tests its dependencies and incompatibilities.
#-----------------------------------------------------------------------------
def self.register(options)
name = nil
version = nil
@@ -302,12 +344,12 @@ module PluginManager
: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
#MBOX.call(Win32API.pbFindRgssWindow, msg, "Plugin Error", 0x10)
p "Plugin Error:\n#{msg}"
Thread.exit
end
@@ -316,10 +358,11 @@ module PluginManager
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?
@@ -328,36 +371,41 @@ module PluginManager
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.split("")
d1.insert(0, "0") if d1[0] == "." # Turn ".123" into "0.123"
@@ -378,4 +426,243 @@ module PluginManager
end
return 0
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 '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
for fl in Dir.all(dir)
next if !File.extname(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")
# 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]
# go through all dependencies
for dname in meta[name][:dependencies]
# 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 dependecy issue
self.error("Plugin '#{og}' has looping dependencies which cannot be resolved automatically.") if !og.nil? && og == dname
self.validateDependencies(dname, meta, name)
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
for o in order
next if !plugins[o] || !plugins[o][:dependencies]
# go through all dependencies
for dname in plugins[o][:dependencies]
# 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
# 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.
for dir in self.listAll
# 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")
return true if Input.press?(Input::CTRL)
ret = false
# analyze whether or not to push recompile
mtime = File.mtime("Data/PluginScripts.rxdata")
for o in order
# go through all the registered plugin scripts
scr = plugins[o][:scripts]
dir = plugins[o][:dir]
for sc in scr
ret = true if File.mtime("#{dir}/#{sc}") > mtime
end
ret = true if File.mtime("#{dir}/meta.txt") > mtime
end
# return result
return ret
end
#-----------------------------------------------------------------------------
# Check if plugins need compiling
#-----------------------------------------------------------------------------
def self.compilePlugins(order, plugins)
scripts = []
# go through the entire order one by one
for o in order
# 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
for file in plugins[o][:scripts]
File.open("#{plugins[o][:dir]}/#{file}", 'rb') { |f| dat[2].push(Zlib::Deflate.deflate(f.read)) }
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
end
#-----------------------------------------------------------------------------
# Check if plugins need compiling
#-----------------------------------------------------------------------------
def self.runPlugins
# get the order of plugins to interpret
order, plugins = self.getPluginOrder
# compile if necessary
self.compilePlugins(order, plugins) if self.needCompiling?(order, plugins)
# run the plugins from compiled archive
# load plugins
scripts = load_data("Data/PluginScripts.rxdata")
for plugin in scripts
# get the required data
name, meta, script = plugin
# register plugin
self.register(meta)
# go through each script and interpret
for scr in script
# turn code into plaintext
code = Zlib::Inflate.inflate(scr)
# get rid of tabs
code.gsub!("\t", " ")
# construct filename
fname = "[Plugin] " + name
# try to run the code
begin
eval(code, TOPLEVEL_BINDING, fname)
rescue Exception # format error message to display
msg = "[Pokémon Essentials v#{Essentials::VERSION}] #{Essentials::ERROR_TEXT}\r\n\r\n"
msg += "#{$raise_msg}\r\n-------------------------------\r\n" if $raise_msg
msg += "Error in Plugin [#{name}]:\r\n"
msg += "#{$!.class} occurred.\r\n"
for line in $!.message.split("\r\n")
next if !line || line == ""
n = line[/\d+/]
err = line.split(":")[-1].strip
lms = line.split(":")[0].strip
err.gsub!(n, "") if n
err = err.capitalize if err.is_a?(String) && !err.empty?
linum = n ? "Line #{n}: " : ""
msg += "#{linum}#{err}: #{lms}\r\n"
end
msg += "\r\nFull trace can be found below:\r\n"
for bck in $!.backtrace
msg += "#{bck}\r\n"
end
msg += "\r\nEnd of Error."
$raise_msg = nil
raise msg
end
end
end
end
end