#==============================================================================#
#                                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:                                     #
#                                                                              #
# A Pokémon Essentials plugin should register itself using the PluginManager.  #
# The simplest way to do so, for a plugin without dependencies, is as follows: #
#                                                                              #
#     PluginManager.register({                                                 #
#       :name    => "Basic Plugin",                                            #
#       :version => "1.0",                                                     #
#       :link    => "https://reliccastle.com/link-to-the-plugin/",             #
#       :credits => "Marin"                                                    #
#     })                                                                       #
#                                                                              #
# The link portion here is optional, but recommended. This will be shown in    #
# the error message if the PluginManager detects that this plugin needs to be  #
# updated.                                                                     #
#                                                                              #
# 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"],                                         #
#                                                                              #
#                                                                              #
#                                                                              #
# 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",                                        #
#       :version => "1.0",                                                     #
#       :link    => "https://reliccastle.com/link-to-the-plugin/",             #
#       :credits => ["Marin", "Maruno"],                                       #
#       :dependencies => ["Basic Plugin"]                                      #
#     })                                                                       #
#                                                                              #
# 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:                                                     #
#                                                                              #
#     :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:                                                             #
#                                                                              #
#     :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.                                      #
#                                                                              #
#     PluginManager.register({                                                 #
#       :name    => "QoL Improvements",                                        #
#       :version => "1.0",                                                     #
#       :link    => "https://reliccastle.com/link-to-the-plugin/",             #
#       :credits => "Marin",                                                   #
#       :incompatibilities => [                                                #
#         "Simple Extension"                                                   #
#       ]                                                                      #
#     })                                                                       #
#                                                                              #
#------------------------------------------------------------------------------#
#                               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.           #
#                                                                              #
# 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.                        #
#                                                                              #
# 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.                                                #
#                                                                              #
#------------------------------------------------------------------------------#
#                     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
    link         = nil
    dependencies = nil
    incompats    = nil
    credits      = []
    order = [:name, :version, :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)
      idx_a = order.size if idx_a == -1
      idx_b = order.index(b)
      idx_b = order.size if idx_b == -1
      next idx_a <=> idx_b
    end
    for key in keys
      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 :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)
        for dep in value
          if dep.is_a?(String)   # "plugin name"
            if !self.installed?(dep)
              self.error("Plugin '#{name}' requires plugin '#{dep}' to be installed above it.")
            end
          elsif dep.is_a?(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)}."
                  if dep_link = self.link(dep_name)
                    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)}."
                  if dep_link = self.link(dep_name)
                    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)}."
                  if dep_link = self.link(dep_name)
                    msg << "\r\nCheck #{dep_link} for an update to plugin '#{dep_name}'."
                  end
                  self.error(msg)
                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."
                  self.error(msg)
                end
              end
            end
          end
        end
      when :incompatibilities   # Plugin incompatibilities
        incompats = value
        incompats = [incompats] if !incompats.is_a?(Array)
        for incompat in incompats
          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)
          for entry in value
            if !entry.is_a?(String)
              self.error("Plugin '#{name}'s credits array contains a non-string value.")
            else
              credits << entry
            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
    for plugin in @@Plugins.values
      if plugin[:incompatibilities] && 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,
      :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
      echoln "Plugin Error:\r\n#{msg}"
      p "Plugin Error: #{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.split("")
    d1.insert(0, "0") if d1[0] == "."          # Turn ".123" into "0.123"
    while d1[-1] == "."; d1 = d1[0..-2]; end   # Turn "123." into "123"
    d2 = v2.split("")
    d2.insert(0, "0") if d2[0] == "."          # Turn ".123" into "0.123"
    while d2[-1] == "."; d2 = d2[0..-2]; end   # Turn "123." into "123"
    for i in 0...[d1.size, d2.size].max   # 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)
      else
        return -1 if c2
      end
    end
    return 0
  end
  #-----------------------------------------------------------------------------
  #  formats the error message
  #-----------------------------------------------------------------------------
  def self.pluginErrorMsg(name, script)
    # begin message formatting
    message  = "[Infinite Fusion version #{Settings::GAME_VERSION_NUMBER}]\r\n"
    message += "#{Essentials::ERROR_TEXT}\r\n"   # For third party scripts to add to
    message += "Error in Plugin [#{name}]:\r\n"
    message += "#{$!.class} occurred.\r\n"
    # go through message content
    for line in $!.message.split("\r\n")
      next if nil_or_empty?(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}: " : ""
      message += "#{linum}#{err}: #{lms}\r\n"
    end
    # show last 10 lines of backtrace
    message += "\r\nBacktrace:\r\n"
    $!.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) >= 500000
      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 '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 !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 []
    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]
    og = [name] if !og
    # 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 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
    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
        # catch missing dependency
        self.error("Plugin '#{o}' requires plugin '#{dname}' to work properly.") if !order.include?(dname)
        # 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")
    Input.update
    return true if Input.press?(Input::CTRL)
    # 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
        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)
    echo 'Compiling plugin scripts...'
    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') 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
    echoln ' done.'
    echoln ''
  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)
    # load plugins
    scripts = load_data("Data/PluginScripts.rxdata")
    echoed_plugins = []
    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[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)
          echoln "Loaded plugin: #{name}" 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
    echoln '' if !echoed_plugins.empty?
  end
  #-----------------------------------------------------------------------------
end
