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>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<noprefix.version>1.0.1</noprefix.version>
<kotlin.version>1.8.10</kotlin.version>
<kotlin.version>1.8.20</kotlin.version>
</properties>
<scm>
<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.ConsoleCommandSender
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.function.Function
import java.util.function.Supplier
import java.util.logging.Logger
class MainPlugin : ButtonPlugin() {
private var logger: Logger? = null
private var economy: Economy? = null
/**
@ -45,12 +38,6 @@ class MainPlugin : ButtonPlugin() {
*/
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.
*/
@ -68,12 +55,11 @@ class MainPlugin : ButtonPlugin() {
*/
val prioritizeCustomCommands = iConfig.getData("prioritizeCustomCommands", false)
public override fun pluginEnable() {
Instance = this
instance = this
val pdf = description
logger = getLogger()
if (!setupPermissions()) throw NullPointerException("No permission plugin found!")
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()
registerComponent(this, RestartComponent())
registerComponent(this, ChannelComponent())
@ -125,31 +111,20 @@ class MainPlugin : ButtonPlugin() {
TBMCChatAPI.RegisterChatChannel(ChatRoom("§aGREEN§f", Color.Green, "green"))
TBMCChatAPI.RegisterChatChannel(ChatRoom("§bBLUE§f", Color.Blue, "blue"))
TBMCChatAPI.RegisterChatChannel(ChatRoom("§5PURPLE§f", Color.DarkPurple, "purple"))
val playerSupplier = Supplier { Bukkit.getOnlinePlayers().map { obj: Player -> obj.name }.asIterable() }
command2MC.addParamConverter(OfflinePlayer::class.java, { name: String? ->
Bukkit.getOfflinePlayer(
name!!
)
}, "Player not found!", playerSupplier)
command2MC.addParamConverter<Player>(
Player::class.java, Function { name: String ->
Bukkit.getPlayer(name)
}, "Online player not found!", playerSupplier
val playerSupplier = Supplier { Bukkit.getOnlinePlayers().map { obj -> obj.name }.asIterable() }
command2MC.addParamConverter(
OfflinePlayer::class.java,
{ name -> Bukkit.getOfflinePlayer(name) },
"Player not found!",
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
command2MC.addParamConverter(
Player::class.java,
{ name -> Bukkit.getPlayer(name) },
"Online player not found!",
playerSupplier
)
if (server.pluginManager.isPluginEnabled("Essentials")) ess = getPlugin(Essentials::class.java)
logger!!.info(pdf.name + " has been Enabled (V." + pdf.version + ") Test: " + test.get() + ".")
}
@ -182,8 +157,7 @@ class MainPlugin : ButtonPlugin() {
}
companion object {
@JvmField
var Instance: MainPlugin = null
lateinit var instance: MainPlugin
@JvmField
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.lib.TBMCSystemChatEvent;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.chat.*;
import buttondevteam.lib.player.ChromaGamerBase;
import lombok.RequiredArgsConstructor;
import org.bukkit.plugin.java.JavaPlugin;
import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils
import buttondevteam.lib.TBMCSystemChatEvent.BroadcastTarget
import buttondevteam.lib.architecture.Component
import buttondevteam.lib.chat.*
import buttondevteam.lib.chat.Command2.*
import buttondevteam.lib.player.ChromaGamerBase
import org.bukkit.plugin.java.JavaPlugin
/**
* Manages chat channels. If disabled, only global channels will be registered.
*/
public class ChannelComponent extends Component<JavaPlugin> {
static TBMCSystemChatEvent.BroadcastTarget roomJoinLeave;
class ChannelComponent : Component<MainPlugin>() {
override fun register(plugin: JavaPlugin) {
super.register(plugin)
roomJoinLeave = BroadcastTarget.add("roomJoinLeave") //Even if it's disabled, global channels continue to work
}
@Override
protected void register(JavaPlugin plugin) {
super.register(plugin);
roomJoinLeave = TBMCSystemChatEvent.BroadcastTarget.add("roomJoinLeave"); //Even if it's disabled, global channels continue to work
}
override fun unregister(plugin: JavaPlugin) {
super.unregister(plugin)
BroadcastTarget.remove(roomJoinLeave)
roomJoinLeave = null
}
@Override
protected void unregister(JavaPlugin plugin) {
super.unregister(plugin);
TBMCSystemChatEvent.BroadcastTarget.remove(roomJoinLeave);
roomJoinLeave = null;
}
override fun enable() {}
override fun disable() {}
fun registerChannelCommand(channel: Channel) {
if (!ChromaUtils.isTest()) registerCommand(ChannelCommand(channel))
}
@Override
protected void enable() {
}
@CommandClass
private class ChannelCommand(private val channel: Channel) : ICommand2MC() {
override fun getCommandPath(): String {
return channel.identifier
}
@Override
protected void disable() {
}
override fun getCommandPaths(): Array<String> {
return channel.extraIdentifiers.get().toTypedArray()
}
void registerChannelCommand(Channel channel) {
if (!ChromaUtils.isTest())
registerCommand(new ChannelCommand(channel));
}
@Subcommand
fun def(senderMC: Command2MCSender, @OptionalArg @TextArg message: String?) {
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
@RequiredArgsConstructor
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);
}
}
}
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) {
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> {
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.instance, () -> {
if (!op.hasPlayedBefore()) {
sender.sendMessage("§cCannot find player or haven't played before.");
return;

View file

@ -51,7 +51,7 @@ public class ScheduledRestartCommand extends ICommand2MC {
sender.sendMessage("Scheduled restart in " + seconds);
ScheduledServerRestartEvent e = new ScheduledServerRestartEvent(restarttime, this);
Bukkit.getPluginManager().callEvent(e);
restarttask = Bukkit.getScheduler().runTaskTimer(MainPlugin.Instance, () -> {
restarttask = Bukkit.getScheduler().runTaskTimer(MainPlugin.instance, () -> {
if (restartCounter < 0) {
restarttask.cancel();
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) {
if (Bukkit.isPrimaryThread())
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, what::get);
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.instance, what::get);
else
return what.get();
return def;

View file

@ -170,7 +170,7 @@ public class TBMCCoreAPI {
}
public static boolean IsTestServer() {
if (MainPlugin.Instance == null) return true;
return MainPlugin.Instance.test.get();
if (MainPlugin.instance == null) return true;
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 }
}
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
val `val` = value?.let { setter.apply(value) }
val `val` = value?.let { setter.apply(it) }
setInternal(`val`)
this.value = value
}
@ -89,14 +89,14 @@ class ConfigData<T> internal constructor(
val sa = config.saveAction
val root = cc.root
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
}
if (!saveTasks.containsKey(cc.root)) {
synchronized(saveTasks) {
saveTasks.put(
root,
SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, {
SaveTask(Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.instance, {
synchronized(saveTasks) {
saveTasks.remove(root)
sa.run()

View file

@ -179,7 +179,7 @@ class IHaveConfig(
.filter(Predicate<ConfigData<Any?>> { obj: ConfigData<Any?>? -> Objects.nonNull(obj) })
.collect(Collectors.toList())
} 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(
m.parameterTypes
)
@ -187,7 +187,7 @@ class IHaveConfig(
continue
}
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
)
c.get() //Saves the default value if needed - also checks validity

View file

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

View file

@ -94,7 +94,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
return false // Unknown command
}
//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 {
dispatcher.execute(results)
} catch (e: CommandSyntaxException) {
@ -103,7 +103,7 @@ abstract class Command2<TC : ICommand2<TP>, TP : Command2Sender>(
TBMCCoreAPI.SendException(
"Command execution failed for sender " + sender.name + "(" + sender.javaClass.canonicalName + ") and message " + commandline,
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))
if (mainCommandNode == null) mainCommandNode = mainNode
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) {

View file

@ -170,8 +170,9 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
val i = commandline.indexOf(' ')
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
return if ((!checkPlugin || (MainPlugin.Instance.prioritizeCustomCommands.get() == true))
|| Bukkit.getPluginCommand(mainpath)?.let { it.plugin is ButtonPlugin } != false)
return if ((!checkPlugin || (MainPlugin.instance.prioritizeCustomCommands.get() == true))
|| Bukkit.getPluginCommand(mainpath)?.let { it.plugin is ButtonPlugin } != 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 {
val user = ChromaGamerBase.getFromSender(sender)
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.")
return true
}
@ -250,9 +255,16 @@ class Command2MC : Command2<ICommand2MC, Command2MCSender>('/', true), Listener
private fun registerTabcomplete(command2MC: ICommand2MC, commandNode: LiteralCommandNode<Command2MCSender>, bukkitCommand: Command) {
if (commodore == null) {
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",
StringArgumentType.word()).suggests { context: CommandContext<Any?>?, builder: SuggestionsBuilder -> builder.suggest("untest").buildFuture() }.build()))
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",
StringArgumentType.word()
).suggests { context: CommandContext<Any?>?, builder: SuggestionsBuilder ->
builder.suggest("untest").buildFuture()
}.build()
)
)
}
commodore!!.dispatcher.root.getChild(commandNode.name) // TODO: Probably unnecessary
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(
"Error while getting command data!",
Exception("Resource not found!"),
MainPlugin.Instance
MainPlugin.instance
)
return@use
}
val config = YamlConfiguration.loadConfiguration(InputStreamReader(str))
commandConfig = config.getConfigurationSection(commandClass.canonicalName.replace('$', '.'))
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) {
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? {
val cs = commandConfig?.getConfigurationSection(method.name)
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
}
val mname = cs.getString("method")
@ -68,7 +68,7 @@ class CommandArgumentHelpManager<TC : ICommand2<TP>, TP : Command2Sender>(comman
} else TBMCCoreAPI.SendException(
"Error while getting command data for $method!",
Exception("Method '$method' != $mname or params is $params"),
MainPlugin.Instance
MainPlugin.instance
)
return null
}

View file

@ -1,317 +1,327 @@
package buttondevteam.lib.player;
package buttondevteam.lib.player
import buttondevteam.core.MainPlugin;
import buttondevteam.core.component.channel.Channel;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.IHaveConfig;
import lombok.Getter;
import lombok.val;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.YamlConfiguration;
import javax.annotation.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import buttondevteam.core.MainPlugin
import buttondevteam.core.component.channel.Channel
import buttondevteam.core.component.channel.Channel.Companion.getChannels
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.ConfigData
import buttondevteam.lib.architecture.ConfigData.Companion.saveNow
import buttondevteam.lib.architecture.IHaveConfig
import org.bukkit.Bukkit
import org.bukkit.command.CommandSender
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
import java.util.*
import java.util.function.Consumer
import java.util.function.Function
import java.util.function.Supplier
@ChromaGamerEnforcer
public abstract class ChromaGamerBase {
private static final String TBMC_PLAYERS_DIR = "TBMC/players/";
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<>();
abstract class ChromaGamerBase {
lateinit var config: IHaveConfig
/**
* Use {@link #getConfig()} where possible; the 'id' must be always set
*/
//protected YamlConfiguration plugindata;
@JvmField
protected var commonUserData: CommonUserData<*>? = null
protected open fun init() {
config.reset(commonUserData!!.playerData)
}
@Getter
protected final IHaveConfig config = new IHaveConfig(this::save);
protected CommonUserData<?> commonUserData;
protected fun updateUserConfig() {}
/**
* 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.
*/
public static <T extends ChromaGamerBase> void RegisterPluginUserClass(Class<T> userclass, Supplier<T> constructor) {
Class<? extends T> cl;
String folderName;
if (userclass.isAnnotationPresent(UserClass.class)) {
cl = userclass;
folderName = userclass.getAnnotation(UserClass.class).foldername();
} else if (userclass.isAnnotationPresent(AbstractUserClass.class)) {
var ucl = userclass.getAnnotation(AbstractUserClass.class).prototype();
if (!userclass.isAssignableFrom(ucl))
throw new RuntimeException("The prototype class (" + ucl.getSimpleName() + ") must be a subclass of the userclass parameter (" + userclass.getSimpleName() + ")!");
//noinspection unchecked
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);
}
/**
* 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
)
}
}
/**
* 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)
* @return The folder name for the given type
* @throws RuntimeException If the class doesn't have the {@link UserClass} annotation.
*/
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");
}
/**
* 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!" } }
}
/**
* Returns the player class for the given folder name.
*
* @param foldername The folder to get the class from (like "minecraft")
* @return The type for the given folder name or null if not found
*/
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);
}
}
protected open fun scheduleUncache() {
Bukkit.getScheduler().runTaskLaterAsynchronously(
MainPlugin.instance,
Runnable { uncache() },
(2 * 60 * 60 * 20).toLong()
) //2 hours
}
/***
* Retrieves a user from cache or loads it from disk.
*
* @param fname Filename without .yml, the user's identifier for that type
* @param cl User class
* @return The user object
*/
public static synchronized <T extends ChromaGamerBase> T getUser(String fname, Class<T> cl) {
StaticUserData<?> staticUserData = null;
for (var sud : staticDataMap.entrySet()) {
if (sud.getKey().isAssignableFrom(cl)) {
staticUserData = sud.getValue();
break;
}
}
if (staticUserData == null)
throw new RuntimeException("User class not registered! Use @UserClass or @AbstractUserClass");
var commonUserData = staticUserData.getUserDataMap().get(fname);
if (commonUserData == null) {
final String folder = staticUserData.getFolder();
final File file = new File(TBMC_PLAYERS_DIR + folder, fname + ".yml");
file.getParentFile().mkdirs();
var playerData = YamlConfiguration.loadConfiguration(file);
commonUserData = new CommonUserData<>(playerData);
playerData.set(staticUserData.getFolder() + "_id", fname);
staticUserData.getUserDataMap().put(fname, commonUserData);
}
if (commonUserData.getUserCache().containsKey(cl))
return (T) commonUserData.getUserCache().get(cl);
T obj;
if (staticUserData.getConstructors().containsKey(cl))
//noinspection unchecked
obj = (T) staticUserData.getConstructors().get(cl).get();
else {
try {
obj = cl.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to create new instance of user of type " + cl.getSimpleName() + "!", e);
}
}
obj.commonUserData = commonUserData;
obj.init();
obj.scheduleUncache();
return obj;
}
/**
* 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)
}
/**
* 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.
*/
public static <T extends ChromaGamerBase> void addConverter(Function<CommandSender, Optional<T>> converter) {
senderConverters.add(0, converter);
}
/**
* 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")!!
}
/**
* 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
*/
public static ChromaGamerBase getFromSender(CommandSender sender) { // TODO: Use Command2Sender
for (val converter : senderConverters) {
val ocg = converter.apply(sender);
if (ocg.isPresent())
return ocg.get();
}
return null;
}
/**
* 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
)
}
public static void saveUsers() {
synchronized (staticDataMap) {
for (var sud : staticDataMap.values())
for (var cud : sud.getUserDataMap().values())
ConfigData.saveNow(cud.getPlayerData()); //Calls save()
}
}
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)
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
}
/**
* 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);
}
}
enum class InfoTarget {
MCHover, MCCommand, Discord
}
/**
* 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!");
}
}
//-----------------------------------------------------------------
@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 }
protected void scheduleUncache() {
Bukkit.getScheduler().runTaskLaterAsynchronously(MainPlugin.Instance, this::uncache, 2 * 60 * 60 * 20); //2 hours
}
companion object {
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.
*
* @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);
}
/**
* Holds data per user class
*/
private val staticDataMap = HashMap<Class<out ChromaGamerBase>, StaticUserData<*>>()
/**
* 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");
}
/**
* 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.
*/
@JvmStatic
fun <T : ChromaGamerBase?> RegisterPluginUserClass(userclass: Class<T>, constructor: Supplier<T>?) {
val cl: Class<out T>
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>
* 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);
}
/**
* Returns the folder name for the given player class.
*
* @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
* @throws RuntimeException If the class doesn't have the [UserClass] annotation.
*/
fun <T : ChromaGamerBase?> getFolderForType(cl: Class<T>): String {
if (cl.isAnnotationPresent(UserClass::class.java)) return cl.getAnnotation(UserClass::class.java).foldername else if (cl.isAnnotationPresent(
AbstractUserClass::class.java
)
) return cl.getAnnotation(AbstractUserClass::class.java).foldername
throw RuntimeException("Class not registered as a user class! Use @UserClass or @AbstractUserClass")
}
/**
* 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");
}
/**
* Returns the player class for the given folder name.
*
* @param foldername The folder to get the class from (like "minecraft")
* @return The type for the given folder name or null if not found
*/
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".
*/
public final String getFolder() {
return getFolderForType(getClass());
}
/***
* Retrieves a user from cache or loads it from disk.
*
* @param fname Filename without .yml, the user's identifier for that type
* @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")
/**
* 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();
}
@Suppress("UNCHECKED_CAST")
val commonUserData: CommonUserData<T> = (staticUserData.userDataMap[fname]
?: run {
val folder = staticUserData.folder
val file = File(TBMC_PLAYERS_DIR + folder, "$fname.yml")
file.parentFile.mkdirs()
val playerData = YamlConfiguration.loadConfiguration(file)
playerData[staticUserData.folder + "_id"] = fname
CommonUserData<T>(playerData)
}.also { staticUserData.userDataMap[fname] = it }) as CommonUserData<T>
public enum InfoTarget {
MCHover, MCCommand, Discord
}
return if (commonUserData.userCache.containsKey(cl)) commonUserData.userCache[cl] as T
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 lombok.RequiredArgsConstructor;
import org.bukkit.configuration.file.YamlConfiguration;
import java.util.HashMap;
import org.bukkit.configuration.file.YamlConfiguration
/**
* Per user, regardless of actual type
*
* @param <T> The user class, may be abstract
*/
@Getter
@RequiredArgsConstructor
public class CommonUserData<T extends ChromaGamerBase> {
private final HashMap<Class<? extends T>, ? extends T> userCache = new HashMap<>();
private final YamlConfiguration playerData;
}
</T> */
class CommonUserData<T : ChromaGamerBase>(@JvmField val playerData: YamlConfiguration) {
@JvmField
val userCache: HashMap<Class<out T>, out T> = HashMap()
}

View file

@ -49,7 +49,7 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase {
else
throw new RuntimeException("Class not defined as player class! Use @PlayerClass");
var playerData = commonUserData.getPlayerData();
var playerData = commonUserData.playerData;
var section = playerData.getConfigurationSection(pluginname);
if (section == null) section = playerData.createSection(pluginname);
config.reset(section);
@ -76,7 +76,7 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase {
@Override
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
super.save();
}