Files
infinitefusion-e18/Data/Scripts/001_Technical/005_PluginManager.rb
2025-06-07 08:16:50 -04:00

713 lines
36 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: #
# #
# 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