More conversions, user classes, some configs

Removed writePluginList config option
This commit is contained in:
Norbi Peti 2023-04-14 03:07:25 +02:00
parent 85efc873d6
commit 4ed001cb54
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
16 changed files with 423 additions and 447 deletions

View file

@ -248,7 +248,7 @@
<github.global.server>github</github.global.server> <github.global.server>github</github.global.server>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<noprefix.version>1.0.1</noprefix.version> <noprefix.version>1.0.1</noprefix.version>
<kotlin.version>1.8.10</kotlin.version> <kotlin.version>1.8.20</kotlin.version>
</properties> </properties>
<scm> <scm>
<url>https://github.com/TBMCPlugins/mvn-repo</url> <url>https://github.com/TBMCPlugins/mvn-repo</url>

View file

@ -26,17 +26,10 @@ import org.bukkit.command.Command
import org.bukkit.command.CommandSender import org.bukkit.command.CommandSender
import org.bukkit.command.ConsoleCommandSender import org.bukkit.command.ConsoleCommandSender
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.util.* import java.util.*
import java.util.function.Function
import java.util.function.Supplier import java.util.function.Supplier
import java.util.logging.Logger
class MainPlugin : ButtonPlugin() { class MainPlugin : ButtonPlugin() {
private var logger: Logger? = null
private var economy: Economy? = null private var economy: Economy? = null
/** /**
@ -45,12 +38,6 @@ class MainPlugin : ButtonPlugin() {
*/ */
var isChatHandlerEnabled = true var isChatHandlerEnabled = true
/**
* Sets whether the plugin should write a list of installed plugins in a txt file.
* It can be useful if some other software needs to know the plugins.
*/
private val writePluginList = iConfig.getData("writePluginList", false)
/** /**
* The chat format to use for messages from other platforms if Chroma-Chat is not installed. * The chat format to use for messages from other platforms if Chroma-Chat is not installed.
*/ */
@ -68,12 +55,11 @@ class MainPlugin : ButtonPlugin() {
*/ */
val prioritizeCustomCommands = iConfig.getData("prioritizeCustomCommands", false) val prioritizeCustomCommands = iConfig.getData("prioritizeCustomCommands", false)
public override fun pluginEnable() { public override fun pluginEnable() {
Instance = this instance = this
val pdf = description val pdf = description
logger = getLogger()
if (!setupPermissions()) throw NullPointerException("No permission plugin found!") if (!setupPermissions()) throw NullPointerException("No permission plugin found!")
if (!setupEconomy()) //Though Essentials always provides economy, but we don't require Essentials if (!setupEconomy()) //Though Essentials always provides economy, but we don't require Essentials
getLogger().warning("No economy plugin found! Components using economy will not be registered.") logger.warning("No economy plugin found! Components using economy will not be registered.")
saveConfig() saveConfig()
registerComponent(this, RestartComponent()) registerComponent(this, RestartComponent())
registerComponent(this, ChannelComponent()) registerComponent(this, ChannelComponent())
@ -125,31 +111,20 @@ class MainPlugin : ButtonPlugin() {
TBMCChatAPI.RegisterChatChannel(ChatRoom("§aGREEN§f", Color.Green, "green")) TBMCChatAPI.RegisterChatChannel(ChatRoom("§aGREEN§f", Color.Green, "green"))
TBMCChatAPI.RegisterChatChannel(ChatRoom("§bBLUE§f", Color.Blue, "blue")) TBMCChatAPI.RegisterChatChannel(ChatRoom("§bBLUE§f", Color.Blue, "blue"))
TBMCChatAPI.RegisterChatChannel(ChatRoom("§5PURPLE§f", Color.DarkPurple, "purple")) TBMCChatAPI.RegisterChatChannel(ChatRoom("§5PURPLE§f", Color.DarkPurple, "purple"))
val playerSupplier = Supplier { Bukkit.getOnlinePlayers().map { obj: Player -> obj.name }.asIterable() } val playerSupplier = Supplier { Bukkit.getOnlinePlayers().map { obj -> obj.name }.asIterable() }
command2MC.addParamConverter(OfflinePlayer::class.java, { name: String? -> command2MC.addParamConverter(
Bukkit.getOfflinePlayer( OfflinePlayer::class.java,
name!! { name -> Bukkit.getOfflinePlayer(name) },
) "Player not found!",
}, "Player not found!", playerSupplier) playerSupplier
command2MC.addParamConverter<Player>(
Player::class.java, Function { name: String ->
Bukkit.getPlayer(name)
}, "Online player not found!", playerSupplier
) )
if (writePluginList.get()) { command2MC.addParamConverter(
try { Player::class.java,
Files.write(File("plugins", "plugins.txt").toPath(), Iterable { { name -> Bukkit.getPlayer(name) },
Arrays.stream(Bukkit.getPluginManager().plugins) "Online player not found!",
.map { p: Plugin -> p.dataFolder.name as CharSequence } playerSupplier
.iterator()
})
} catch (e: IOException) {
TBMCCoreAPI.SendException("Failed to write plugin list!", e, this)
}
}
if (server.pluginManager.isPluginEnabled("Essentials")) ess = getPlugin(
Essentials::class.java
) )
if (server.pluginManager.isPluginEnabled("Essentials")) ess = getPlugin(Essentials::class.java)
logger!!.info(pdf.name + " has been Enabled (V." + pdf.version + ") Test: " + test.get() + ".") logger!!.info(pdf.name + " has been Enabled (V." + pdf.version + ") Test: " + test.get() + ".")
} }
@ -182,8 +157,7 @@ class MainPlugin : ButtonPlugin() {
} }
companion object { companion object {
@JvmField lateinit var instance: MainPlugin
var Instance: MainPlugin = null
@JvmField @JvmField
var permission: Permission? = null var permission: Permission? = null

View file

@ -1,83 +1,69 @@
package buttondevteam.core.component.channel; package buttondevteam.core.component.channel
import buttondevteam.lib.ChromaUtils; import buttondevteam.core.MainPlugin
import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.ChromaUtils
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.TBMCSystemChatEvent.BroadcastTarget
import buttondevteam.lib.chat.*; import buttondevteam.lib.architecture.Component
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.chat.*
import lombok.RequiredArgsConstructor; import buttondevteam.lib.chat.Command2.*
import org.bukkit.plugin.java.JavaPlugin; import buttondevteam.lib.player.ChromaGamerBase
import org.bukkit.plugin.java.JavaPlugin
/** /**
* Manages chat channels. If disabled, only global channels will be registered. * Manages chat channels. If disabled, only global channels will be registered.
*/ */
public class ChannelComponent extends Component<JavaPlugin> { class ChannelComponent : Component<MainPlugin>() {
static TBMCSystemChatEvent.BroadcastTarget roomJoinLeave; override fun register(plugin: JavaPlugin) {
super.register(plugin)
roomJoinLeave = BroadcastTarget.add("roomJoinLeave") //Even if it's disabled, global channels continue to work
}
@Override override fun unregister(plugin: JavaPlugin) {
protected void register(JavaPlugin plugin) { super.unregister(plugin)
super.register(plugin); BroadcastTarget.remove(roomJoinLeave)
roomJoinLeave = TBMCSystemChatEvent.BroadcastTarget.add("roomJoinLeave"); //Even if it's disabled, global channels continue to work roomJoinLeave = null
} }
@Override override fun enable() {}
protected void unregister(JavaPlugin plugin) { override fun disable() {}
super.unregister(plugin); fun registerChannelCommand(channel: Channel) {
TBMCSystemChatEvent.BroadcastTarget.remove(roomJoinLeave); if (!ChromaUtils.isTest()) registerCommand(ChannelCommand(channel))
roomJoinLeave = null; }
}
@Override @CommandClass
protected void enable() { private class ChannelCommand(private val channel: Channel) : ICommand2MC() {
} override fun getCommandPath(): String {
return channel.identifier
}
@Override override fun getCommandPaths(): Array<String> {
protected void disable() { return channel.extraIdentifiers.get().toTypedArray()
} }
void registerChannelCommand(Channel channel) { @Subcommand
if (!ChromaUtils.isTest()) fun def(senderMC: Command2MCSender, @OptionalArg @TextArg message: String?) {
registerCommand(new ChannelCommand(channel)); val sender = senderMC.sender
} val user = ChromaGamerBase.getFromSender(sender)
if (user == null) {
sender.sendMessage("§cYou can't use channels from this platform.")
return
}
if (message == null) {
val oldch = user.channel.get()
if (oldch is ChatRoom) oldch.leaveRoom(sender)
if (oldch == channel) user.channel.set(Channel.GlobalChat) else {
user.channel.set(channel)
if (channel is ChatRoom) channel.joinRoom(sender)
}
sender.sendMessage("§6You are now talking in: §b" + user.channel.get().displayName.get())
} else TBMCChatAPI.SendChatMessage(
ChatMessage.builder(sender, user, message).fromCommand(true)
.permCheck(senderMC.permCheck).build(), channel
)
}
}
@CommandClass companion object {
@RequiredArgsConstructor var roomJoinLeave: BroadcastTarget? = null
private static class ChannelCommand extends ICommand2MC { }
private final Channel channel;
@Override
public String getCommandPath() {
return channel.identifier;
}
@Override
public String[] getCommandPaths() {
return channel.extraIdentifiers.get();
}
@Command2.Subcommand
public void def(Command2MCSender senderMC, @Command2.OptionalArg @Command2.TextArg String message) {
var sender = senderMC.getSender();
var user = ChromaGamerBase.getFromSender(sender);
if (user == null) {
sender.sendMessage("§cYou can't use channels from this platform.");
return;
}
if (message == null) {
Channel oldch = user.channel.get();
if (oldch instanceof ChatRoom)
((ChatRoom) oldch).leaveRoom(sender);
if (oldch.equals(channel))
user.channel.set(Channel.GlobalChat);
else {
user.channel.set(channel);
if (channel instanceof ChatRoom)
((ChatRoom) channel).joinRoom(sender);
}
sender.sendMessage("§6You are now talking in: §b" + user.channel.get().displayName.get());
} else
TBMCChatAPI.SendChatMessage(ChatMessage.builder(sender, user, message).fromCommand(true)
.permCheck(senderMC.getPermCheck()).build(), channel);
}
}
} }

View file

@ -33,7 +33,7 @@ public class MemberCommand extends ICommand2MC {
} }
public boolean addRemove(CommandSender sender, OfflinePlayer op, boolean add) { public boolean addRemove(CommandSender sender, OfflinePlayer op, boolean add) {
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> { Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.instance, () -> {
if (!op.hasPlayedBefore()) { if (!op.hasPlayedBefore()) {
sender.sendMessage("§cCannot find player or haven't played before."); sender.sendMessage("§cCannot find player or haven't played before.");
return; return;

View file

@ -51,7 +51,7 @@ public class ScheduledRestartCommand extends ICommand2MC {
sender.sendMessage("Scheduled restart in " + seconds); sender.sendMessage("Scheduled restart in " + seconds);
ScheduledServerRestartEvent e = new ScheduledServerRestartEvent(restarttime, this); ScheduledServerRestartEvent e = new ScheduledServerRestartEvent(restarttime, this);
Bukkit.getPluginManager().callEvent(e); Bukkit.getPluginManager().callEvent(e);
restarttask = Bukkit.getScheduler().runTaskTimer(MainPlugin.Instance, () -> { restarttask = Bukkit.getScheduler().runTaskTimer(MainPlugin.instance, () -> {
if (restartCounter < 0) { if (restartCounter < 0) {
restarttask.cancel(); restarttask.cancel();
restartbar.getPlayers().forEach(p -> restartbar.removePlayer(p)); restartbar.getPlayers().forEach(p -> restartbar.removePlayer(p));

View file

@ -81,7 +81,7 @@ public final class ChromaUtils {
*/ */
public static <T> T doItAsync(Supplier<T> what, T def) { public static <T> T doItAsync(Supplier<T> what, T def) {
if (Bukkit.isPrimaryThread()) if (Bukkit.isPrimaryThread())
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, what::get); Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.instance, what::get);
else else
return what.get(); return what.get();
return def; return def;

View file

@ -170,7 +170,7 @@ public class TBMCCoreAPI {
} }
public static boolean IsTestServer() { public static boolean IsTestServer() {
if (MainPlugin.Instance == null) return true; if (MainPlugin.instance == null) return true;
return MainPlugin.Instance.test.get(); return MainPlugin.instance.test.get();
} }
} }

View file

@ -67,9 +67,9 @@ class ConfigData<T> internal constructor(
return getter.apply(convert(`val`, pdef)).also { value = it } return getter.apply(convert(`val`, pdef)).also { value = it }
} }
override fun set(value: T?) { override fun set(value: T?) { // TODO: Have a separate method for removing the value from the config and make this non-nullable
if (readOnly) return //Safety for Discord channel/role data if (readOnly) return //Safety for Discord channel/role data
val `val` = value?.let { setter.apply(value) } val `val` = value?.let { setter.apply(it) }
setInternal(`val`) setInternal(`val`)
this.value = value this.value = value
} }
@ -89,14 +89,14 @@ class ConfigData<T> internal constructor(
val sa = config.saveAction val sa = config.saveAction
val root = cc.root val root = cc.root
if (root == null) { if (root == null) {
MainPlugin.Instance.logger.warning("Attempted to save config with no root! Name: ${config.config.name}") MainPlugin.instance.logger.warning("Attempted to save config with no root! Name: ${config.config.name}")
return return
} }
if (!saveTasks.containsKey(cc.root)) { if (!saveTasks.containsKey(cc.root)) {
synchronized(saveTasks) { synchronized(saveTasks) {
saveTasks.put( saveTasks.put(
root, root,
SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, { SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.instance, {
synchronized(saveTasks) { synchronized(saveTasks) {
saveTasks.remove(root) saveTasks.remove(root)
sa.run() sa.run()

View file

@ -179,7 +179,7 @@ class IHaveConfig(
.filter(Predicate<ConfigData<Any?>> { obj: ConfigData<Any?>? -> Objects.nonNull(obj) }) .filter(Predicate<ConfigData<Any?>> { obj: ConfigData<Any?>? -> Objects.nonNull(obj) })
.collect(Collectors.toList()) .collect(Collectors.toList())
} else { } else {
if (TBMCCoreAPI.IsTestServer()) MainPlugin.Instance.logger.warning( if (TBMCCoreAPI.IsTestServer()) MainPlugin.instance.logger.warning(
"Method " + mName + " returns a config but its parameters are unknown: " + Arrays.toString( "Method " + mName + " returns a config but its parameters are unknown: " + Arrays.toString(
m.parameterTypes m.parameterTypes
) )
@ -187,7 +187,7 @@ class IHaveConfig(
continue continue
} }
for (c in configList) { for (c in configList) {
if (c.path.length == 0) c.setPath(mName) else if (c.path != mName) MainPlugin.Instance.logger.warning( if (c.path.length == 0) c.setPath(mName) else if (c.path != mName) MainPlugin.instance.logger.warning(
"Config name does not match: " + c.path + " instead of " + mName "Config name does not match: " + c.path + " instead of " + mName
) )
c.get() //Saves the default value if needed - also checks validity c.get() //Saves the default value if needed - also checks validity

View file

@ -23,7 +23,7 @@ class ListConfigData<T> internal constructor(
listConfig.reset() listConfig.reset()
} }
override fun get(): List? { override fun get(): List {
return listConfig.get() return listConfig.get()
} }

View file

@ -94,7 +94,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
return false // Unknown command return false // Unknown command
} }
//Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread //Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance) { _ -> Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.instance) { _ ->
try { try {
dispatcher.execute(results) dispatcher.execute(results)
} catch (e: CommandSyntaxException) { } catch (e: CommandSyntaxException) {
@ -103,7 +103,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
TBMCCoreAPI.SendException( TBMCCoreAPI.SendException(
"Command execution failed for sender " + sender.name + "(" + sender.javaClass.canonicalName + ") and message " + commandline, "Command execution failed for sender " + sender.name + "(" + sender.javaClass.canonicalName + ") and message " + commandline,
e, e,
MainPlugin.Instance MainPlugin.instance
) )
} }
} }
@ -140,7 +140,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
lastNode.addChild(getExecutableNode(meth, command, ann, remainingPath, CommandArgumentHelpManager(command), fullPath)) lastNode.addChild(getExecutableNode(meth, command, ann, remainingPath, CommandArgumentHelpManager(command), fullPath))
if (mainCommandNode == null) mainCommandNode = mainNode if (mainCommandNode == null) mainCommandNode = mainNode
else if (mainNode!!.name != mainCommandNode.name) { else if (mainNode!!.name != mainCommandNode.name) {
MainPlugin.Instance.logger.warning("Multiple commands are defined in the same class! This is not supported. Class: " + command.javaClass.simpleName) MainPlugin.instance.logger.warning("Multiple commands are defined in the same class! This is not supported. Class: " + command.javaClass.simpleName)
} }
} }
if (mainCommandNode == null) { if (mainCommandNode == null) {

View file

@ -170,8 +170,9 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
val i = commandline.indexOf(' ') val i = commandline.indexOf(' ')
val mainpath = commandline.substring(1, if (i == -1) commandline.length else i) //Without the slash val mainpath = commandline.substring(1, if (i == -1) commandline.length else i) //Without the slash
//Our commands aren't PluginCommands, unless it's specified in the plugin.yml //Our commands aren't PluginCommands, unless it's specified in the plugin.yml
return if ((!checkPlugin || (MainPlugin.Instance.prioritizeCustomCommands.get() == true)) return if ((!checkPlugin || (MainPlugin.instance.prioritizeCustomCommands.get() == true))
|| Bukkit.getPluginCommand(mainpath)?.let { it.plugin is ButtonPlugin } != false) || Bukkit.getPluginCommand(mainpath)?.let { it.plugin is ButtonPlugin } != false
)
super.handleCommand(sender, commandline) else false super.handleCommand(sender, commandline) else false
} }
@ -207,7 +208,11 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
private fun executeCommand(sender: CommandSender, command: Command, label: String, args: Array<String>): Boolean { private fun executeCommand(sender: CommandSender, command: Command, label: String, args: Array<String>): Boolean {
val user = ChromaGamerBase.getFromSender(sender) val user = ChromaGamerBase.getFromSender(sender)
if (user == null) { if (user == null) {
TBMCCoreAPI.SendException("Failed to run Bukkit command for user!", Throwable("No Chroma user found"), MainPlugin.Instance) TBMCCoreAPI.SendException(
"Failed to run Bukkit command for user!",
Throwable("No Chroma user found"),
MainPlugin.instance
)
sender.sendMessage("§cAn internal error occurred.") sender.sendMessage("§cAn internal error occurred.")
return true return true
} }
@ -250,9 +255,16 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
private fun registerTabcomplete(command2MC: ICommand2MC, commandNode: LiteralCommandNode<Command2MCSender>, bukkitCommand: Command) { private fun registerTabcomplete(command2MC: ICommand2MC, commandNode: LiteralCommandNode<Command2MCSender>, bukkitCommand: Command) {
if (commodore == null) { if (commodore == null) {
commodore = CommodoreProvider.getCommodore(MainPlugin.Instance) //Register all to the Core, it's easier commodore = CommodoreProvider.getCommodore(MainPlugin.instance) //Register all to the Core, it's easier
commodore.register(LiteralArgumentBuilder.literal<Any?>("un").redirect(RequiredArgumentBuilder.argument<Any?, String>("unsomething", commodore.register(LiteralArgumentBuilder.literal<Any?>("un")
StringArgumentType.word()).suggests { context: CommandContext<Any?>?, builder: SuggestionsBuilder -> builder.suggest("untest").buildFuture() }.build())) .redirect(RequiredArgumentBuilder.argument<Any?, String>(
"unsomething",
StringArgumentType.word()
).suggests { context: CommandContext<Any?>?, builder: SuggestionsBuilder ->
builder.suggest("untest").buildFuture()
}.build()
)
)
} }
commodore!!.dispatcher.root.getChild(commandNode.name) // TODO: Probably unnecessary commodore!!.dispatcher.root.getChild(commandNode.name) // TODO: Probably unnecessary
val customTCmethods = Arrays.stream(command2MC.javaClass.declaredMethods) //val doesn't recognize the type arguments val customTCmethods = Arrays.stream(command2MC.javaClass.declaredMethods) //val doesn't recognize the type arguments

View file

@ -32,18 +32,18 @@ class CommandArgumentHelpManager<TC : ICommand2<TP>, TP : Command2Sender>(comman
TBMCCoreAPI.SendException( TBMCCoreAPI.SendException(
"Error while getting command data!", "Error while getting command data!",
Exception("Resource not found!"), Exception("Resource not found!"),
MainPlugin.Instance MainPlugin.instance
) )
return@use return@use
} }
val config = YamlConfiguration.loadConfiguration(InputStreamReader(str)) val config = YamlConfiguration.loadConfiguration(InputStreamReader(str))
commandConfig = config.getConfigurationSection(commandClass.canonicalName.replace('$', '.')) commandConfig = config.getConfigurationSection(commandClass.canonicalName.replace('$', '.'))
if (commandConfig == null) { if (commandConfig == null) {
MainPlugin.Instance.logger.warning("Failed to get command data for $commandClass! Make sure to use 'clean install' when building the project.") MainPlugin.instance.logger.warning("Failed to get command data for $commandClass! Make sure to use 'clean install' when building the project.")
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
TBMCCoreAPI.SendException("Error while getting command data!", e, MainPlugin.Instance) TBMCCoreAPI.SendException("Error while getting command data!", e, MainPlugin.instance)
} }
} }
@ -56,7 +56,7 @@ class CommandArgumentHelpManager<TC : ICommand2<TP>, TP : Command2Sender>(comman
fun getParameterHelpForMethod(method: Method): String? { fun getParameterHelpForMethod(method: Method): String? {
val cs = commandConfig?.getConfigurationSection(method.name) val cs = commandConfig?.getConfigurationSection(method.name)
if (cs == null) { if (cs == null) {
MainPlugin.Instance.logger.warning("Failed to get command data for $method! Make sure to use 'clean install' when building the project.") MainPlugin.instance.logger.warning("Failed to get command data for $method! Make sure to use 'clean install' when building the project.")
return null return null
} }
val mname = cs.getString("method") val mname = cs.getString("method")
@ -68,7 +68,7 @@ class CommandArgumentHelpManager<TC : ICommand2<TP>, TP : Command2Sender>(comman
} else TBMCCoreAPI.SendException( } else TBMCCoreAPI.SendException(
"Error while getting command data for $method!", "Error while getting command data for $method!",
Exception("Method '$method' != $mname or params is $params"), Exception("Method '$method' != $mname or params is $params"),
MainPlugin.Instance MainPlugin.instance
) )
return null return null
} }

View file

@ -1,317 +1,327 @@
package buttondevteam.lib.player; package buttondevteam.lib.player
import buttondevteam.core.MainPlugin; import buttondevteam.core.MainPlugin
import buttondevteam.core.component.channel.Channel; import buttondevteam.core.component.channel.Channel
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.core.component.channel.Channel.Companion.getChannels
import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.IHaveConfig; import buttondevteam.lib.architecture.ConfigData
import lombok.Getter; import buttondevteam.lib.architecture.ConfigData.Companion.saveNow
import lombok.val; import buttondevteam.lib.architecture.IHaveConfig
import org.bukkit.Bukkit; import org.bukkit.Bukkit
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
import javax.annotation.Nullable; import java.util.*
import java.io.File; import java.util.function.Consumer
import java.util.ArrayList; import java.util.function.Function
import java.util.HashMap; import java.util.function.Supplier
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@ChromaGamerEnforcer @ChromaGamerEnforcer
public abstract class ChromaGamerBase { abstract class ChromaGamerBase {
private static final String TBMC_PLAYERS_DIR = "TBMC/players/"; lateinit var config: IHaveConfig
private static final ArrayList<Function<CommandSender, ? extends Optional<? extends ChromaGamerBase>>> senderConverters = new ArrayList<>();
/**
* Holds data per user class
*/
private static final HashMap<Class<? extends ChromaGamerBase>, StaticUserData<?>> staticDataMap = new HashMap<>();
/** @JvmField
* Use {@link #getConfig()} where possible; the 'id' must be always set protected var commonUserData: CommonUserData<*>? = null
*/ protected open fun init() {
//protected YamlConfiguration plugindata; config.reset(commonUserData!!.playerData)
}
@Getter protected fun updateUserConfig() {}
protected final IHaveConfig config = new IHaveConfig(this::save);
protected CommonUserData<?> commonUserData;
/** /**
* Used for connecting with every type of user ({@link #connectWith(ChromaGamerBase)}) and to init the configs. * Saves the player. It'll handle all exceptions that may happen. Called automatically.
* Also, to construct an instance if an abstract class is provided. */
*/ protected open fun save() {
public static <T extends ChromaGamerBase> void RegisterPluginUserClass(Class<T> userclass, Supplier<T> constructor) { try {
Class<? extends T> cl; if (commonUserData!!.playerData.getKeys(false).size > 0) commonUserData!!.playerData.save(
String folderName; File(
if (userclass.isAnnotationPresent(UserClass.class)) { TBMC_PLAYERS_DIR + folder, fileName + ".yml"
cl = userclass; )
folderName = userclass.getAnnotation(UserClass.class).foldername(); )
} else if (userclass.isAnnotationPresent(AbstractUserClass.class)) { } catch (e: Exception) {
var ucl = userclass.getAnnotation(AbstractUserClass.class).prototype(); TBMCCoreAPI.SendException(
if (!userclass.isAssignableFrom(ucl)) "Error while saving player to " + folder + "/" + fileName + ".yml!",
throw new RuntimeException("The prototype class (" + ucl.getSimpleName() + ") must be a subclass of the userclass parameter (" + userclass.getSimpleName() + ")!"); e,
//noinspection unchecked MainPlugin.instance
cl = (Class<? extends T>) ucl; )
folderName = userclass.getAnnotation(AbstractUserClass.class).foldername(); }
} else // <-- Really important }
throw new RuntimeException("Class not registered as a user class! Use @UserClass or TBMCPlayerBase");
var sud = new StaticUserData<T>(folderName);
sud.getConstructors().put(cl, constructor);
sud.getConstructors().put(userclass, constructor); // Alawys register abstract and prototype class (TBMCPlayerBase and TBMCPlayer)
staticDataMap.put(userclass, sud);
}
/** /**
* Returns the folder name for the given player class. * Removes the user from the cache. This will be called automatically after some time by default.
* */
* @param cl The class to get the folder from (like {@link TBMCPlayerBase} or one of it's subclasses) fun uncache() {
* @return The folder name for the given type val userCache: HashMap<Class<out Any?>, out ChromaGamerBase> = commonUserData!!.userCache
* @throws RuntimeException If the class doesn't have the {@link UserClass} annotation. synchronized(userCache) { if (userCache.containsKey(javaClass)) check(userCache.remove(javaClass) === this) { "A different player instance was cached!" } }
*/ }
public static <T extends ChromaGamerBase> String getFolderForType(Class<T> cl) {
if (cl.isAnnotationPresent(UserClass.class))
return cl.getAnnotation(UserClass.class).foldername();
else if (cl.isAnnotationPresent(AbstractUserClass.class))
return cl.getAnnotation(AbstractUserClass.class).foldername();
throw new RuntimeException("Class not registered as a user class! Use @UserClass or @AbstractUserClass");
}
/** protected open fun scheduleUncache() {
* Returns the player class for the given folder name. Bukkit.getScheduler().runTaskLaterAsynchronously(
* MainPlugin.instance,
* @param foldername The folder to get the class from (like "minecraft") Runnable { uncache() },
* @return The type for the given folder name or null if not found (2 * 60 * 60 * 20).toLong()
*/ ) //2 hours
public static Class<? extends ChromaGamerBase> getTypeForFolder(String foldername) { }
synchronized (staticDataMap) {
return staticDataMap.entrySet().stream().filter(e -> e.getValue().getFolder().equalsIgnoreCase(foldername))
.map(Map.Entry::getKey).findAny().orElse(null);
}
}
/*** /**
* Retrieves a user from cache or loads it from disk. * Connect two accounts. Do not use for connecting two Minecraft accounts or similar. Also make sure you have the "id" tag set.
* *
* @param fname Filename without .yml, the user's identifier for that type * @param user The account to connect with
* @param cl User class */
* @return The user object fun <T : ChromaGamerBase?> connectWith(user: T) {
*/ // Set the ID, go through all linked files and connect them as well
public static synchronized <T extends ChromaGamerBase> T getUser(String fname, Class<T> cl) { val ownFolder = folder
StaticUserData<?> staticUserData = null; val userFolder = user!!.folder
for (var sud : staticDataMap.entrySet()) { if (ownFolder.equals(
if (sud.getKey().isAssignableFrom(cl)) { userFolder,
staticUserData = sud.getValue(); ignoreCase = true
break; )
} ) throw RuntimeException("Do not connect two accounts of the same type! Type: $ownFolder")
} val ownData = commonUserData!!.playerData
if (staticUserData == null) val userData = user.commonUserData!!.playerData
throw new RuntimeException("User class not registered! Use @UserClass or @AbstractUserClass"); userData[ownFolder + "_id"] = ownData.getString(ownFolder + "_id")
var commonUserData = staticUserData.getUserDataMap().get(fname); ownData[userFolder + "_id"] = userData.getString(userFolder + "_id")
if (commonUserData == null) { config.signalChange()
final String folder = staticUserData.getFolder(); user.config.signalChange()
final File file = new File(TBMC_PLAYERS_DIR + folder, fname + ".yml"); val sync = Consumer { sourcedata: YamlConfiguration ->
file.getParentFile().mkdirs(); val sourcefolder = if (sourcedata === ownData) ownFolder else userFolder
var playerData = YamlConfiguration.loadConfiguration(file); val id = sourcedata.getString(sourcefolder + "_id")!!
commonUserData = new CommonUserData<>(playerData); for ((key, value) in staticDataMap) { // Set our ID in all files we can find, both from our connections and the new ones
playerData.set(staticUserData.getFolder() + "_id", fname); if (key == javaClass || key == user.javaClass) continue
staticUserData.getUserDataMap().put(fname, commonUserData); val entryFolder = value.folder
} val otherid = sourcedata.getString(entryFolder + "_id") ?: continue
if (commonUserData.getUserCache().containsKey(cl)) val cg = getUser(otherid, key)!!
return (T) commonUserData.getUserCache().get(cl); val cgData = cg.commonUserData!!.playerData
T obj; cgData[sourcefolder + "_id"] = id // Set new IDs
if (staticUserData.getConstructors().containsKey(cl)) for ((_, value1) in staticDataMap) {
//noinspection unchecked val itemFolder = value1.folder
obj = (T) staticUserData.getConstructors().get(cl).get(); if (sourcedata.contains(itemFolder + "_id")) {
else { cgData[itemFolder + "_id"] = sourcedata.getString(itemFolder + "_id") // Set all existing IDs
try { }
obj = cl.getConstructor().newInstance(); }
} catch (Exception e) { cg.config.signalChange()
throw new RuntimeException("Failed to create new instance of user of type " + cl.getSimpleName() + "!", e); }
} }
} sync.accept(ownData)
obj.commonUserData = commonUserData; sync.accept(userData)
obj.init(); }
obj.scheduleUncache();
return obj;
}
/** /**
* Adds a converter to the start of the list. * Returns the ID for the T typed player object connected with this one or null if no connection found.
* *
* @param converter The converter that returns an object corresponding to the sender or null, if it's not the right type. * @param cl The player class to get the ID from
*/ * @return The ID or null if not found
public static <T extends ChromaGamerBase> void addConverter(Function<CommandSender, Optional<T>> converter) { */
senderConverters.add(0, converter); fun <T : ChromaGamerBase?> getConnectedID(cl: Class<T>): String {
} return commonUserData!!.playerData.getString(getFolderForType(cl) + "_id")!!
}
/** /**
* Get from the given sender. the object's type will depend on the sender's type. May be null, but shouldn't be. * Returns a player instance of the given type that represents the same player. This will return a new instance unless the player is cached.<br></br>
* * If the class is a subclass of the current class then the same ID is used, otherwise, a connected ID is used, if found.
* @param sender The sender to use *
* @return A user as returned by a converter or null if none can supply it * @param cl The target player class
*/ * @return The player as a [T] object or null if the user doesn't have an account there
public static ChromaGamerBase getFromSender(CommandSender sender) { // TODO: Use Command2Sender */
for (val converter : senderConverters) { fun <T : ChromaGamerBase?> getAs(cl: Class<T>): T? {
val ocg = converter.apply(sender); if (cl.simpleName == javaClass.simpleName) return this as T
if (ocg.isPresent()) val newfolder = getFolderForType(cl)
return ocg.get(); ?: throw RuntimeException("The specified class " + cl.simpleName + " isn't registered!")
} if (newfolder == folder) // If in the same folder, the same filename is used
return null; return getUser(fileName, cl)
} val playerData = commonUserData!!.playerData
return if (!playerData.contains(newfolder + "_id")) null else getUser(
playerData.getString(newfolder + "_id")!!,
cl
)
}
public static void saveUsers() { val fileName: String
synchronized (staticDataMap) { /**
for (var sud : staticDataMap.values()) * This method returns the filename for this player data. For example, for Minecraft-related data, MC UUIDs, for Discord data, Discord IDs, etc.<br></br>
for (var cud : sud.getUserDataMap().values()) * **Does not include .yml**
ConfigData.saveNow(cud.getPlayerData()); //Calls save() */
} get() = commonUserData!!.playerData.getString(folder + "_id")!!
} val folder: String
/**
* This method returns the folder that this player data is stored in. For example: "minecraft".
*/
get() = getFolderForType(javaClass)
protected void init() { /**
config.reset(commonUserData.getPlayerData()); * Get player information. This method calls the [TBMCPlayerGetInfoEvent] to get all the player information across the TBMC plugins.
} *
* @param target The [InfoTarget] to return the info for.
* @return The player information.
*/
fun getInfo(target: InfoTarget?): String {
val event = TBMCPlayerGetInfoEvent(this, target)
Bukkit.getServer().pluginManager.callEvent(event)
return event.result
}
/** enum class InfoTarget {
* Saves the player. It'll handle all exceptions that may happen. Called automatically. MCHover, MCCommand, Discord
*/ }
protected void save() {
try {
if (commonUserData.getPlayerData().getKeys(false).size() > 0)
commonUserData.getPlayerData().save(new File(TBMC_PLAYERS_DIR + getFolder(), getFileName() + ".yml"));
} catch (Exception e) {
TBMCCoreAPI.SendException("Error while saving player to " + getFolder() + "/" + getFileName() + ".yml!", e, MainPlugin.Instance);
}
}
/** //-----------------------------------------------------------------
* Removes the user from the cache. This will be called automatically after some time by default. @JvmField
*/ val channel: ConfigData<Channel> = config.getData("channel", Channel.GlobalChat,
public void uncache() { { id ->
final var userCache = commonUserData.getUserCache(); getChannels().filter { ch: Channel -> ch.identifier.equals(id as String, ignoreCase = true) }
//noinspection SynchronizationOnLocalVariableOrMethodParameter .findAny().orElse(null)
synchronized (userCache) { }) { ch -> ch.ID }
if (userCache.containsKey(getClass()))
if (userCache.remove(getClass()) != this)
throw new IllegalStateException("A different player instance was cached!");
}
}
protected void scheduleUncache() { companion object {
Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, this::uncache, 2 * 60 * 60 * 20); //2 hours private const val TBMC_PLAYERS_DIR = "TBMC/players/"
} private val senderConverters = ArrayList<Function<CommandSender, out Optional<out ChromaGamerBase>>>()
/** /**
* Connect two accounts. Do not use for connecting two Minecraft accounts or similar. Also make sure you have the "id" tag set. * Holds data per user class
* */
* @param user The account to connect with private val staticDataMap = HashMap<Class<out ChromaGamerBase>, StaticUserData<*>>()
*/
public final <T extends ChromaGamerBase> void connectWith(T user) {
// Set the ID, go through all linked files and connect them as well
final String ownFolder = getFolder();
final String userFolder = user.getFolder();
if (ownFolder.equalsIgnoreCase(userFolder))
throw new RuntimeException("Do not connect two accounts of the same type! Type: " + ownFolder);
var ownData = commonUserData.getPlayerData();
var userData = user.commonUserData.getPlayerData();
userData.set(ownFolder + "_id", ownData.getString(ownFolder + "_id"));
ownData.set(userFolder + "_id", userData.getString(userFolder + "_id"));
config.signalChange();
user.config.signalChange();
Consumer<YamlConfiguration> sync = sourcedata -> {
final String sourcefolder = sourcedata == ownData ? ownFolder : userFolder;
final String id = sourcedata.getString(sourcefolder + "_id");
for (val entry : staticDataMap.entrySet()) { // Set our ID in all files we can find, both from our connections and the new ones
if (entry.getKey() == getClass() || entry.getKey() == user.getClass())
continue;
var entryFolder = entry.getValue().getFolder();
final String otherid = sourcedata.getString(entryFolder + "_id");
if (otherid == null)
continue;
ChromaGamerBase cg = getUser(otherid, entry.getKey());
var cgData = cg.commonUserData.getPlayerData();
cgData.set(sourcefolder + "_id", id); // Set new IDs
for (val item : staticDataMap.entrySet()) {
var itemFolder = item.getValue().getFolder();
if (sourcedata.contains(itemFolder + "_id")) {
cgData.set(itemFolder + "_id", sourcedata.getString(itemFolder + "_id")); // Set all existing IDs
}
}
cg.config.signalChange();
}
};
sync.accept(ownData);
sync.accept(userData);
}
/** /**
* Returns the ID for the T typed player object connected with this one or null if no connection found. * Used for connecting with every type of user ([.connectWith]) and to init the configs.
* * Also, to construct an instance if an abstract class is provided.
* @param cl The player class to get the ID from */
* @return The ID or null if not found @JvmStatic
*/ fun <T : ChromaGamerBase?> RegisterPluginUserClass(userclass: Class<T>, constructor: Supplier<T>?) {
public final <T extends ChromaGamerBase> String getConnectedID(Class<T> cl) { val cl: Class<out T>
return commonUserData.getPlayerData().getString(getFolderForType(cl) + "_id"); val folderName: String
} if (userclass.isAnnotationPresent(UserClass::class.java)) {
cl = userclass
folderName = userclass.getAnnotation(UserClass::class.java).foldername
} else if (userclass.isAnnotationPresent(AbstractUserClass::class.java)) {
val ucl: Class<out ChromaGamerBase> = userclass.getAnnotation(
AbstractUserClass::class.java
).prototype
if (!userclass.isAssignableFrom(ucl)) throw RuntimeException("The prototype class (" + ucl.simpleName + ") must be a subclass of the userclass parameter (" + userclass.simpleName + ")!")
cl = ucl as Class<out T>
folderName = userclass.getAnnotation(AbstractUserClass::class.java).foldername
} else throw RuntimeException("Class not registered as a user class! Use @UserClass or TBMCPlayerBase")
val sud = StaticUserData<T>(folderName)
sud.constructors[cl] = constructor
sud.constructors[userclass] =
constructor // Alawys register abstract and prototype class (TBMCPlayerBase and TBMCPlayer)
staticDataMap[userclass] = sud
}
/** /**
* Returns a player instance of the given type that represents the same player. This will return a new instance unless the player is cached.<br> * Returns the folder name for the given player class.
* If the class is a subclass of the current class then the same ID is used, otherwise, a connected ID is used, if found. *
* * @param cl The class to get the folder from (like [TBMCPlayerBase] or one of it's subclasses)
* @param cl The target player class * @return The folder name for the given type
* @return The player as a {@link T} object or null if the user doesn't have an account there * @throws RuntimeException If the class doesn't have the [UserClass] annotation.
*/ */
@SuppressWarnings("unchecked") fun <T : ChromaGamerBase?> getFolderForType(cl: Class<T>): String {
@Nullable if (cl.isAnnotationPresent(UserClass::class.java)) return cl.getAnnotation(UserClass::class.java).foldername else if (cl.isAnnotationPresent(
public final <T extends ChromaGamerBase> T getAs(Class<T> cl) { AbstractUserClass::class.java
if (cl.getSimpleName().equals(getClass().getSimpleName())) )
return (T) this; ) return cl.getAnnotation(AbstractUserClass::class.java).foldername
String newfolder = getFolderForType(cl); throw RuntimeException("Class not registered as a user class! Use @UserClass or @AbstractUserClass")
if (newfolder == null) }
throw new RuntimeException("The specified class " + cl.getSimpleName() + " isn't registered!");
if (newfolder.equals(getFolder())) // If in the same folder, the same filename is used
return getUser(getFileName(), cl);
var playerData = commonUserData.getPlayerData();
if (!playerData.contains(newfolder + "_id"))
return null;
return getUser(playerData.getString(newfolder + "_id"), cl);
}
/** /**
* This method returns the filename for this player data. For example, for Minecraft-related data, MC UUIDs, for Discord data, Discord IDs, etc.<br> * Returns the player class for the given folder name.
* <b>Does not include .yml</b> *
*/ * @param foldername The folder to get the class from (like "minecraft")
public final String getFileName() { * @return The type for the given folder name or null if not found
return commonUserData.getPlayerData().getString(getFolder() + "_id"); */
} fun getTypeForFolder(foldername: String?): Class<out ChromaGamerBase> {
synchronized(staticDataMap) {
return staticDataMap.entries.stream()
.filter { (_, value): Map.Entry<Class<out ChromaGamerBase>, StaticUserData<*>> ->
value.folder.equals(
foldername,
ignoreCase = true
)
}
.map { (key, value) -> java.util.Map.Entry.key }.findAny().orElse(null)
}
}
/** /***
* This method returns the folder that this player data is stored in. For example: "minecraft". * Retrieves a user from cache or loads it from disk.
*/ *
public final String getFolder() { * @param fname Filename without .yml, the user's identifier for that type
return getFolderForType(getClass()); * @param cl User class
} * @return The user object
*/
@JvmStatic
@Synchronized
fun <T : ChromaGamerBase> getUser(fname: String, cl: Class<T>): T {
val staticUserData: StaticUserData<*> = staticDataMap.entries
.filter { (key, _) -> key.isAssignableFrom(cl) }
.map { (_, value) -> value }
.firstOrNull()
?: throw RuntimeException("User class not registered! Use @UserClass or @AbstractUserClass")
/** @Suppress("UNCHECKED_CAST")
* Get player information. This method calls the {@link TBMCPlayerGetInfoEvent} to get all the player information across the TBMC plugins. val commonUserData: CommonUserData<T> = (staticUserData.userDataMap[fname]
* ?: run {
* @param target The {@link InfoTarget} to return the info for. val folder = staticUserData.folder
* @return The player information. val file = File(TBMC_PLAYERS_DIR + folder, "$fname.yml")
*/ file.parentFile.mkdirs()
public final String getInfo(InfoTarget target) { val playerData = YamlConfiguration.loadConfiguration(file)
TBMCPlayerGetInfoEvent event = new TBMCPlayerGetInfoEvent(this, target); playerData[staticUserData.folder + "_id"] = fname
Bukkit.getServer().getPluginManager().callEvent(event); CommonUserData<T>(playerData)
return event.getResult(); }.also { staticUserData.userDataMap[fname] = it }) as CommonUserData<T>
}
public enum InfoTarget { return if (commonUserData.userCache.containsKey(cl)) commonUserData.userCache[cl] as T
MCHover, MCCommand, Discord else {
} val obj = createNewUser(cl, staticUserData, commonUserData)
commonUserData.userCache[cl] = obj
obj
}
}
//----------------------------------------------------------------- private fun <T : ChromaGamerBase> createNewUser(
cl: Class<T>,
staticUserData: StaticUserData<*>,
commonUserData: CommonUserData<*>
): T {
@Suppress("UNCHECKED_CAST")
val obj = staticUserData.constructors[cl]?.get() as T? ?: run {
try {
cl.getConstructor().newInstance()
} catch (e: Exception) {
throw RuntimeException("Failed to create new instance of user of type ${cl.simpleName}!", e)
}
}
obj.commonUserData = commonUserData
obj.init()
obj.scheduleUncache()
return obj
}
public final ConfigData<Channel> channel = config.getData("channel", Channel.GlobalChat, /**
id -> Channel.getChannels().filter(ch -> ch.identifier.equalsIgnoreCase((String) id)).findAny().orElse(null), ch -> ch.ID); * Adds a converter to the start of the list.
*
* @param converter The converter that returns an object corresponding to the sender or null, if it's not the right type.
*/
fun <T : ChromaGamerBase?> addConverter(converter: Function<CommandSender, Optional<T>>) {
senderConverters.add(0, converter)
}
/**
* Get from the given sender. the object's type will depend on the sender's type. May be null, but shouldn't be.
*
* @param sender The sender to use
* @return A user as returned by a converter or null if none can supply it
*/
fun getFromSender(sender: CommandSender): ChromaGamerBase? { // TODO: Use Command2Sender
for (converter in senderConverters) {
val ocg = converter.apply(sender)
if (ocg.isPresent) return ocg.get()
}
return null
}
fun saveUsers() {
synchronized(staticDataMap) {
for (sud in staticDataMap.values) for (cud in sud.userDataMap.values) saveNow(cud.playerData) //Calls save()
}
}
}
} }

View file

@ -1,19 +1,13 @@
package buttondevteam.lib.player; package buttondevteam.lib.player
import lombok.Getter; import org.bukkit.configuration.file.YamlConfiguration
import lombok.RequiredArgsConstructor;
import org.bukkit.configuration.file.YamlConfiguration;
import java.util.HashMap;
/** /**
* Per user, regardless of actual type * Per user, regardless of actual type
* *
* @param <T> The user class, may be abstract * @param <T> The user class, may be abstract
*/ </T> */
@Getter class CommonUserData<T : ChromaGamerBase>(@JvmField val playerData: YamlConfiguration) {
@RequiredArgsConstructor @JvmField
public class CommonUserData<T extends ChromaGamerBase> { val userCache: HashMap<Class<out T>, out T> = HashMap()
private final HashMap<Class<? extends T>, ? extends T> userCache = new HashMap<>();
private final YamlConfiguration playerData;
} }

View file

@ -49,7 +49,7 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase {
else else
throw new RuntimeException("Class not defined as player class! Use @PlayerClass"); throw new RuntimeException("Class not defined as player class! Use @PlayerClass");
var playerData = commonUserData.getPlayerData(); var playerData = commonUserData.playerData;
var section = playerData.getConfigurationSection(pluginname); var section = playerData.getConfigurationSection(pluginname);
if (section == null) section = playerData.createSection(pluginname); if (section == null) section = playerData.createSection(pluginname);
config.reset(section); config.reset(section);
@ -76,7 +76,7 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase {
@Override @Override
protected void save() { protected void save() {
Set<String> keys = commonUserData.getPlayerData().getKeys(false); Set<String> keys = commonUserData.playerData.getKeys(false);
if (keys.size() > 1) // PlayerName is always saved, but we don't need a file for just that if (keys.size() > 1) // PlayerName is always saved, but we don't need a file for just that
super.save(); super.save();
} }