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!",
playerSupplier
) )
}, "Player not found!", playerSupplier) command2MC.addParamConverter(
command2MC.addParamConverter<Player>( Player::class.java,
Player::class.java, Function { name: String -> { name -> Bukkit.getPlayer(name) },
Bukkit.getPlayer(name) "Online player not found!",
}, "Online player not found!", playerSupplier playerSupplier
)
if (writePluginList.get()) {
try {
Files.write(File("plugins", "plugins.txt").toPath(), Iterable {
Arrays.stream(Bukkit.getPluginManager().plugins)
.map { p: Plugin -> p.dataFolder.name as CharSequence }
.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)
@Override roomJoinLeave = BroadcastTarget.add("roomJoinLeave") //Even if it's disabled, global channels continue to work
protected void register(JavaPlugin plugin) {
super.register(plugin);
roomJoinLeave = TBMCSystemChatEvent.BroadcastTarget.add("roomJoinLeave"); //Even if it's disabled, global channels continue to work
} }
@Override override fun unregister(plugin: JavaPlugin) {
protected void unregister(JavaPlugin plugin) { super.unregister(plugin)
super.unregister(plugin); BroadcastTarget.remove(roomJoinLeave)
TBMCSystemChatEvent.BroadcastTarget.remove(roomJoinLeave); roomJoinLeave = null
roomJoinLeave = null;
} }
@Override override fun enable() {}
protected void enable() { override fun disable() {}
} fun registerChannelCommand(channel: Channel) {
if (!ChromaUtils.isTest()) registerCommand(ChannelCommand(channel))
@Override
protected void disable() {
}
void registerChannelCommand(Channel channel) {
if (!ChromaUtils.isTest())
registerCommand(new ChannelCommand(channel));
} }
@CommandClass @CommandClass
@RequiredArgsConstructor private class ChannelCommand(private val channel: Channel) : ICommand2MC() {
private static class ChannelCommand extends ICommand2MC { override fun getCommandPath(): String {
private final Channel channel; return channel.identifier
@Override
public String getCommandPath() {
return channel.identifier;
} }
@Override override fun getCommandPaths(): Array<String> {
public String[] getCommandPaths() { return channel.extraIdentifiers.get().toTypedArray()
return channel.extraIdentifiers.get();
} }
@Command2.Subcommand @Subcommand
public void def(Command2MCSender senderMC, @Command2.OptionalArg @Command2.TextArg String message) { fun def(senderMC: Command2MCSender, @OptionalArg @TextArg message: String?) {
var sender = senderMC.getSender(); val sender = senderMC.sender
var user = ChromaGamerBase.getFromSender(sender); val user = ChromaGamerBase.getFromSender(sender)
if (user == null) { if (user == null) {
sender.sendMessage("§cYou can't use channels from this platform."); sender.sendMessage("§cYou can't use channels from this platform.")
return; return
} }
if (message == null) { if (message == null) {
Channel oldch = user.channel.get(); val oldch = user.channel.get()
if (oldch instanceof ChatRoom) if (oldch is ChatRoom) oldch.leaveRoom(sender)
((ChatRoom) oldch).leaveRoom(sender); if (oldch == channel) user.channel.set(Channel.GlobalChat) else {
if (oldch.equals(channel)) user.channel.set(channel)
user.channel.set(Channel.GlobalChat); if (channel is ChatRoom) channel.joinRoom(sender)
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()); sender.sendMessage("§6You are now talking in: §b" + user.channel.get().displayName.get())
} else } else TBMCChatAPI.SendChatMessage(
TBMCChatAPI.SendChatMessage(ChatMessage.builder(sender, user, message).fromCommand(true) ChatMessage.builder(sender, user, message).fromCommand(true)
.permCheck(senderMC.getPermCheck()).build(), channel); .permCheck(senderMC.permCheck).build(), channel
)
} }
} }
companion object {
var roomJoinLeave: BroadcastTarget? = null
}
} }

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

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();
} }