Plugins: Add plugin settings

- Add support for plugin settings, they appear in the same menu as the plugins, indented from the left to emphasize which plugin the setting belongs to
  - Note that plugin settings can still be changed even when the plugins are stopped
- Add tooltips to plugin menu to view the plugin descriptions, description_lang can also be used
- Fix scolling not working on song select when returning from game settings
- Let instance owners set default plugin files in config.py, to make them easier to maintain
- plugins.add() can now add plugins using a url
- Plugins can be hidden from the plugin menu using PluginLoader.hide, an option in plugins.add(), or in config.py
- Make p2.disable() incremental so that multiple plugins can disable multiplayer independently
- Server no longer crashes if certain optional config fields were not copied over from an updated example config
- Fix not being able to unload plugins if one was imported with errors
This commit is contained in:
KatieFrogs
2022-02-22 16:23:01 +03:00
parent 78fe7062dc
commit 7d818877f8
15 changed files with 375 additions and 71 deletions

View File

@@ -8,18 +8,39 @@ class Plugins{
this.hashes = []
this.startOrder = []
}
add(script, name){
add(script, options){
options = options || {}
var hash = md5.base64(script.toString())
var isUrl = typeof script === "string" && !options.raw
if(isUrl){
hash = "url " + hash
}else if(typeof script !== "string"){
hash = "class " + hash
}
var name = options.name
if(!name && isUrl){
name = script
var index = name.lastIndexOf("/")
if(index !== -1){
name = name.slice(index + 1)
}
if(name.endsWith(".taikoweb.js")){
name = name.slice(0, -".taikoweb.js".length)
}else if(name.endsWith(".js")){
name = name.slice(0, -".js".length)
}
}
name = name || "plugin"
if(this.hashes.indexOf(hash) !== -1){
console.warn("Skip adding an already addded plugin: " + name)
return
}
name = name || "plugin"
var baseName = name
for(var i = 2; name in this.pluginMap; i++){
name = baseName + i.toString()
}
var plugin = new PluginLoader(script, name, hash)
var plugin = new PluginLoader(script, name, hash, options.raw)
plugin.hide = !!options.hide
this.allPlugins.push({
name: name,
plugin: plugin
@@ -41,10 +62,14 @@ class Plugins{
if(index !== -1){
this.allPlugins.splice(index, 1)
}
var index = this.startOrder.indexOf(name)
if(index !== -1){
this.startOrder.splice(index, 1)
}
delete this.pluginMap[name]
}
load(name){
this.pluginMap[name].load()
return this.pluginMap[name].load()
}
loadAll(){
for(var i = 0; i < this.allPlugins.length; i++){
@@ -52,7 +77,7 @@ class Plugins{
}
}
start(name){
this.pluginMap[name].start()
return this.pluginMap[name].start()
}
startAll(){
for(var i = 0; i < this.allPlugins.length; i++){
@@ -60,7 +85,7 @@ class Plugins{
}
}
stop(name){
this.pluginMap[name].stop()
return this.pluginMap[name].stop()
}
stopAll(){
for(var i = this.startOrder.length; i--;){
@@ -68,7 +93,7 @@ class Plugins{
}
}
unload(name){
this.pluginMap[name].unload()
return this.pluginMap[name].unload()
}
unloadAll(){
for(var i = this.startOrder.length; i--;){
@@ -127,30 +152,79 @@ class Plugins{
}
getSettings(){
var items = {}
var items = []
for(var i = 0; i < this.allPlugins.length; i++){
var obj = this.allPlugins[i]
let plugin = obj.plugin
items[obj.name] = {
name: plugin.module ? this.getLocalTitle(plugin.module.name || obj.name, plugin.module.name_lang) : obj.name,
type: "toggle",
default: true,
getItem: () => plugin.started,
setItem: value => {
if(plugin.started && !value){
this.stop(plugin.name)
}else if(!plugin.started && value){
this.start(plugin.name)
}
if(!plugin.loaded){
continue
}
if(!plugin.hide){
let description
let description_lang
var module = plugin.module
if(module){
description = [
module.description,
module.author ? strings.plugins.author.replace("%s", module.author) : null,
module.version ? strings.plugins.version.replace("%s", module.version) : null
].filter(Boolean).join("\n")
description_lang = {}
languageList.forEach(lang => {
description_lang[lang] = [
this.getLocalTitle(module.description, module.description_lang, lang),
module.author ? allStrings[lang].plugins.author.replace("%s", module.author) : null,
module.version ? allStrings[lang].plugins.version.replace("%s", module.version) : null
].filter(Boolean).join("\n")
})
}
var name = module && module.name || obj.name
var name_lang = module && module.name_lang
items.push({
name: name,
name_lang: name_lang,
description: description,
description_lang: description_lang,
type: "toggle",
default: true,
getItem: () => plugin.started,
setItem: value => {
if(plugin.started && !value){
this.stop(plugin.name)
}else if(!plugin.started && value){
this.start(plugin.name)
}
}
})
}
var settings = plugin.settings()
if(settings){
settings.forEach(setting => {
if(!setting.name){
setting.name = name
if(!setting.name_lang){
setting.name_lang = name_lang
}
}
if(typeof setting.getItem !== "function"){
setting.getItem = () => {}
}
if(typeof setting.setItem !== "function"){
setting.setItem = () => {}
}
if(!("indent" in setting) && !plugin.hide){
setting.indent = 1
}
items.push(setting)
})
}
}
return items
}
getLocalTitle(title, titleLang){
getLocalTitle(title, titleLang, lang){
if(titleLang){
for(var id in titleLang){
if(id === strings.id && titleLang[id]){
if(id === (lang || strings.id) && titleLang[id]){
return titleLang[id]
}
}
@@ -163,20 +237,30 @@ class PluginLoader{
constructor(...args){
this.init(...args)
}
init(script, name, hash){
init(script, name, hash, raw){
this.name = name
this.hash = hash
if(typeof script === "string"){
this.url = URL.createObjectURL(new Blob([script], {
type: "application/javascript"
}))
if(raw){
this.url = URL.createObjectURL(new Blob([script], {
type: "application/javascript"
}))
}else{
this.url = script
}
}else{
this.class = script
}
}
load(){
if(this.loaded || !this.url && !this.class){
load(loadErrors){
if(this.loaded){
return Promise.resolve()
}else if(!this.url && !this.class){
if(loadErrors){
return Promise.reject()
}else{
return Promise.resolve()
}
}else{
return (this.url ? import(this.url) : Promise.resolve({
default: this.class
@@ -209,7 +293,11 @@ class PluginLoader{
}, e => {
console.error(e)
this.error()
return Promise.resolve()
if(loadErrors){
return Promise.reject(e)
}else{
return Promise.resolve()
}
})
}
}
@@ -300,6 +388,20 @@ class PluginLoader{
}
this.unload(true)
}
settings(){
if(this.module && this.module.settings){
try{
var settings = this.module.settings()
}catch(e){
console.error(e)
this.error()
return
}
if(Array.isArray(settings)){
return settings
}
}
}
}
class EditValue{
@@ -308,6 +410,9 @@ class EditValue{
}
init(parent, name){
if(name){
if(!parent){
throw new Error("Parent is not defined")
}
this.name = [parent, name]
this.delete = !(name in parent)
}else{