From c365e2f3052ba0b67250313a3c0ff30dcafd0ca5 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sun, 28 Dec 2025 17:26:07 +0100 Subject: [PATCH 01/17] Start refactoring an injector system. --- playtime-core/build.gradle.kts | 3 + .../imdmk/playtime/PlayTimeApiAdapter.java | 12 +- .../github/imdmk/playtime/PlayTimeBinder.java | 77 ------- .../github/imdmk/playtime/PlayTimePlugin.java | 199 ++---------------- ...onfigBinder.java => ConfigConfigurer.java} | 15 +- .../imdmk/playtime/config/ConfigFactory.java | 3 +- .../playtime/config/ConfigLifecycle.java | 12 +- .../imdmk/playtime/config/ConfigSection.java | 37 ---- ...{ConfigManager.java => ConfigService.java} | 42 ++-- .../playtime/config/InjectorConfigBinder.java | 53 ----- .../imdmk/playtime/config/PluginConfig.java | 32 --- ...onfigurerFactory.java => YamlFactory.java} | 12 +- ...gRepresenter.java => YamlRepresenter.java} | 4 +- .../database/DatabaseConfig.java | 2 +- .../database/DatabaseConnector.java | 6 +- .../database/DatabaseManager.java | 10 +- .../database/DatabaseMode.java | 2 +- .../driver/configurer/DriverConfigurer.java | 14 ++ .../configurer/DriverConfigurerFactory.java | 34 +++ .../driver/configurer/H2Configurer.java | 4 +- .../driver/configurer/MariaDBConfigurer.java | 4 +- .../driver/configurer/MySQLConfigurer.java | 4 +- .../configurer/PostgreSQLConfigurer.java | 4 +- .../driver/configurer/SQLConfigurer.java | 4 +- .../driver/configurer/SQLiteConfigurer.java | 4 +- .../driver/library}/DriverLibraries.java | 2 +- .../driver/library/DriverLibraryLoader.java | 42 ++++ .../database/repository/Repository.java | 2 +- .../repository/RepositoryContext.java | 4 +- .../repository/RepositoryManager.java | 4 +- .../repository/ormlite/BaseDaoRepository.java | 7 +- .../repository/ormlite/EntityMapper.java | 2 +- .../repository/ormlite/EntityMeta.java | 2 +- .../feature/reload/ReloadCommand.java | 10 +- .../driver/configurer/DriverConfigurer.java | 41 ---- .../configurer/DriverConfigurerFactory.java | 53 ----- .../dependency/DriverDependencyLoader.java | 74 ------- .../infrastructure/injector/Bind.java | 25 --- .../infrastructure/module/Module.java | 85 -------- .../infrastructure/module/ModuleContext.java | 41 ---- .../module/ModuleInitializer.java | 161 -------------- .../infrastructure/module/ModuleOrdered.java | 20 -- .../infrastructure/module/ModuleRegistry.java | 97 --------- .../module/phase/CommandPhase.java | 22 -- .../infrastructure/module/phase/GuiPhase.java | 21 -- .../module/phase/ListenerPhase.java | 21 -- .../module/phase/PlaceholderPhase.java | 10 - .../module/phase/RepositoryPhase.java | 21 -- .../module/phase/TaskPhase.java | 21 -- .../imdmk/playtime/injector/Component.java | 13 ++ .../playtime/injector/ComponentContainer.java | 71 +++++++ .../injector/ComponentFunctional.java | 17 ++ .../playtime/injector/ComponentManager.java | 84 ++++++++ .../playtime/injector/ComponentScanner.java | 30 +++ .../injector/annotations/ConfigFile.java | 20 ++ .../injector/annotations/Listener.java | 18 ++ .../injector/annotations/Service.java | 17 ++ .../playtime/injector/annotations/Task.java | 16 ++ .../priority/AnnotationPriorityProvider.java | 35 +++ .../playtime/injector/priority/Priority.java | 5 + .../injector/priority/PriorityProvider.java | 10 + .../processor/AbstractComponentProcessor.java | 28 +++ .../processor/ComponentProcessor.java | 15 ++ .../processor/ComponentProcessorContext.java | 12 ++ .../FunctionalComponentProcessor.java | 51 +++++ .../user/repository/UserEntityMapper.java | 2 +- .../user/repository/UserEntityMeta.java | 2 +- .../user/repository/UserRepository.java | 2 +- .../repository/UserRepositoryOrmLite.java | 4 +- .../driver/DriverConfigurerFactoryTest.java | 4 +- .../repository/RepositoryManagerTest.java | 2 - .../ormlite/BaseDaoRepositoryTest.java | 3 +- .../repository/ormlite/EntityMapperTest.java | 1 - .../imdmk/playtime/LoaderDefaultSettings.java | 47 ----- .../github/imdmk/playtime/LoaderSettings.java | 32 --- .../playtime/PlayTimeExecutorFactory.java | 62 ------ .../imdmk/playtime/PlayTimePluginLoader.java | 32 +-- 77 files changed, 651 insertions(+), 1365 deletions(-) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeBinder.java rename playtime-core/src/main/java/com/github/imdmk/playtime/config/{ConfigBinder.java => ConfigConfigurer.java} (60%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java rename playtime-core/src/main/java/com/github/imdmk/playtime/config/{ConfigManager.java => ConfigService.java} (56%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/config/InjectorConfigBinder.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/config/PluginConfig.java rename playtime-core/src/main/java/com/github/imdmk/playtime/config/{YamlConfigurerFactory.java => YamlFactory.java} (69%) rename playtime-core/src/main/java/com/github/imdmk/playtime/config/{ConfigRepresenter.java => YamlRepresenter.java} (95%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/DatabaseConfig.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/DatabaseConnector.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/DatabaseManager.java (78%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/DatabaseMode.java (97%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurer.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurerFactory.java rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/driver/configurer/H2Configurer.java (86%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/driver/configurer/MariaDBConfigurer.java (82%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/driver/configurer/MySQLConfigurer.java (85%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/driver/configurer/PostgreSQLConfigurer.java (81%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/driver/configurer/SQLConfigurer.java (81%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/driver/configurer/SQLiteConfigurer.java (87%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure/database/driver/dependency => database/driver/library}/DriverLibraries.java (94%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraryLoader.java rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/repository/Repository.java (92%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/repository/RepositoryContext.java (86%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/repository/RepositoryManager.java (96%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/repository/ormlite/BaseDaoRepository.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/repository/ormlite/EntityMapper.java (96%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{infrastructure => }/database/repository/ormlite/EntityMeta.java (85%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurer.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurerFactory.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverDependencyLoader.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/injector/Bind.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/Module.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleContext.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleInitializer.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleOrdered.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleRegistry.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/CommandPhase.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/GuiPhase.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/ListenerPhase.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/PlaceholderPhase.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/RepositoryPhase.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/TaskPhase.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentContainer.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Listener.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java delete mode 100644 playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderDefaultSettings.java delete mode 100644 playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderSettings.java delete mode 100644 playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimeExecutorFactory.java diff --git a/playtime-core/build.gradle.kts b/playtime-core/build.gradle.kts index 918ecdb..2fdc0bb 100644 --- a/playtime-core/build.gradle.kts +++ b/playtime-core/build.gradle.kts @@ -13,6 +13,9 @@ dependencies { // Dynamic dependency loader implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT") + // Reflections + implementation("org.reflections:reflections:0.10.2") + // Multification implementation("com.eternalcode:multification-bukkit:1.2.3") implementation("com.eternalcode:multification-okaeri:1.2.3") diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java index c66e1be..bbd2196 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java @@ -7,15 +7,7 @@ @Inject record PlayTimeApiAdapter( @NotNull UserService userService, - @NotNull PlaytimeService playtimeService) implements PlayTimeApi { + @NotNull PlaytimeService playtimeService +) implements PlayTimeApi { - @Override - public @NotNull UserService userService() { - return userService; - } - - @Override - public @NotNull PlaytimeService playtimeService() { - return playtimeService; - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeBinder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeBinder.java deleted file mode 100644 index 1bacd48..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeBinder.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.infrastructure.injector.Bind; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; - -/** - * Discovers fields in {@link PlayTimePlugin} annotated with {@link Bind} - * and registers their instances into the DI {@link Resources}. - *

- * This approach keeps {@link PlayTimePlugin} focused on lifecycle/bootstrap logic - * while delegating dependency wiring to a dedicated, reflection-based binder. - * Only non-static fields with {@code @Bind} are processed. - */ -final class PlayTimeBinder { - - private final PlayTimePlugin core; - - /** - * Creates a new binder for the given plugin instance. - * - * @param core the plugin root object providing core dependencies - */ - PlayTimeBinder(@NotNull PlayTimePlugin core) { - this.core = Validator.notNull(core, "core"); - } - - /** - * Scans the {@link PlayTimePlugin} class hierarchy, locates fields annotated with - * {@link Bind}, reads their values, and registers them into the provided - * {@link Resources} instance. - * - * @param resources DI container resources to bind into - */ - void bind(@NotNull Resources resources) { - Validator.notNull(resources, "resources"); - - Class type = core.getClass(); - - while (type != null && type != Object.class) { - for (Field field : type.getDeclaredFields()) { - if (!field.isAnnotationPresent(Bind.class)) { - continue; - } - - if (Modifier.isStatic(field.getModifiers())) { - continue; - } - - field.setAccessible(true); - - final Object value; - try { - value = field.get(core); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Failed to access @BindCore field: " + field, e); - } - - if (value == null) { - throw new IllegalStateException("@BindCore field " + field + " is null during binding"); - } - - resources.on(field.getType()).assignInstance(value); - } - - type = type.getSuperclass(); - } - - // Provide Injector via lazy supplier - resources.on(Injector.class).assignInstance(() -> core.injector); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index a6311bf..2840e73 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -1,211 +1,46 @@ package com.github.imdmk.playtime; -import com.eternalcode.multification.notice.Notice; -import com.github.imdmk.playtime.config.ConfigManager; -import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.config.InjectorConfigBinder; -import com.github.imdmk.playtime.config.PluginConfig; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; -import com.github.imdmk.playtime.infrastructure.database.DatabaseManager; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; -import com.github.imdmk.playtime.infrastructure.injector.Bind; -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.ModuleContext; -import com.github.imdmk.playtime.infrastructure.module.ModuleInitializer; -import com.github.imdmk.playtime.infrastructure.module.ModuleRegistry; -import com.github.imdmk.playtime.message.MessageConfig; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.platform.events.BukkitEventCaller; -import com.github.imdmk.playtime.platform.events.BukkitListenerRegistrar; -import com.github.imdmk.playtime.platform.gui.GuiRegistry; -import com.github.imdmk.playtime.platform.litecommands.InvalidUsageHandlerImpl; -import com.github.imdmk.playtime.platform.litecommands.MissingPermissionsHandlerImpl; -import com.github.imdmk.playtime.platform.litecommands.NoticeResultHandlerImpl; +import com.github.imdmk.playtime.injector.ComponentManager; +import com.github.imdmk.playtime.injector.ServiceProcessor; +import com.github.imdmk.playtime.injector.priority.AnnotationPriorityProvider; +import com.github.imdmk.playtime.injector.scanner.DependencyScanner; import com.github.imdmk.playtime.platform.logger.BukkitPluginLogger; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapter; -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapterFactory; -import com.github.imdmk.playtime.platform.scheduler.BukkitTaskScheduler; -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.time.Durations; import com.github.imdmk.playtime.shared.validate.Validator; -import com.google.common.base.Stopwatch; -import dev.rollczi.litecommands.LiteCommands; -import dev.rollczi.litecommands.LiteCommandsBuilder; -import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; -import org.bstats.bukkit.Metrics; import org.bukkit.Server; -import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.DependencyInjection; import org.panda_lang.utilities.inject.Injector; -import java.sql.SQLException; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.ExecutorService; +import java.io.File; -/** - * Main runtime bootstrap for the PlayTime plugin. - * Threading note: heavy I/O offloaded to {@link ExecutorService}. - */ final class PlayTimePlugin { private static final String PREFIX = "AdvancedPlayTime"; private static final int PLUGIN_METRICS_ID = 19362; - @Bind private final ModuleRegistry moduleRegistry = new ModuleRegistry(); + private final Injector injector; - @Bind private final Plugin plugin; - @Bind private final PluginLogger logger; - @Bind private final Server server; - @Bind private final ExecutorService executor; + PlayTimePlugin(@NotNull Plugin plugin) { + Validator.notNull(plugin, "plugin"); - @Bind private ConfigManager configManager; - - @Bind private DatabaseManager databaseManager; - @Bind private RepositoryContext repositoryContext; - @Bind private RepositoryManager repositoryManager; - - @Bind private MessageService messageService; - @Bind private TaskScheduler taskScheduler; - @Bind private BukkitEventCaller eventCaller; - @Bind private BukkitListenerRegistrar listenerRegistrar; - @Bind private GuiRegistry guiRegistry; - @Bind private PlaceholderAdapter placeholderAdapter; - - @Bind private LiteCommandsBuilder liteCommandsBuilder; - @Bind private LiteCommands liteCommands; - - private Metrics metrics; - - Injector injector; - - PlayTimePlugin( - @NotNull Plugin plugin, - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull ExecutorService executor - ) { - this.plugin = Validator.notNull(plugin, "plugin"); - this.server = Validator.notNull(server, "server"); - this.logger = Validator.notNull(logger, "logger"); - this.executor = Validator.notNull(executor, "executorService"); - } - - PlayTimePlugin(@NotNull Plugin plugin, @NotNull ExecutorService executor) { - this(plugin, plugin.getServer(), new BukkitPluginLogger(plugin), executor); - } - - void enable( - @NotNull List> enabledConfigs, - @NotNull List> enabledModules - ) { - Validator.notNull(enabledConfigs, "enabledConfigs"); - Validator.notNull(enabledModules, "enabled modules"); - - final Stopwatch stopwatch = Stopwatch.createStarted(); - - // Configuration - configManager = new ConfigManager(logger, plugin.getDataFolder()); - configManager.createAll(enabledConfigs); - - // Duration format style - final PluginConfig pluginConfig = configManager.require(PluginConfig.class); - Durations.setDefaultFormatStyle(pluginConfig.durationFormatStyle); - - // Database - final DatabaseConfig databaseConfig = configManager.require(DatabaseConfig.class); - databaseManager = new DatabaseManager(plugin, logger, databaseConfig); - - databaseManager.loadDriver(); - try { - databaseManager.connect(); - } catch (SQLException e) { - logger.error(e, "An error occurred while trying to start all repositories. Disabling plugin..."); - plugin.getPluginLoader().disablePlugin(plugin); - throw new IllegalStateException("Repository startup failed", e); - } - - // Infrastructure services - repositoryContext = new RepositoryContext(executor); - repositoryManager = new RepositoryManager(logger); - - final MessageConfig messageConfig = configManager.require(MessageConfig.class); - messageService = new MessageService(messageConfig, BukkitAudiences.create(plugin)); - - taskScheduler = new BukkitTaskScheduler(plugin, server.getScheduler()); - eventCaller = new BukkitEventCaller(server, taskScheduler); - listenerRegistrar = new BukkitListenerRegistrar(plugin); - guiRegistry = new GuiRegistry(); - placeholderAdapter = PlaceholderAdapterFactory.createFor(plugin, server, logger); - - liteCommandsBuilder = LiteBukkitFactory.builder(PREFIX, plugin, server); - liteCommandsBuilder - .invalidUsage(new InvalidUsageHandlerImpl(messageService)) - .missingPermission(new MissingPermissionsHandlerImpl(messageService)) - .result(Notice.class, new NoticeResultHandlerImpl(messageService)); - - // Dependency Injection injector = DependencyInjection.createInjector(resources -> { - new PlayTimeBinder(this).bind(resources); - InjectorConfigBinder.bind(resources, configManager.getConfigs()); - }); - - // Module initialization - final ModuleContext context = injector.newInstance(ModuleContext.class); - final ModuleInitializer initializer = new ModuleInitializer(context, moduleRegistry, injector); - - initializer.loadAndSort(enabledModules); - initializer.bindAll(); - initializer.initAll(); - initializer.registerRepositories(); - - // Start repositories - Validator.ifNotNull(databaseManager.getConnection(), connection -> { - try { - repositoryManager.startAll(connection); - } catch (SQLException e) { - logger.error(e, "An error occurred while trying to start all repositories. Disabling plugin..."); - plugin.getPluginLoader().disablePlugin(plugin); - throw new IllegalStateException("Repository startup failed", e); - } + resources.on(Plugin.class).assignInstance(plugin); + resources.on(Server.class).assignInstance(plugin.getServer()); + resources.on(File.class).assignInstance(plugin.getDataFolder()); + resources.on(PluginLogger.class).assignInstance(new BukkitPluginLogger(plugin)); }); - // Activate all feature modules - initializer.activateFeatures(); - - // Build commands - liteCommands = liteCommandsBuilder.build(); - - // Metrics - metrics = new Metrics(plugin, PLUGIN_METRICS_ID); + ComponentManager componentManager = new ComponentManager(injector, this.getClass().getPackageName()) + .setPriorityProvider(new AnnotationPriorityProvider()) + .addProcessor(); - // API - final PlayTimeApiAdapter api = injector.newInstance(PlayTimeApiAdapter.class); - PlayTimeApiProvider.register(api); + componentManager.scanAll(); + componentManager.processAll(); - final Duration elapsed = stopwatch.stop().elapsed(); - logger.info("%s plugin enabled in %s ms", PREFIX, elapsed.toMillis()); } void disable() { - Validator.ifNotNull(configManager, (manager) -> { - manager.saveAll(); - manager.clearAll(); - }); - Validator.ifNotNull(repositoryManager, RepositoryManager::close); - Validator.ifNotNull(databaseManager, DatabaseManager::shutdown); - Validator.ifNotNull(messageService, MessageService::shutdown); - Validator.ifNotNull(taskScheduler, TaskScheduler::shutdown); - Validator.ifNotNull(liteCommands, LiteCommands::unregister); - Validator.ifNotNull(metrics, Metrics::shutdown); - - PlayTimeApiProvider.unregister(); - - logger.info("%s plugin disabled successfully.", PREFIX); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigBinder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java similarity index 60% rename from playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigBinder.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java index f0b2f6f..ad4b8aa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigBinder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.config; import com.github.imdmk.playtime.shared.validate.Validator; +import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import eu.okaeri.configs.serdes.commons.SerdesCommons; import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; @@ -8,17 +9,19 @@ import java.io.File; -final class ConfigBinder { +final class ConfigConfigurer { - void bind(@NotNull ConfigSection config, @NotNull File file) { + void configure( + @NotNull OkaeriConfig config, + @NotNull File file, + OkaeriSerdesPack... serdesPacks + ) { Validator.notNull(config, "config"); Validator.notNull(file, "file"); - final OkaeriSerdesPack serdesPack = config.getSerdesPack(); - final YamlSnakeYamlConfigurer yamlConfigurer = YamlConfigurerFactory.create(); + final YamlSnakeYamlConfigurer configurer = new YamlSnakeYamlConfigurer(YamlFactory.create()); - config.withConfigurer(yamlConfigurer) - .withSerdesPack(serdesPack) + config.withConfigurer(configurer, serdesPacks) .withSerdesPack(new SerdesCommons()) .withBindFile(file) .withRemoveOrphans(true); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java index 2327c7a..3cd9db2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java @@ -2,12 +2,13 @@ import com.github.imdmk.playtime.shared.validate.Validator; import eu.okaeri.configs.ConfigManager; +import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; final class ConfigFactory { - @NotNull T instantiate(@NotNull Class type) { + @NotNull T instantiate(@NotNull Class type) { Validator.notNull(type, "type"); try { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java index 78097f8..1a9bc9c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java @@ -2,6 +2,7 @@ import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.validate.Validator; +import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; @@ -13,12 +14,14 @@ final class ConfigLifecycle { this.logger = Validator.notNull(logger, "logger"); } - void initialize(@NotNull ConfigSection config) { + void initialize(@NotNull OkaeriConfig config) { + Validator.notNull(config, "config"); config.saveDefaults(); load(config); } - void load(@NotNull ConfigSection config) { + void load(@NotNull OkaeriConfig config) { + Validator.notNull(config, "config"); try { config.load(true); } catch (OkaeriException e) { @@ -27,10 +30,11 @@ void load(@NotNull ConfigSection config) { } } - void save(@NotNull ConfigSection config) { + void save(@NotNull OkaeriConfig config) { + Validator.notNull(config, "config"); try { config.save(); - } catch (Exception e) { + } catch (OkaeriException e) { logger.error(e, "Failed to save config %s", config.getClass().getSimpleName()); throw new ConfigAccessException(e); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java deleted file mode 100644 index 8aa1d87..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.imdmk.playtime.config; - -import eu.okaeri.configs.OkaeriConfig; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -/** - * Abstract base class for configuration sections. - * - *

- * Extends {@link OkaeriConfig} to provide a reusable foundation for plugin - * configuration sections. Subclasses are required to specify the - * serialization/deserialization pack and the configuration file name. - *

- * - *

- * Supports automatic recursive loading of nested {@link ConfigSection} - * subclasses declared as fields inside this class. - *

- */ -public abstract class ConfigSection extends OkaeriConfig { - - /** - * Returns the {@link OkaeriSerdesPack} instance used for serializing and deserializing - * this configuration section. - * - * @return non-null serialization/deserialization pack - */ - public abstract @NotNull OkaeriSerdesPack getSerdesPack(); - - /** - * Returns the filename (including extension) used to persist this configuration section. - * - * @return non-null configuration file name - */ - public abstract @NotNull String getFileName(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java similarity index 56% rename from playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigManager.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index 017ac9c..ddfa961 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -1,59 +1,59 @@ package com.github.imdmk.playtime.config; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.validate.Validator; +import eu.okaeri.configs.OkaeriConfig; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; +import org.panda_lang.utilities.inject.annotations.Inject; import java.io.File; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -public final class ConfigManager { +@Service(priority = Priority.LOWEST) +public final class ConfigService { - private final Set configs = ConcurrentHashMap.newKeySet(); - private final Map, ConfigSection> byType = new ConcurrentHashMap<>(); + private final Set configs = ConcurrentHashMap.newKeySet(); + private final Map, OkaeriConfig> byType = new ConcurrentHashMap<>(); private final File dataFolder; private final ConfigFactory factory; - private final ConfigBinder binder; + private final ConfigConfigurer configurer; private final ConfigLifecycle lifecycle; - public ConfigManager(@NotNull PluginLogger logger, @NotNull File dataFolder) { + @Inject + public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { this.dataFolder = Validator.notNull(dataFolder, "dataFolder"); this.factory = new ConfigFactory(); - this.binder = new ConfigBinder(); + this.configurer = new ConfigConfigurer(); this.lifecycle = new ConfigLifecycle(logger); } - public @NotNull T create(@NotNull Class type) { + public @NotNull T create(@NotNull Class type, @NotNull String fileName) { final T config = factory.instantiate(type); - final File file = new File(dataFolder, config.getFileName()); + final File file = new File(dataFolder, fileName); - binder.bind(config, file); + configurer.configure(config, file); lifecycle.initialize(config); register(type, config); return config; } - public void createAll(@NotNull List> types) { - types.forEach(this::create); - } - @SuppressWarnings("unchecked") - public T get(@NotNull Class type) { + public T get(@NotNull Class type) { return (T) byType.get(type); } - public @NotNull T require(@NotNull Class type) { - T config = get(type); - + public @NotNull T require(@NotNull Class type) { + final T config = get(type); if (config == null) { throw new IllegalStateException("Config not created: " + type.getName()); } @@ -69,7 +69,9 @@ public void saveAll() { configs.forEach(lifecycle::save); } - public @NotNull @Unmodifiable Set getConfigs() { + @NotNull + @Unmodifiable + public Set getConfigs() { return Collections.unmodifiableSet(configs); } @@ -78,7 +80,7 @@ public void clearAll() { byType.clear(); } - private void register(Class type, ConfigSection config) { + private void register(Class type, OkaeriConfig config) { configs.add(config); byType.put(type, config); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/InjectorConfigBinder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/InjectorConfigBinder.java deleted file mode 100644 index c1ce395..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/InjectorConfigBinder.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.imdmk.playtime.config; - -import eu.okaeri.configs.OkaeriConfig; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Resources; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.Set; - -public final class InjectorConfigBinder { - - private InjectorConfigBinder() { - throw new UnsupportedOperationException("This is utility class and cannot be instantiated."); - } - - public static void bind(@NotNull Resources resources, @NotNull Set sections) { - final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); - for (final var section : sections) { - bindRecursive(resources, section, visited); - } - } - - private static void bindRecursive(@NotNull Resources resources, @NotNull Object object, @NotNull Set visited) { - if (!visited.add(object)) { - return; - } - - resources.on(object.getClass()).assignInstance(object); - - for (Class clazz = object.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) { - for (Field field : clazz.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) { - continue; - } - - try { - field.setAccessible(true); - Object value = field.get(object); - if (value instanceof OkaeriConfig nested) { - bindRecursive(resources, nested, visited); - } - } catch (IllegalAccessException e) { - throw new IllegalStateException("Failed to bind config field: " - + clazz.getSimpleName() + "#" + field.getName(), e); - } - } - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/PluginConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/PluginConfig.java deleted file mode 100644 index b135f37..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/PluginConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime.config; - -import com.github.imdmk.playtime.shared.time.DurationFormatStyle; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -public final class PluginConfig extends ConfigSection { - - @Comment({ - "#", - "# Determines how durations (playtime, cooldowns, timers) are formatted", - "#", - "# Available options:", - "# - COMPACT: short form like 3d 4h 10m", - "# - LONG: full names, e.g. 3 days 4 hours", - "# - LONG_WITH_AND: natural flow, e.g. 3 days and 4 hours", - "# - NATURAL: comma-separated, e.g. 3 days, 4 hours", - "#" - }) - public DurationFormatStyle durationFormatStyle = DurationFormatStyle.LONG_WITH_AND; - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> {}; - } - - @Override - public @NotNull String getFileName() { - return "pluginConfig.yml"; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java similarity index 69% rename from playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlConfigurerFactory.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java index a00409a..d2e31e2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlConfigurerFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.config; -import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -8,13 +7,13 @@ import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.resolver.Resolver; -final class YamlConfigurerFactory { +final class YamlFactory { - private YamlConfigurerFactory() { + private YamlFactory() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - static YamlSnakeYamlConfigurer create() { + static Yaml create() { final LoaderOptions loader = new LoaderOptions(); loader.setAllowRecursiveKeys(false); loader.setMaxAliasesForCollections(50); @@ -26,11 +25,10 @@ static YamlSnakeYamlConfigurer create() { options.setIndent(2); options.setSplitLines(false); - final Representer representer = new ConfigRepresenter(options); + final Representer representer = new YamlRepresenter(options); final Resolver resolver = new Resolver(); - final Yaml yaml = new Yaml(constructor, representer, options, loader, resolver); - return new YamlSnakeYamlConfigurer(yaml); + return new Yaml(constructor, representer, options, loader, resolver); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigRepresenter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlRepresenter.java similarity index 95% rename from playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigRepresenter.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlRepresenter.java index 0dd3c67..0e5e41c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigRepresenter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlRepresenter.java @@ -11,9 +11,9 @@ import java.util.LinkedHashMap; import java.util.Map; -final class ConfigRepresenter extends Representer { +final class YamlRepresenter extends Representer { - ConfigRepresenter(DumperOptions options) { + YamlRepresenter(DumperOptions options) { super(options); this.representers.put(String.class, new RepresentString()); this.representers.put(Boolean.class, new RepresentBoolean()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConfig.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java index 58c6d18..11784b6 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.infrastructure.database; +package com.github.imdmk.playtime.database; import com.github.imdmk.playtime.config.ConfigSection; import eu.okaeri.configs.annotation.Comment; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConnector.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConnector.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConnector.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConnector.java index 5da685e..8cbc902 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConnector.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConnector.java @@ -1,7 +1,7 @@ -package com.github.imdmk.playtime.infrastructure.database; +package com.github.imdmk.playtime.database; -import com.github.imdmk.playtime.infrastructure.database.driver.configurer.DriverConfigurer; -import com.github.imdmk.playtime.infrastructure.database.driver.configurer.DriverConfigurerFactory; +import com.github.imdmk.playtime.database.driver.configurer.DriverConfigurer; +import com.github.imdmk.playtime.database.driver.configurer.DriverConfigurerFactory; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.validate.Validator; import com.j256.ormlite.jdbc.DataSourceConnectionSource; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseManager.java similarity index 78% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseManager.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseManager.java index 46d50e8..a8970b0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseManager.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database; +package com.github.imdmk.playtime.database; -import com.github.imdmk.playtime.infrastructure.database.driver.dependency.DriverDependencyLoader; +import com.github.imdmk.playtime.database.driver.library.DriverLibraryLoader; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.validate.Validator; import com.j256.ormlite.support.ConnectionSource; @@ -15,7 +15,7 @@ public final class DatabaseManager { private final Plugin plugin; private final DatabaseConfig config; - private final DriverDependencyLoader driverLoader; + private final DriverLibraryLoader driverLoader; private final DatabaseConnector connector; public DatabaseManager( @@ -26,12 +26,12 @@ public DatabaseManager( this.plugin = Validator.notNull(plugin, "plugin"); this.config = Validator.notNull(config, "config"); - this.driverLoader = new DriverDependencyLoader(plugin); + this.driverLoader = new DriverLibraryLoader(plugin); this.connector = new DatabaseConnector(logger, config); } public void loadDriver() { - driverLoader.loadDriverFor(config.databaseMode); + driverLoader.loadFor(config.databaseMode); } public void connect() throws SQLException { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseMode.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseMode.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java index 940faa7..3f17504 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseMode.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.infrastructure.database; +package com.github.imdmk.playtime.database; /** * Enumerates all database engines supported by the plugin. diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurer.java new file mode 100644 index 0000000..3cc8652 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurer.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.database.driver.configurer; + +import com.github.imdmk.playtime.database.DatabaseConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +public interface DriverConfigurer { + + void configure(@NotNull HikariDataSource dataSource, + @NotNull DatabaseConfig config, + @NotNull File dataFolder); +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurerFactory.java new file mode 100644 index 0000000..c2591a6 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurerFactory.java @@ -0,0 +1,34 @@ +package com.github.imdmk.playtime.database.driver.configurer; + +import com.github.imdmk.playtime.database.DatabaseMode; +import com.github.imdmk.playtime.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public final class DriverConfigurerFactory { + + private static final Map CONFIGURER_BY_MODE = Map.of( + DatabaseMode.MYSQL, new MySQLConfigurer(), + DatabaseMode.MARIADB, new MariaDBConfigurer(), + DatabaseMode.POSTGRESQL, new PostgreSQLConfigurer(), + DatabaseMode.SQLITE, new SQLiteConfigurer(), + DatabaseMode.H2, new H2Configurer(), + DatabaseMode.SQL, new SQLConfigurer() + ); + + public static @NotNull DriverConfigurer getFor(@NotNull DatabaseMode mode) { + Validator.notNull(mode, "mode cannot be null"); + + final DriverConfigurer configurer = CONFIGURER_BY_MODE.get(mode); + if (configurer == null) { + throw new IllegalArgumentException("Unsupported database mode: " + mode); + } + + return configurer; + } + + private DriverConfigurerFactory() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/H2Configurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/H2Configurer.java similarity index 86% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/H2Configurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/H2Configurer.java index 05d37a0..aa73a61 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/H2Configurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/H2Configurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.driver.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MariaDBConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MariaDBConfigurer.java similarity index 82% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MariaDBConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MariaDBConfigurer.java index 2e0d70f..abf6dc4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MariaDBConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MariaDBConfigurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.driver.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MySQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MySQLConfigurer.java similarity index 85% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MySQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MySQLConfigurer.java index 1a81ce9..45e37d6 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MySQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MySQLConfigurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.driver.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/PostgreSQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/PostgreSQLConfigurer.java similarity index 81% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/PostgreSQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/PostgreSQLConfigurer.java index 1dd14f0..c9ac4cd 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/PostgreSQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/PostgreSQLConfigurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.driver.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLConfigurer.java similarity index 81% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLConfigurer.java index 82d4fe4..aeb1b6a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLConfigurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.driver.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLiteConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLiteConfigurer.java similarity index 87% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLiteConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLiteConfigurer.java index 5f344c1..5c76fda 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLiteConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLiteConfigurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.driver.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverLibraries.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraries.java similarity index 94% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverLibraries.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraries.java index b48a4f6..7dd2a30 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverLibraries.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraries.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.dependency; +package com.github.imdmk.playtime.database.driver.library; import com.alessiodp.libby.Library; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraryLoader.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraryLoader.java new file mode 100644 index 0000000..0c5a139 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraryLoader.java @@ -0,0 +1,42 @@ +package com.github.imdmk.playtime.database.driver.library; + +import com.alessiodp.libby.BukkitLibraryManager; +import com.alessiodp.libby.Library; +import com.github.imdmk.playtime.database.DatabaseMode; +import com.github.imdmk.playtime.shared.validate.Validator; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.Map; + +public final class DriverLibraryLoader { + + private static final Map LIBRARIES_BY_MODE = Map.of( + DatabaseMode.MYSQL, DriverLibraries.MYSQL, + DatabaseMode.MARIADB, DriverLibraries.MARIADB, + DatabaseMode.SQLITE, DriverLibraries.SQLITE, + DatabaseMode.POSTGRESQL, DriverLibraries.POSTGRESQL, + DatabaseMode.H2, DriverLibraries.H2, + DatabaseMode.SQL, DriverLibraries.SQL + ); + + private final BukkitLibraryManager libraryManager; + + @Inject + public DriverLibraryLoader(@NotNull Plugin plugin) { + this.libraryManager = new BukkitLibraryManager(plugin); + this.libraryManager.addMavenCentral(); + } + + public void loadFor(@NotNull DatabaseMode mode) { + Validator.notNull(mode, "mode cannot be null"); + + final Library library = LIBRARIES_BY_MODE.get(mode); + if (library == null) { + throw new IllegalArgumentException("Unsupported database mode: " + mode); + } + + libraryManager.loadLibrary(library); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/Repository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java similarity index 92% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/Repository.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java index 97327d7..afc6e63 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/Repository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.infrastructure.database.repository; +package com.github.imdmk.playtime.database.repository; import com.j256.ormlite.support.ConnectionSource; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryContext.java similarity index 86% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryContext.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryContext.java index fddcbb3..8e3dd74 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryContext.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryContext.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.repository; +package com.github.imdmk.playtime.database.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; +import com.github.imdmk.playtime.database.repository.ormlite.BaseDaoRepository; import org.jetbrains.annotations.NotNull; import java.util.concurrent.ExecutorService; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryManager.java similarity index 96% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryManager.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryManager.java index 9258eb7..d364e06 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryManager.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.repository; +package com.github.imdmk.playtime.database.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; +import com.github.imdmk.playtime.database.repository.ormlite.BaseDaoRepository; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.validate.Validator; import com.j256.ormlite.support.ConnectionSource; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/BaseDaoRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepository.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/BaseDaoRepository.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepository.java index 065bb32..7ddac81 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/BaseDaoRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepository.java @@ -1,7 +1,7 @@ -package com.github.imdmk.playtime.infrastructure.database.repository.ormlite; +package com.github.imdmk.playtime.database.repository.ormlite; -import com.github.imdmk.playtime.infrastructure.database.repository.Repository; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; +import com.github.imdmk.playtime.database.repository.Repository; +import com.github.imdmk.playtime.database.repository.RepositoryContext; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.validate.Validator; import com.j256.ormlite.dao.Dao; @@ -61,6 +61,7 @@ protected BaseDaoRepository(@NotNull PluginLogger logger, @NotNull RepositoryCon } protected abstract Class entityClass(); + protected abstract List> entitySubClasses(); /** diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java similarity index 96% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMapper.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java index bac72a6..00c199b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.infrastructure.database.repository.ormlite; +package com.github.imdmk.playtime.database.repository.ormlite; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java similarity index 85% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMeta.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java index 37ce878..75be5a3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMeta.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.infrastructure.database.repository.ormlite; +package com.github.imdmk.playtime.database.repository.ormlite; /** * Marker interface for database entity metadata containers. diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java index 8819020..7a5dbaf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.feature.reload; -import com.github.imdmk.playtime.config.ConfigManager; +import com.github.imdmk.playtime.config.ConfigService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; @@ -19,19 +19,19 @@ public final class ReloadCommand { private final PluginLogger logger; - private final ConfigManager configManager; + private final ConfigService configService; private final TaskScheduler taskScheduler; private final MessageService messageService; @Inject public ReloadCommand( @NotNull PluginLogger logger, - @NotNull ConfigManager configManager, + @NotNull ConfigService configService, @NotNull TaskScheduler taskScheduler, @NotNull MessageService messageService ) { this.logger = Validator.notNull(logger, "logger"); - this.configManager = Validator.notNull(configManager, "configManager"); + this.configService = Validator.notNull(configService, "configManager"); this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler"); this.messageService = Validator.notNull(messageService, "messageService"); } @@ -40,7 +40,7 @@ public ReloadCommand( void reload(@Context CommandSender sender) { taskScheduler.runAsync(() -> { try { - configManager.loadAll(); + configService.loadAll(); messageService.send(sender, n -> n.reloadMessages.configReloadedSuccess()); } catch (OkaeriException e) { logger.error(e, "Failed to reload plugin configuration files"); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurer.java deleted file mode 100644 index 84621cf..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurer.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; - -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.jetbrains.annotations.NotNull; - -import java.io.File; - -/** - * Strategy interface defining how to configure a {@link HikariDataSource} - * for a specific database engine. - *

- * Implementations are responsible for: - *

    - *
  • constructing the correct JDBC URL,
  • - *
  • applying engine-specific HikariCP properties,
  • - *
  • performing any required filesystem preparation (e.g. SQLite/H2 directories).
  • - *
- * This abstraction allows {@link com.github.imdmk.playtime.infrastructure.database.DatabaseConnector} - * to remain engine-agnostic while still supporting multiple database types. - */ -public interface DriverConfigurer { - - /** - * Configures the provided {@link HikariDataSource} instance using the database - * settings supplied in {@link DatabaseConfig} and the plugin data folder. - *

- * Implementations must be deterministic and side-effect-free except for: - *

    - *
  • modifying the {@code dataSource} instance,
  • - *
  • creating required directories for file-based databases.
  • - *
- * - * @param dataSource the HikariCP data source to configure (never null) - * @param config the database configuration containing connection details (never null) - * @param dataFolder the plugin data folder, used especially for file-based engines like SQLite/H2 (never null) - */ - void configure(@NotNull HikariDataSource dataSource, - @NotNull DatabaseConfig config, - @NotNull File dataFolder); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurerFactory.java deleted file mode 100644 index 4085841..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurerFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; - -import com.github.imdmk.playtime.infrastructure.database.DatabaseMode; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; - -/** - * Factory responsible for selecting the correct {@link DriverConfigurer} - * implementation for a given {@link DatabaseMode}. - *

- * All supported drivers are registered statically in an immutable lookup table. - * This ensures fast resolution, avoids reflection, and cleanly separates - * database-specific logic into dedicated strategy classes. - *

- * The factory acts as the single entry point for retrieving driver configuration - * strategies used by {@code DatabaseConnector}. - */ -public final class DriverConfigurerFactory { - - /** Immutable lookup table mapping database modes to their respective configurers. */ - private static final Map CONFIGURER_BY_MODE = Map.of( - DatabaseMode.MYSQL, new MySQLConfigurer(), - DatabaseMode.MARIADB, new MariaDBConfigurer(), - DatabaseMode.POSTGRESQL, new PostgreSQLConfigurer(), - DatabaseMode.SQLITE, new SQLiteConfigurer(), - DatabaseMode.H2, new H2Configurer(), - DatabaseMode.SQL, new SQLConfigurer() - ); - - /** - * Returns the {@link DriverConfigurer} associated with the given {@link DatabaseMode}. - * - * @param mode the selected database engine (never null) - * @return the matching non-null {@link DriverConfigurer} - * @throws IllegalArgumentException if the mode is not supported - */ - public static @NotNull DriverConfigurer getFor(@NotNull DatabaseMode mode) { - Validator.notNull(mode, "mode cannot be null"); - - DriverConfigurer configurer = CONFIGURER_BY_MODE.get(mode); - if (configurer == null) { - throw new IllegalArgumentException("Unsupported database mode: " + mode); - } - - return configurer; - } - - private DriverConfigurerFactory() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverDependencyLoader.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverDependencyLoader.java deleted file mode 100644 index f10cf1a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverDependencyLoader.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.dependency; - -import com.alessiodp.libby.BukkitLibraryManager; -import com.alessiodp.libby.Library; -import com.github.imdmk.playtime.infrastructure.database.DatabaseMode; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; - -/** - * Loads JDBC driver libraries dynamically at runtime using Libby. - *

- * Each {@link DatabaseMode} is mapped to a specific third-party JDBC driver - * defined in {@link DriverLibraries}. This allows the plugin to ship without - * any embedded JDBC drivers and load only the required one on demand. - *

- * This component is deliberately isolated from connection logic to keep the - * database layer modular and compliant with SRP (single responsibility). - */ -public final class DriverDependencyLoader { - - /** Immutable lookup table mapping supported database modes to driver artifacts. */ - private static final Map LIBRARIES_BY_MODE = Map.of( - DatabaseMode.MYSQL, DriverLibraries.MYSQL, - DatabaseMode.MARIADB, DriverLibraries.MARIADB, - DatabaseMode.SQLITE, DriverLibraries.SQLITE, - DatabaseMode.POSTGRESQL, DriverLibraries.POSTGRESQL, - DatabaseMode.H2, DriverLibraries.H2, - DatabaseMode.SQL, DriverLibraries.SQL - ); - - private final BukkitLibraryManager libraryManager; - - /** - * Creates a new dependency loader using a pre-initialized {@link BukkitLibraryManager}. - * Maven Central is automatically added as the default repository source. - * - * @param libraryManager the library manager used to load driver JARs dynamically - */ - public DriverDependencyLoader(@NotNull BukkitLibraryManager libraryManager) { - this.libraryManager = Validator.notNull(libraryManager, "libraryManager cannot be null"); - this.libraryManager.addMavenCentral(); - } - - /** - * Convenience constructor that initializes a {@link BukkitLibraryManager} using the plugin instance. - * - * @param plugin the owning plugin instance - */ - public DriverDependencyLoader(@NotNull Plugin plugin) { - this(new BukkitLibraryManager(plugin)); - } - - /** - * Loads the JDBC driver dependency associated with the given {@link DatabaseMode}. - *

- * If the driver is already loaded, Libby will skip re-loading it automatically. - * - * @param mode the database mode requesting its driver (never null) - * @throws IllegalArgumentException if the mode has no registered driver - */ - public void loadDriverFor(@NotNull DatabaseMode mode) { - Validator.notNull(mode, "mode cannot be null"); - - Library library = LIBRARIES_BY_MODE.get(mode); - if (library == null) { - throw new IllegalArgumentException("Unsupported database mode: " + mode); - } - - libraryManager.loadLibrary(library); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/injector/Bind.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/injector/Bind.java deleted file mode 100644 index 917412d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/injector/Bind.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.injector; - -import org.panda_lang.utilities.inject.annotations.Injectable; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a field as a core - * dependency that should be automatically registered into the DI container. - *

- * Fields annotated with {@code @BindCore} are discovered by - * PlayTimeCoreBinder and exposed to - * the Panda DI {@link org.panda_lang.utilities.inject.Resources} as singleton instances. - *

- * Only non-static fields are eligible. A {@code null} value at binding time - * results in a bootstrap failure. - */ -@Injectable -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Bind { -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/Module.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/Module.java deleted file mode 100644 index 879c498..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/Module.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.GuiPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.ListenerPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.PlaceholderPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.RepositoryPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.TaskPhase; -import org.bukkit.Server; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -/** - * Base lifecycle contract for all PlayTime modules. - * - *

Lifecycle phases: - *

    - *
  1. bind(Resources) – expose and wire resources into the DI context.
  2. - *
  3. init(Injector) – initialize internal state; safe to use bound resources.
  4. - *
- * The manager guarantees {@code bind()} is called before {@code init()}.

- * - *

Threading: modules are initialized on the server main thread unless documented otherwise. - * Implementations should avoid long blocking operations in {@code bind()} and {@code init()}.

- */ -public interface Module extends ModuleOrdered { - - /** - * Binds resources into the DI container. This phase happens before {@link #init(Injector)}. - * - * @param resources DI resources registry (never {@code null}) - */ - void bind(@NotNull Resources resources); - - /** - * Initializes the module. At this point, all resources are already bound. - * - * @param injector DI injector (never {@code null}) - */ - void init(@NotNull Injector injector); - - /** - * Repositories registration phase (optional). - */ - default RepositoryPhase repositories(@NotNull Injector injector) { return repositoryManager -> {}; } - - /** - * Task scheduling phase (optional). - */ - default TaskPhase tasks(@NotNull Injector injector) { return taskScheduler -> {}; } - - /** - * Listener registration phase (optional). - */ - default ListenerPhase listeners(@NotNull Injector injector) { return listenerRegistrar -> {}; } - - /** - * Command registration phase (optional). - */ - default CommandPhase commands(@NotNull Injector injector) { return liteCommandsConfigurer -> {}; } - - /** - * Gui's registration phase (optional). - */ - default GuiPhase guis(@NotNull Injector injector) { return guiRegistry -> {}; } - - /** - * Placeholder's registration phase (optional). - */ - default PlaceholderPhase placeholders(@NotNull Injector injector) { return placeholderRegistry -> {}; } - - /** - * Final hook invoked after all registrations of this feature are complete. - * Runs on the main server thread. - */ - default void afterRegister(@NotNull Plugin plugin, @NotNull Server server, @NotNull Injector injector) {} - - /** - * Default neutral order. Lower values initialize earlier. - */ - @Override - default int order() { return 0; } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleContext.java deleted file mode 100644 index a3202c4..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleContext.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; -import com.github.imdmk.playtime.platform.events.BukkitListenerRegistrar; -import com.github.imdmk.playtime.platform.gui.GuiRegistry; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapter; -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import dev.rollczi.litecommands.LiteCommandsBuilder; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -/** - * Immutable container holding all shared services exposed to {@link Module} implementations. - * - *

Acts as a central context object passed to all module lifecycle phases, providing access to: - *

    - *
  • Bukkit plugin and server environment,
  • - *
  • logging, scheduling and repository infrastructure,
  • - *
  • listener/command/GUI registrars,
  • - *
  • placeholder adapter (PlaceholderAPI-enabled or no-op).
  • - *
- * - *

This record is created once during plugin bootstrap and reused throughout the - * module initialization pipeline.

- */ -@Inject -public record ModuleContext( - @NotNull Plugin plugin, - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull TaskScheduler taskScheduler, - @NotNull RepositoryManager repositoryManager, - @NotNull BukkitListenerRegistrar listenerRegistrar, - @NotNull LiteCommandsBuilder liteCommandsBuilder, - @NotNull GuiRegistry guiRegistry, - @NotNull PlaceholderAdapter placeholderAdapter) { -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleInitializer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleInitializer.java deleted file mode 100644 index b0f7382..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleInitializer.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; - -import java.util.List; -import java.util.function.Consumer; - -/** - * Coordinates the entire lifecycle of all {@link Module} instances. - * - *

The initializer executes modules through a strict, ordered pipeline: - *

    - *
  1. {@link #loadAndSort(List)} – instantiation and deterministic ordering,
  2. - *
  3. {@link #bindAll()} – DI resource binding phase,
  4. - *
  5. {@link #initAll()} – internal module initialization,
  6. - *
  7. {@link #registerRepositories()} – repository descriptor registration,
  8. - *
  9. {@link #activateFeatures()} – tasks, listeners, commands, GUIs, placeholders, hooks.
  10. - *
- * - *

Each step is validated against an internal state machine to enforce correct order and avoid - * partially initialized modules. All operations run exclusively on the Bukkit main thread.

- * - *

Errors thrown by individual modules never abort the lifecycle — they are logged and the - * pipeline continues for remaining modules.

- */ -public final class ModuleInitializer { - - private final ModuleContext context; - private final ModuleRegistry registry; - private final Injector injector; - - private State state = State.NEW; - - /** - * Creates a new module initializer. - * - * @param context shared runtime services accessible to modules - * @param registry module registry used for instantiation and lookup - * @param injector dependency injection container used during load/init - */ - public ModuleInitializer( - @NotNull ModuleContext context, - @NotNull ModuleRegistry registry, - @NotNull Injector injector) { - this.context = Validator.notNull(context, "context cannot be null"); - this.registry = Validator.notNull(registry, "moduleRegistry cannot be null"); - this.injector = Validator.notNull(injector, "injector cannot be null"); - } - - /** - * Instantiates and sorts all module types. - * Must be executed first in the module lifecycle. - */ - public void loadAndSort(@NotNull List> types) { - Validator.notNull(types, "types cannot be null"); - - ensureMainThread(); - ensureState(State.NEW, "loadAndSort"); - - registry.setModuleTypes(types); - registry.instantiateAndSort(injector); - - state = State.LOADED; - } - - /** - * Executes the DI binding phase for all modules. - */ - public void bindAll() { - ensureMainThread(); - ensureState(State.LOADED, "bindAll"); - - forEachModule("bindAll", m -> m.bind(injector.getResources())); - state = State.BOUND; - } - - /** - * Invokes the initialization phase for all modules. - */ - public void initAll() { - ensureMainThread(); - ensureState(State.BOUND, "initAll"); - - forEachModule("initAll", m -> m.init(injector)); - state = State.INITIALIZED; - } - - /** - * Registers repository metadata for all modules. - * Does not perform database I/O. - */ - public void registerRepositories() { - ensureMainThread(); - ensureState(State.INITIALIZED, "registerRepositories"); - - forEachModule("registerRepositories", - m -> m.repositories(injector).register(context.repositoryManager())); - - state = State.REPOS_REGISTERED; - } - - /** - * Activates all runtime features: - * tasks, listeners, commands, GUIs, placeholders, and after-register hooks. - */ - public void activateFeatures() { - ensureMainThread(); - ensureState(State.REPOS_REGISTERED, "activateFeatures"); - - forEachModule("activateFeatures", m -> { - m.tasks(injector).schedule(context.taskScheduler()); - m.listeners(injector).register(context.listenerRegistrar()); - m.commands(injector).configure(context.liteCommandsBuilder()); - m.guis(injector).register(context.guiRegistry()); - m.placeholders(injector).register(context.placeholderAdapter()); - m.afterRegister(context.plugin(), context.server(), injector); - }); - - state = State.FEATURES_ACTIVATED; - } - - /** - * Internal helper executing a phase for each module, - * catching and logging exceptions from individual modules. - */ - private void forEachModule(@NotNull String phase, @NotNull Consumer moduleConsumer) { - for (final Module m : registry.modules()) { - try { - moduleConsumer.accept(m); - } catch (Throwable t) { - context.logger().error(t, "%s phase failed for module %s", phase, m.getClass().getName()); - } - } - } - - /** Validates the current initializer state. */ - private void ensureState(@NotNull State required, @NotNull String op) { - if (state != required) { - throw new IllegalStateException(op + " requires state " + required + ", but was " + state); - } - } - - /** Ensures execution on the Bukkit main thread. */ - private void ensureMainThread() { - if (!context.server().isPrimaryThread()) { - throw new IllegalStateException("PluginModuleInitializer must run on Bukkit main thread"); - } - } - - /** Internal lifecycle states used to validate the correct execution order. */ - private enum State { - NEW, - LOADED, - BOUND, - INITIALIZED, - REPOS_REGISTERED, - FEATURES_ACTIVATED - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleOrdered.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleOrdered.java deleted file mode 100644 index 846c1a7..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleOrdered.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -/** - * Defines a simple ordering contract for modules. - * Lower values indicate higher priority (executed earlier). - * - *

Ordering is used by the module manager to produce a deterministic - * initialization sequence. When two modules return the same value, the - * manager should apply a stable tie-breaker (e.g., class name).

- */ -public interface ModuleOrdered { - - /** - * Returns the order value of this component. - * Lower values mean earlier execution. - * - * @return the order value (may be negative) - */ - int order(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleRegistry.java deleted file mode 100644 index dff4f9b..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleRegistry.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * Maintains the registry of all {@link Module} classes and their instantiated, sorted instances. - *

- * This registry is responsible for: - *

    - *
  • holding the declared module types,
  • - *
  • instantiating them via dependency injection,
  • - *
  • sorting them deterministically according to {@link Module#order()}.
  • - *
- *

- * The registry itself is stateless between runs: every call to - * {@link #instantiateAndSort(Injector)} rebuilds the internal module list from the current types. - *

- * Thread-safety: This class is not thread-safe and must be accessed from the main server thread. - */ -public final class ModuleRegistry { - - /** Comparator defining deterministic module ordering: lower {@link Module#order()} first, then by class name. */ - private static final Comparator MODULE_ORDER = Comparator - .comparingInt(Module::order) - .thenComparing(m -> m.getClass().getName()); - - private List> moduleTypes = List.of(); - private List modules = List.of(); - - /** - * Replaces the current set of module types with a new, uninitialized list. - *

- * This method does not instantiate modules; call {@link #instantiateAndSort(Injector)} afterwards - * to build and sort the instances. - * - * @param types the new list of module classes (must not be null) - * @param the module type extending {@link Module} - * @throws NullPointerException if {@code types} is null - */ - public void setModuleTypes(@NotNull List> types) { - Validator.notNull(types, "types cannot be null"); - // defensive copy - moduleTypes = List.copyOf(types); - // reset instances - modules = List.of(); - } - - /** - * Instantiates all declared module classes using the provided {@link Injector} - * and sorts them deterministically by {@link Module#order()} and class name. - *

- * This operation is idempotent for the current module types; previous instances are discarded. - * - * @param injector the dependency injector used to construct module instances (never null) - * @throws NullPointerException if {@code injector} is null - */ - public void instantiateAndSort(@NotNull Injector injector) { - Validator.notNull(injector, "injector cannot be null"); - - final List created = new ArrayList<>(moduleTypes.size()); - for (Class type : moduleTypes) { - created.add(injector.newInstance(type)); - } - - created.sort(MODULE_ORDER); - modules = List.copyOf(created); - } - - /** - * Returns an immutable, deterministically sorted view of all instantiated modules. - *

- * The returned list is guaranteed to be ordered by {@link Module#order()} ascending, - * with a lexicographic tiebreaker on the class name for consistency across JVM runs. - * - * @return an unmodifiable list of module instances (never null, may be empty) - */ - public List modules() { - return Collections.unmodifiableList(modules); - } - - /** - * Clears all registered module types and instances. - *

- * After calling this method, the registry returns to its initial empty state. - */ - public void clear() { - moduleTypes = List.of(); - modules = List.of(); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/CommandPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/CommandPhase.java deleted file mode 100644 index 7bb0447..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/CommandPhase.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import dev.rollczi.litecommands.LiteCommandsBuilder; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for command registration. - *

- * Implementations should declare and configure commands using the - * provided {@link LiteCommandsBuilder}. - */ -@FunctionalInterface -public interface CommandPhase { - - /** - * Configures and registers commands for this module. - * - * @param configurer the command configurer used to register LiteCommands commands (never {@code null}) - */ - void configure(@NotNull LiteCommandsBuilder configurer); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/GuiPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/GuiPhase.java deleted file mode 100644 index 60ce01a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/GuiPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.gui.GuiRegistry; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for GUI registration. - *

- * Implementations should register all inventory or interface GUIs - * via the provided {@link GuiRegistry}. - */ -@FunctionalInterface -public interface GuiPhase { - - /** - * Registers all GUIs provided by this module. - * - * @param guiRegistry the GUI registry used for GUI definitions and factories (never {@code null}) - */ - void register(@NotNull GuiRegistry guiRegistry); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/ListenerPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/ListenerPhase.java deleted file mode 100644 index ba7ed99..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/ListenerPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.events.BukkitListenerRegistrar; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for event listener registration. - *

- * Implementations should register Bukkit {@link org.bukkit.event.Listener}s - * using the provided {@link BukkitListenerRegistrar}. - */ -@FunctionalInterface -public interface ListenerPhase { - - /** - * Registers all Bukkit listeners for this module. - * - * @param registrar the listener registrar used to bind Bukkit event listeners (never {@code null}) - */ - void register(@NotNull BukkitListenerRegistrar registrar); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/PlaceholderPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/PlaceholderPhase.java deleted file mode 100644 index 15f50e4..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/PlaceholderPhase.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapter; -import org.jetbrains.annotations.NotNull; - -@FunctionalInterface -public interface PlaceholderPhase { - - void register(@NotNull PlaceholderAdapter placeholderAdapter); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/RepositoryPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/RepositoryPhase.java deleted file mode 100644 index ad9ab8d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/RepositoryPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for repository registration. - *

- * Implementations should declare repository descriptors only — no database I/O - * should occur during this phase. - */ -@FunctionalInterface -public interface RepositoryPhase { - - /** - * Registers repository descriptors into the {@link RepositoryManager}. - * - * @param manager the repository manager used for descriptor registration (never {@code null}) - */ - void register(@NotNull RepositoryManager manager); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/TaskPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/TaskPhase.java deleted file mode 100644 index b3da145..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/TaskPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for scheduling asynchronous or repeating tasks. - *

- * Implementations register all background or periodic tasks needed by a module - * through the provided {@link TaskScheduler}. - */ -@FunctionalInterface -public interface TaskPhase { - - /** - * Registers all scheduled tasks for this module. - * - * @param scheduler the task scheduler used to register Bukkit or async tasks (never {@code null}) - */ - void schedule(@NotNull TaskScheduler scheduler); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java new file mode 100644 index 0000000..946c780 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java @@ -0,0 +1,13 @@ +package com.github.imdmk.playtime.injector; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public record Component( + @NotNull Class type, + @NotNull A annotation +) {} + + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentContainer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentContainer.java new file mode 100644 index 0000000..bf3b4cb --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentContainer.java @@ -0,0 +1,71 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.priority.PriorityProvider; +import com.github.imdmk.playtime.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class ComponentContainer { + + private final Object lock = new Object(); + + private final EnumMap, Deque>>> componentsByPriority = new EnumMap<>(Priority.class); + + private PriorityProvider priorityProvider; + + ComponentContainer(@NotNull PriorityProvider priorityProvider) { + setPriorityProvider(priorityProvider); + + for (final Priority priority : Priority.values()) { + this.componentsByPriority.put(priority, new HashMap<>()); + } + } + + void setPriorityProvider(PriorityProvider priorityProvider) { + this.priorityProvider = Validator.notNull(priorityProvider, "priorityProvider"); + } + + void add(Component component) { + final Priority priority = this.priorityProvider.apply(component); + + synchronized (this.lock) { + this.componentsByPriority + .get(priority) + .computeIfAbsent( + component.annotation().annotationType(), + a -> new ArrayDeque<>() + ) + .addLast(component); + } + } + + List> consume(Class annotation) { + final List> result = new ArrayList<>(); + + synchronized (this.lock) { + for (final Priority priority : Priority.values()) { // Iteration order is defined by Priority enum declaration order + final Deque> queue = this.componentsByPriority.get(priority).get(annotation); + if (queue == null) { + continue; + } + + while (!queue.isEmpty()) { + result.add(queue.pollFirst()); + } + } + } + + return result; + } +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java new file mode 100644 index 0000000..11ce942 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +@FunctionalInterface +public interface ComponentFunctional { + + void accept( + @NotNull T instance, + @NotNull A annotation, + @NotNull ComponentProcessorContext context + ); +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java new file mode 100644 index 0000000..cf1c1f9 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -0,0 +1,84 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.priority.PriorityProvider; +import com.github.imdmk.playtime.injector.processor.ComponentProcessor; +import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; +import com.github.imdmk.playtime.injector.processor.FunctionalComponentProcessor; +import com.github.imdmk.playtime.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Injector; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class ComponentManager { + + private static final PriorityProvider DEFAULT_PRIORITY = (priority -> Priority.NORMAL); + + private final Injector injector; + + private final ComponentContainer container; + private final ComponentScanner scanner; + + private final Map, ComponentProcessor> processors = new ConcurrentHashMap<>(); + + public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) { + this.injector = Validator.notNull(injector, "injector"); + + this.container = new ComponentContainer(DEFAULT_PRIORITY); + this.scanner = new ComponentScanner(basePackage); + } + + public ComponentManager setPriorityProvider(@NotNull PriorityProvider provider) { + container.setPriorityProvider(provider); + return this; + } + + public ComponentManager addProcessor(@NotNull ComponentProcessor processor) { + processors.put(processor.annotation(), processor); + return this; + } + + public ComponentManager addProcessor(@NotNull Class> processorClass) { + return addProcessor(injector.newInstance(processorClass)); + } + + public ComponentManager onProcess( + @NotNull Class annotation, + @NotNull ComponentFunctional consumer + ) { + return onProcess(annotation, Object.class, consumer); + } + + public ComponentManager onProcess( + @NotNull Class annotation, + @NotNull Class targetType, + @NotNull ComponentFunctional consumer + ) { + return addProcessor(new FunctionalComponentProcessor<>(annotation, targetType, consumer)); + } + + public void scanAll() { + for (final Class annotation : processors.keySet()) { + scanner.scan(annotation).forEach(container::add); + } + } + + public void processAll() { + for (final ComponentProcessor processor : processors.values()) { + container.consume(processor.annotation()) + .forEach(component -> process(processor, component)); + } + } + + @SuppressWarnings("unchecked") + private void process( + ComponentProcessor processor, + Component component + ) { + processor.process((Component) component, new ComponentProcessorContext(injector)); + } + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java new file mode 100644 index 0000000..16a4b5a --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java @@ -0,0 +1,30 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; +import org.reflections.Reflections; + +import java.lang.annotation.Annotation; +import java.util.Set; +import java.util.stream.Collectors; + +final class ComponentScanner { + + private final Reflections reflections; + + ComponentScanner(@NotNull String basePackage) { + Validator.notNull(basePackage, "basePackage"); + this.reflections = new Reflections(basePackage); + } + + Set> scan(@NotNull Class annotation) { + Validator.notNull(annotation, "annotation"); + return reflections.getTypesAnnotatedWith(annotation).stream() + .map(type -> new Component<>( + type, + type.getAnnotation(annotation) + )) + .collect(Collectors.toSet()); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java new file mode 100644 index 0000000..f5f4688 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java @@ -0,0 +1,20 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ConfigFile { + + Priority priority() default Priority.LOWEST; + + String name(); + + Class[] serdes() default {}; +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Listener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Listener.java new file mode 100644 index 0000000..59962f4 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Listener.java @@ -0,0 +1,18 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.Component; +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Listener { + + Priority priority() default Priority.HIGHEST; + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java new file mode 100644 index 0000000..1cfa54e --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Service { + + Priority priority() default Priority.NORMAL; + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java new file mode 100644 index 0000000..a96c2eb --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Task { + + Priority priority() default Priority.NORMAL; + +} \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java new file mode 100644 index 0000000..3f3d9db --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java @@ -0,0 +1,35 @@ +package com.github.imdmk.playtime.injector.priority; + +import com.github.imdmk.playtime.injector.Component; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +public final class AnnotationPriorityProvider implements PriorityProvider { + + @Override + public Priority apply(Component component) { + final Class componentClass = component.type(); + + for (final Annotation annotation : componentClass.getAnnotations()) { + try { + final Method method = annotation.annotationType().getMethod("priority"); + + final Object value = method.invoke(annotation); + if (value instanceof Priority priority) { + return priority; + } + } + catch (NoSuchMethodException ignored) { + // doesn't support priority - skip + } + catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to resolve priority for " + componentClass.getName(), e + ); + } + } + + return Priority.NORMAL; + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java new file mode 100644 index 0000000..2ae1acb --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java @@ -0,0 +1,5 @@ +package com.github.imdmk.playtime.injector.priority; + +public enum Priority { + LOWEST, LOW, NORMAL, HIGH, HIGHEST +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java new file mode 100644 index 0000000..8c415b8 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java @@ -0,0 +1,10 @@ +package com.github.imdmk.playtime.injector.priority; + +import com.github.imdmk.playtime.injector.Component; + +import java.util.function.Function; + +@FunctionalInterface +public interface PriorityProvider extends Function, Priority> { +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java new file mode 100644 index 0000000..6a464a1 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java @@ -0,0 +1,28 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.injector.Component; +import com.github.imdmk.playtime.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public abstract class AbstractComponentProcessor + implements ComponentProcessor { + + @Override + public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { + Validator.notNull(component, "component"); + Validator.notNull(context, "context"); + + final Object instance = context.injector().newInstance(component.type()); + this.handle(instance, component.annotation(), context); + } + + protected abstract void handle( + @NotNull Object instance, + @NotNull A annotation, + @NotNull ComponentProcessorContext context + ); +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java new file mode 100644 index 0000000..8c74dac --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java @@ -0,0 +1,15 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.injector.Component; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public interface ComponentProcessor { + + @NotNull Class annotation(); + + void process(@NotNull Component component, @NotNull ComponentProcessorContext context); + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java new file mode 100644 index 0000000..ef9659e --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java @@ -0,0 +1,12 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Injector; + +public record ComponentProcessorContext(@NotNull Injector injector) { + + public ComponentProcessorContext { + Validator.notNull(injector, "injector"); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java new file mode 100644 index 0000000..59f2c14 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java @@ -0,0 +1,51 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.injector.Component; +import com.github.imdmk.playtime.injector.ComponentFunctional; +import com.github.imdmk.playtime.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public final class FunctionalComponentProcessor + implements ComponentProcessor { + + private final Class annotationType; + private final Class targetType; + private final ComponentFunctional consumer; + + public FunctionalComponentProcessor( + @NotNull Class annotationType, + @NotNull Class targetType, + @NotNull ComponentFunctional consumer + ) { + this.annotationType = Validator.notNull(annotationType, "annotationType"); + this.targetType = Validator.notNull(targetType, "targetType"); + this.consumer = Validator.notNull(consumer, "consumer"); + } + + @Override + public @NotNull Class annotation() { + return this.annotationType; + } + + @Override + @SuppressWarnings("unchecked") + public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { + Validator.notNull(component, "component"); + Validator.notNull(context, "context"); + + final Object instance = context.injector().newInstance(component.type()); + + if (!targetType.isInstance(instance)) { + return; + } + + consumer.accept( + (T) instance, + component.annotation(), + context + ); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java index 2cb2865..50384f2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.EntityMapper; +import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java index 9bdb70b..10758ea 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.EntityMeta; +import com.github.imdmk.playtime.database.repository.ormlite.EntityMeta; /** * Database metadata for the {@code advanced_playtime_users} table. diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java index a7cc1bc..d4a248e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.Repository; +import com.github.imdmk.playtime.database.repository.Repository; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserDeleteResult; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java index 0b891e5..9c2036f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; +import com.github.imdmk.playtime.database.repository.RepositoryContext; +import com.github.imdmk.playtime.database.repository.ormlite.BaseDaoRepository; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java index 8aedc8c..ea3b291 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.database.driver; -import com.github.imdmk.playtime.infrastructure.database.DatabaseMode; -import com.github.imdmk.playtime.infrastructure.database.driver.configurer.DriverConfigurerFactory; +import com.github.imdmk.playtime.database.DatabaseMode; +import com.github.imdmk.playtime.database.driver.configurer.DriverConfigurerFactory; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatCode; diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java index 498b585..1a17802 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java @@ -1,7 +1,5 @@ package com.github.imdmk.playtime.database.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.Repository; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.j256.ormlite.support.ConnectionSource; import org.junit.jupiter.api.Test; diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java index 3e34ea8..a58ddf0 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.database.repository.ormlite; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; +import com.github.imdmk.playtime.database.repository.RepositoryContext; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.j256.ormlite.dao.Dao; import org.junit.jupiter.api.Test; diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java index 8e37c4d..b36e25c 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.database.repository.ormlite; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.EntityMapper; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderDefaultSettings.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderDefaultSettings.java deleted file mode 100644 index a1d98de..0000000 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderDefaultSettings.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.config.PluginConfig; -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationModule; -import com.github.imdmk.playtime.feature.playtime.PlayTimeModule; -import com.github.imdmk.playtime.feature.reload.ReloadModule; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.message.MessageConfig; -import com.github.imdmk.playtime.platform.gui.GuiModule; -import com.github.imdmk.playtime.platform.gui.config.GuiConfig; -import com.github.imdmk.playtime.user.UserModule; -import com.github.imdmk.playtime.user.top.TopUsersCacheConfig; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * Default bootstrap settings for PlayTime: config sections and plugin modules. - */ -class LoaderDefaultSettings implements LoaderSettings { - - @Override - public @NotNull List> configSections() { - return List.of( - PluginConfig.class, - MessageConfig.class, - DatabaseConfig.class, - GuiConfig.class, - MigrationConfig.class, - TopUsersCacheConfig.class - ); - } - - @Override - public @NotNull List> pluginModules() { - return List.of( - UserModule.class, - PlayTimeModule.class, - GuiModule.class, - MigrationModule.class, - ReloadModule.class - ); - } -} diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderSettings.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderSettings.java deleted file mode 100644 index f649c45..0000000 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderSettings.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.infrastructure.module.Module; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * Defines the bootstrap configuration used by {@link PlayTimePluginLoader}. - * - *

This interface decouples the loader from concrete configuration sources, - * enabling custom setups (testing, profiling, modular distributions, etc.).

- */ -public interface LoaderSettings { - - /** - * Returns a list of all {@link ConfigSection} types that should be registered - * and loaded during plugin bootstrap. - * - * @return non-null list of configuration section classes - */ - @NotNull List> configSections(); - - /** - * Returns the ordered list of {@link Module} classes that define - * the plugin's functional modules (features, services, listeners, commands). - * - * @return non-null list of plugin module classes - */ - @NotNull List> pluginModules(); -} diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimeExecutorFactory.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimeExecutorFactory.java deleted file mode 100644 index 5be123c..0000000 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimeExecutorFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.imdmk.playtime; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.time.Duration; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; - -/** - * Factory and utilities for worker executor used by PlayTime. - */ -final class PlayTimeExecutorFactory { - - private static final String WORKER_THREAD_NAME = "AdvancedPlayTime-Worker"; - private static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(3); - - private PlayTimeExecutorFactory() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); - } - - /** - * Creates a dedicated single-threaded worker executor for asynchronous plugin operations. - * The executor uses a named daemon thread ({@code PlayTime-Worker}). - * - * @return configured single-threaded executor service - */ - static @NotNull ExecutorService newWorkerExecutor() { - ThreadFactory factory = runnable -> { - Thread thread = new Thread(runnable, WORKER_THREAD_NAME); - thread.setDaemon(true); - return thread; - }; - - return Executors.newSingleThreadExecutor(factory); - } - - /** - * Shuts down the given executor quietly, awaiting termination for a short period. - * If it fails to terminate gracefully, all running tasks are forcibly cancelled. - * - * @param executor the executor to shut down, may be {@code null} - */ - static void shutdownQuietly(@Nullable ExecutorService executor) { - if (executor == null) { - return; - } - - executor.shutdown(); - try { - if (!executor.awaitTermination(SHUTDOWN_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException ie) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } -} - diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java index a935cb8..c3a18c9 100644 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java +++ b/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java @@ -1,51 +1,23 @@ package com.github.imdmk.playtime; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.ExecutorService; - public final class PlayTimePluginLoader extends JavaPlugin { - private final ExecutorService executor; - private final LoaderSettings settings; - - private volatile PlayTimePlugin pluginCore; - - public PlayTimePluginLoader(@NotNull ExecutorService executor, @NotNull LoaderSettings settings) { - this.executor = Validator.notNull(executor, "executor"); - this.settings = Validator.notNull(settings, "settings"); - } + private PlayTimePlugin pluginCore; - public PlayTimePluginLoader() { - this(PlayTimeExecutorFactory.newWorkerExecutor(), new LoaderDefaultSettings()); - } - - /** - * Called by Bukkit when the plugin is being enabled - */ @Override public void onEnable() { final Plugin plugin = this; - - this.pluginCore = new PlayTimePlugin(plugin, executor); - this.pluginCore.enable(settings.configSections(), settings.pluginModules()); + this.pluginCore = new PlayTimePlugin(plugin); } - /** - * Called by Bukkit when the plugin is being disabled, either on server shutdown - * or via manual reload. - */ @Override public void onDisable() { if (this.pluginCore != null) { this.pluginCore.disable(); this.pluginCore = null; } - - PlayTimeExecutorFactory.shutdownQuietly(executor); } } From 213846ae970cbdcd3ec2930c9cd737c8bba81250 Mon Sep 17 00:00:00 2001 From: imDMK Date: Wed, 31 Dec 2025 16:51:03 +0100 Subject: [PATCH 02/17] Add Subscribe Events via Annotations (thanks to EternalCodeTeam). Code fixes; remove unnecessary not null Validations. --- .../github/imdmk/playtime/PlayTimeApi.java | 53 +--- .../imdmk/playtime/PlayTimeApiProvider.java | 36 +-- .../imdmk/playtime/PlayTimeService.java | 15 ++ .../imdmk/playtime/PlaytimeService.java | 59 ----- .../com/github/imdmk/playtime/user/User.java | 11 +- .../imdmk/playtime/user/UserDeleteResult.java | 24 -- .../imdmk/playtime/user/UserDeleteStatus.java | 26 -- .../imdmk/playtime/user/UserSaveReason.java | 35 --- .../imdmk/playtime/user/UserService.java | 108 +-------- .../github/imdmk/playtime/user/UserTime.java | 168 ++----------- .../imdmk/playtime/UserDeleteEvent.java | 2 +- .../imdmk/playtime/UserPreSaveEvent.java | 84 ------- .../github/imdmk/playtime/UserSaveEvent.java | 6 +- .../imdmk/playtime/PlayTimeApiAdapter.java | 9 +- .../github/imdmk/playtime/PlayTimePlugin.java | 9 +- .../playtime/config/ConfigConfigurer.java | 4 - .../imdmk/playtime/config/ConfigFactory.java | 3 - .../playtime/config/ConfigLifecycle.java | 6 +- .../imdmk/playtime/config/ConfigSection.java | 13 + .../imdmk/playtime/config/ConfigService.java | 5 +- .../database/DataSourceConnector.java | 74 ++++++ .../playtime/database/DataSourceFactory.java | 44 ++++ .../playtime/database/DatabaseBootstrap.java | 64 +++++ .../playtime/database/DatabaseConfig.java | 16 +- .../playtime/database/DatabaseConnector.java | 224 ----------------- .../playtime/database/DatabaseManager.java | 51 ---- .../imdmk/playtime/database/DatabaseMode.java | 65 +---- .../DataSourceConfigurer.java} | 5 +- .../DataSourceConfigurerFactory.java} | 15 +- .../{driver => }/configurer/H2Configurer.java | 4 +- .../configurer/MariaDBConfigurer.java | 4 +- .../configurer/MySQLConfigurer.java | 4 +- .../configurer/PostgreSQLConfigurer.java | 4 +- .../configurer/SQLConfigurer.java | 4 +- .../configurer/SQLiteConfigurer.java | 4 +- .../{driver => }/library/DriverLibraries.java | 2 +- .../library/DriverLibraryLoader.java | 7 +- .../database/repository/Repository.java | 16 -- .../repository/RepositoryContext.java | 28 --- .../repository/RepositoryManager.java | 109 --------- .../repository/ormlite/BaseDaoRepository.java | 228 ------------------ .../repository/ormlite/EntityMapper.java | 46 +--- .../repository/ormlite/EntityMeta.java | 13 +- .../repository/ormlite/OrmLiteRepository.java | 77 ++++++ .../feature/migration/MigrationConfig.java | 79 ------ .../feature/migration/MigrationModule.java | 42 ---- .../feature/migration/MigrationResult.java | 35 --- .../listener/ConfigMigrationListener.java | 21 -- .../listener/LoggerMigrationListener.java | 52 ---- .../migration/listener/MigrationListener.java | 48 ---- .../migration/migrator/PlayerMigrator.java | 36 --- .../migrator/RepositoryPlayerMigrator.java | 32 --- .../provider/BukkitPlayerProvider.java | 27 --- .../migration/provider/PlayerProvider.java | 29 --- .../runner/AsyncMigrationRunner.java | 109 --------- .../runner/BlockingMigrationRunner.java | 50 ---- .../migration/runner/MigrationRunner.java | 39 --- .../migration/runner/MigrationRunnerImpl.java | 104 -------- .../playtime/BukkitPlayTimeService.java | 89 ++----- .../feature/playtime/PlayTimeModule.java | 75 ------ .../feature/playtime/PlayTimeUserFactory.java | 25 +- .../feature/playtime/command/TimeCommand.java | 11 +- .../playtime/command/TimeResetAllCommand.java | 23 +- .../playtime/command/TimeResetCommand.java | 7 +- .../playtime/command/TimeSetCommand.java | 12 +- .../playtime/command/TimeTopCommand.java | 9 +- .../command/TimeTopInvalidateCommand.java | 5 +- .../feature/playtime/gui/PlayTimeTopGui.java | 31 +-- .../playtime/gui/PlayTimeTopGuiConfig.java | 2 + .../listener/PlayTimeSaveController.java | 42 ++++ .../listener/PlayTimeSaveListener.java | 44 ---- .../placeholder/PlayTimePlaceholder.java | 11 +- .../feature/reload/ReloadCommand.java | 9 +- .../playtime/feature/reload/ReloadModule.java | 21 -- .../playtime/injector/ComponentManager.java | 16 +- ...nentContainer.java => ComponentQueue.java} | 13 +- .../playtime/injector/ComponentScanner.java | 16 +- .../injector/annotations/ConfigFile.java | 6 +- .../{Listener.java => Controller.java} | 3 +- .../injector/annotations/NoneAnnotation.java | 14 ++ .../injector/annotations/Placeholder.java | 16 ++ .../injector/annotations/Repository.java | 16 ++ .../playtime/injector/annotations/Task.java | 2 +- .../priority/AnnotationPriorityProvider.java | 5 + .../processor/AbstractComponentProcessor.java | 4 - .../processor/ComponentProcessorContext.java | 5 - .../FunctionalComponentProcessor.java | 11 +- .../injector/subscriber/LocalPublisher.java | 64 +++++ .../injector/subscriber/Publisher.java | 12 + .../injector/subscriber/Subscribe.java | 17 ++ .../event/PlayTimeInitializeEvent.java | 4 + .../event/PlayTimeShutdownEvent.java | 4 + .../subscriber/event/SubscribeEvent.java | 4 + .../imdmk/playtime/message/MessageConfig.java | 6 +- .../playtime/message/MessageService.java | 104 +------- ...java => AdventureComponentSerializer.java} | 2 +- .../adventure/AdventureComponents.java | 121 ++-------- .../adventure/AdventureFormatter.java | 61 +---- .../adventure/AdventurePlaceholders.java | 93 +------ .../platform/event/BukkitEventCaller.java | 34 +++ .../playtime/platform/event/EventCaller.java | 10 + .../platform/events/BukkitEventCaller.java | 46 ---- .../events/BukkitListenerRegistrar.java | 65 ----- .../playtime/platform/gui/GuiModule.java | 32 --- .../playtime/platform/gui/GuiRegistry.java | 53 +--- .../imdmk/playtime/platform/gui/GuiType.java | 25 -- .../platform/gui/IdentifiableGui.java | 11 +- .../platform/gui/config/ConfigurableGui.java | 18 +- .../platform/gui/config/GuiConfig.java | 10 +- .../gui/config/NavigationBarConfig.java | 7 +- .../gui/factory/GuiBuilderFactory.java | 37 +-- .../platform/gui/factory/GuiFactory.java | 35 +-- .../playtime/platform/gui/item/ItemGui.java | 24 +- .../platform/gui/item/ItemGuiSerializer.java | 22 +- .../platform/gui/item/ItemGuiTransformer.java | 56 +---- .../item/ItemVariantPermissionResolver.java | 38 --- .../gui/item/ItemVariantResolver.java | 27 --- .../platform/gui/render/GuiRenderer.java | 12 - .../gui/render/NoPermissionPolicy.java | 13 - .../gui/render/PermissionEvaluator.java | 15 -- .../platform/gui/render/RenderContext.java | 19 +- .../platform/gui/render/RenderOptions.java | 24 +- .../gui/render/TriumphGuiRenderer.java | 154 +++--------- .../platform/gui/view/AbstractGui.java | 60 +---- .../playtime/platform/gui/view/GridSlots.java | 25 -- .../playtime/platform/gui/view/GuiOpener.java | 99 +++----- .../platform/gui/view/NavigationBar.java | 76 ++---- .../platform/gui/view/ParameterizedGui.java | 21 +- .../playtime/platform/gui/view/SimpleGui.java | 15 -- .../litecommands/InvalidUsageHandlerImpl.java | 3 +- .../MissingPermissionsHandlerImpl.java | 3 +- .../litecommands/NoticeResultHandlerImpl.java | 8 +- .../platform/logger/BukkitPluginLogger.java | 53 +--- .../placeholder/PluginPlaceholder.java | 16 -- .../adapter/PlaceholderAdapter.java | 17 -- .../adapter/PlaceholderAdapterFactory.java | 42 +--- ...apter.java => PlaceholderAdapterImpl.java} | 13 +- .../scheduler/BukkitTaskScheduler.java | 97 ++------ .../platform/scheduler/PluginTask.java | 62 ----- .../platform/scheduler/TaskScheduler.java | 129 +--------- .../shared/time/DurationFormatStyle.java | 59 ----- .../shared/time/DurationSplitter.java | 22 +- .../playtime/shared/time/DurationUnit.java | 6 +- .../imdmk/playtime/shared/time/Durations.java | 61 +---- .../playtime/shared/validate/Validator.java | 45 +--- .../imdmk/playtime/user/UserArgument.java | 9 +- .../imdmk/playtime/user/UserFactory.java | 16 +- .../imdmk/playtime/user/UserModule.java | 71 ------ .../imdmk/playtime/user/UserSaveTask.java | 65 ----- .../imdmk/playtime/user/UserServiceImpl.java | 49 ++-- .../user/cache/CaffeineUserCache.java | 37 +-- .../imdmk/playtime/user/cache/UserCache.java | 64 +---- .../user/listener/UserJoinListener.java | 18 +- .../user/listener/UserQuitListener.java | 9 +- .../playtime/user/repository/UserEntity.java | 4 +- .../user/repository/UserEntityMapper.java | 15 +- .../user/repository/UserEntityMeta.java | 18 -- .../user/repository/UserRepository.java | 69 +----- .../repository/UserRepositoryOrmLite.java | 68 +++--- .../playtime/user/top/CachedLeaderboard.java | 34 +-- .../user/top/MemoryTopUsersCache.java | 7 +- .../playtime/user/top/TopUsersCache.java | 24 -- .../user/top/TopUsersCacheConfig.java | 8 +- ...a => DataSourceConfigurerFactoryTest.java} | 10 +- .../repository/ormlite/EntityMapperTest.java | 2 +- ...ryTest.java => OrmLiteRepositoryTest.java} | 4 +- 166 files changed, 1058 insertions(+), 4784 deletions(-) create mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/PlaytimeService.java delete mode 100644 playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserPreSaveEvent.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceConnector.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceFactory.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConnector.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseManager.java rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver/configurer/DriverConfigurer.java => configurer/DataSourceConfigurer.java} (73%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver/configurer/DriverConfigurerFactory.java => configurer/DataSourceConfigurerFactory.java} (60%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/configurer/H2Configurer.java (89%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/configurer/MariaDBConfigurer.java (85%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/configurer/MySQLConfigurer.java (87%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/configurer/PostgreSQLConfigurer.java (83%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/configurer/SQLConfigurer.java (84%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/configurer/SQLiteConfigurer.java (89%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/library/DriverLibraries.java (95%) rename playtime-core/src/main/java/com/github/imdmk/playtime/database/{driver => }/library/DriverLibraryLoader.java (83%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryContext.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryManager.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepository.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationConfig.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationModule.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationResult.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/ConfigMigrationListener.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/LoggerMigrationListener.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/MigrationListener.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/PlayerMigrator.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/RepositoryPlayerMigrator.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/BukkitPlayerProvider.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/PlayerProvider.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/AsyncMigrationRunner.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/BlockingMigrationRunner.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunner.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunnerImpl.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeModule.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveListener.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadModule.java rename playtime-core/src/main/java/com/github/imdmk/playtime/injector/{ComponentContainer.java => ComponentQueue.java} (81%) rename playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/{Listener.java => Controller.java} (82%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Publisher.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Subscribe.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeInitializeEvent.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeShutdownEvent.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/SubscribeEvent.java rename playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/{ComponentSerializer.java => AdventureComponentSerializer.java} (91%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/EventCaller.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitEventCaller.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitListenerRegistrar.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiModule.java rename playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/{PlaceholderAPIAdapter.java => PlaceholderAdapterImpl.java} (85%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/PluginTask.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/UserModule.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/UserSaveTask.java rename playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/{DriverConfigurerFactoryTest.java => DataSourceConfigurerFactoryTest.java} (68%) rename playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/{BaseDaoRepositoryTest.java => OrmLiteRepositoryTest.java} (98%) diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java index 7c239fd..298bec0 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java @@ -1,59 +1,10 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.user.UserService; -import org.jetbrains.annotations.NotNull; -/** - * Central API contract for interacting with the PlayTime plugin’s core services. - * - *

This interface provides unified access to the main subsystems of the plugin:

- * - *
    - *
  • {@link UserService} – manages player data persistence, synchronization, - * and user-specific statistics.
  • - *
  • {@link PlaytimeService} – handles retrieval and manipulation of player - * playtime data.
  • - *
- * - *

External plugins can use this interface to integrate with PlayTime features - * without depending on internal implementation details. The implementation is provided - * automatically by the PlayTime plugin during runtime initialization.

- * - *

Usage Example:

- * - *
{@code
- * PlayTimeApi api = PlayTimeApiProvider.get();
- *
- * UserService userService = api.userService();
- * PlaytimeService playtimeService = api.playtimeService();
- *
- * UUID uuid = player.getUniqueId();
- * UserTime time = playtimeService.getTime(uuid);
- * }
- * - * @see PlaytimeService - * @see com.github.imdmk.playtime.user.UserService - * @see com.github.imdmk.playtime.user.UserTime - */ public interface PlayTimeApi { - /** - * Returns the {@link UserService}, which provides access to user-management operations - * such as creating, saving, and retrieving user data including playtime, - * ranks, and metadata. - * - * @return non-null {@link UserService} instance - */ - @NotNull UserService userService(); + UserService getUserService(); - /** - * Returns the {@link PlaytimeService}, which provides high-level operations for - * retrieving and modifying player playtime data. - * - *

This service acts as the bridge between the plugin’s internal user model - * and the underlying storage or platform-specific systems.

- * - * @return non-null {@link PlaytimeService} instance - */ - @NotNull PlaytimeService playtimeService(); + PlayTimeService getPlayTimeService(); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java index 0a1ff3c..526a273 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java @@ -2,11 +2,6 @@ import org.jetbrains.annotations.NotNull; -/** - * Static access point for the {@link PlayTimeApi}. - *

- * Thread-safe: publication via synchronized register/unregister and a volatile reference. - */ public final class PlayTimeApiProvider { private static volatile PlayTimeApi API; // visibility across threads @@ -15,35 +10,18 @@ private PlayTimeApiProvider() { throw new UnsupportedOperationException("This class cannot be instantiated."); } - /** - * Returns the registered {@link PlayTimeApi}. - * - * @return the registered API - * @throws IllegalStateException if the API is not registered - */ - public static @NotNull PlayTimeApi get() { - PlayTimeApi api = API; + public static PlayTimeApi get() { + final PlayTimeApi api = API; if (api == null) { throw new IllegalStateException("PlayTimeAPI is not registered."); } return api; } - /** - * Checks if the API is registered - * - * @return {@code true} if the API is registered. - */ public static boolean isRegistered() { return API != null; } - /** - * Registers the {@link PlayTimeApi} instance. - * - * @param api the API instance to register - * @throws IllegalStateException if already registered - */ static synchronized void register(@NotNull PlayTimeApi api) { if (API != null) { throw new IllegalStateException("PlayTimeAPI is already registered."); @@ -51,20 +29,10 @@ static synchronized void register(@NotNull PlayTimeApi api) { API = api; } - /** - * Forces registration of the {@link PlayTimeApi} instance. - *

- * Intended for tests/bootstrap only; overwrites any existing instance. - */ static synchronized void forceRegister(@NotNull PlayTimeApi api) { API = api; } - /** - * Unregisters the {@link PlayTimeApi}. - * - * @throws IllegalStateException if no API was registered - */ static synchronized void unregister() { if (API == null) { throw new IllegalStateException("PlayTimeAPI is not registered."); diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java new file mode 100644 index 0000000..1008372 --- /dev/null +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java @@ -0,0 +1,15 @@ +package com.github.imdmk.playtime; + +import com.github.imdmk.playtime.user.UserTime; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public interface PlayTimeService { + + UserTime getTime(@NotNull UUID uuid); + + void setTime(@NotNull UUID uuid, @NotNull UserTime time); + + void resetTime(@NotNull UUID uuid); +} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlaytimeService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlaytimeService.java deleted file mode 100644 index cb3895d..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlaytimeService.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.UserTime; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -/** - * A high-level abstraction for accessing and modifying player playtime data. - *

- * Implementations of this interface are responsible for bridging between - * the plugin domain model ({@link UserTime}) and the underlying platform’s - * data source (e.g., Bukkit statistics API, database, etc.). - *

- * Playtime is typically expressed in Minecraft ticks (20 ticks = 1 second), - * but the {@link UserTime} abstraction handles conversions to and from human-readable units. - * - * @see com.github.imdmk.playtime.user.UserTime - */ -public interface PlaytimeService { - - /** - * Retrieves the total accumulated playtime for the specified player. - * - * @param uuid - * the unique identifier of the player whose playtime should be fetched; - * must not be {@code null} - * @return - * a non-null {@link UserTime} representing the player’s total playtime. - * If no playtime is recorded or the player has never joined, returns {@link UserTime#ZERO}. - * @throws NullPointerException - * if {@code uuid} is {@code null}. - */ - @NotNull UserTime getTime(@NotNull UUID uuid); - - /** - * Sets the total playtime for the specified player to the given value. - * - * @param uuid - * the unique identifier of the player whose playtime should be updated; - * must not be {@code null} - * @param time - * the new total playtime value to assign; must not be {@code null} - * @throws NullPointerException - * if {@code uuid} or {@code time} is {@code null} - */ - void setTime(@NotNull UUID uuid, @NotNull UserTime time); - - /** - * Resets the total recorded playtime of the specified player to zero. - * - * @param uuid - * the unique identifier of the player whose playtime should be reset; - * must not be {@code null} - * @throws NullPointerException - * if {@code uuid} is {@code null} - */ - void resetTime(@NotNull UUID uuid); -} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java index ebc559a..fedad9a 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java @@ -43,10 +43,8 @@ public final class User { * @param playtime initial playtime value (never null) */ public User(@NotNull UUID uuid, @NotNull String name, @NotNull UserTime playtime) { - Objects.requireNonNull(playtime, "playtime cannot be null"); - - this.uuid = Objects.requireNonNull(uuid, "uuid cannot be null"); - this.name = Objects.requireNonNull(name, "name cannot be null"); + this.uuid = uuid; + this.name = name; this.playtimeMillis = new AtomicLong(playtime.millis()); } @@ -88,10 +86,6 @@ public String getName() { * @throws IllegalArgumentException if name is blank */ public void setName(@NotNull String name) { - Objects.requireNonNull(name, "name cannot be null"); - if (name.trim().isEmpty()) { - throw new IllegalArgumentException("name cannot be blank"); - } this.name = name; } @@ -112,7 +106,6 @@ public UserTime getPlaytime() { * @throws NullPointerException if playtime is null */ public void setPlaytime(@NotNull UserTime playtime) { - Objects.requireNonNull(playtime, "playtime cannot be null"); playtimeMillis.set(playtime.millis()); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java index 01e7fa5..7f8ee36 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java @@ -3,32 +3,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** - * Immutable result container representing the outcome of a user deletion attempt. - * - *

This record provides both contextual information: - * the {@link User} instance (if it existed and was deleted) and a - * {@link UserDeleteStatus} value describing the operation result.

- * - *

Usage: Always check {@link #status()} to determine the deletion outcome. - * {@link #user()} may be {@code null} if the user was not found or the operation failed.

- * - * @param user the deleted user instance, or {@code null} if the user did not exist or was not deleted - * @param status non-null result status representing the outcome of the deletion - * - * @see User - * @see UserDeleteStatus - */ public record UserDeleteResult(@Nullable User user, @NotNull UserDeleteStatus status) { - /** - * Indicates whether the deletion succeeded and the user actually existed. - *

- * This method is equivalent to checking: - *

{@code user != null && status == UserDeleteStatus.DELETED}
- * - * @return {@code true} if the user was successfully deleted; {@code false} otherwise - */ public boolean isSuccess() { return this.user != null && this.status == UserDeleteStatus.DELETED; } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java index 8c87147..efb33cc 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java @@ -1,33 +1,7 @@ package com.github.imdmk.playtime.user; -/** - * Enumerates all possible outcomes of a user deletion request. - * - *

This enum is typically returned as part of a {@link UserDeleteResult} - * to describe whether the deletion succeeded, the user was missing, or - * an internal failure occurred during the operation.

- * - *

Usage: Used primarily by {@code UserService} or repository - * implementations to standardize deletion responses.

- * - * @see UserDeleteResult - * @see User - */ public enum UserDeleteStatus { - - /** - * The user existed and was successfully removed from persistent storage. - */ DELETED, - - /** - * The user was not present in the data source at the time of deletion. - */ NOT_FOUND, - - /** - * The deletion operation failed due to an unexpected exception, - * connectivity issue, or database constraint violation. - */ FAILED } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java index 37e44a6..64486e2 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java @@ -1,47 +1,12 @@ package com.github.imdmk.playtime.user; -/** - * Describes the context in which a {@link User} instance is persisted. - * - *

These reasons help services, repositories, logging and auditing systems - * understand why a save operation took place.

- * - *

Typical usage: passed to {@code UserService#save(User, UserSaveReason)} - * to provide semantic context for persistence logic.

- * - * @see User - * @see UserService - */ public enum UserSaveReason { - /** - * The player joined the server — user data is loaded or created. - */ PLAYER_JOIN, - - /** - * The player left the server — user data should be persisted. - */ PLAYER_LEAVE, - /** - * An administrator explicitly set the user's playtime via command. - */ SET_COMMAND, - - /** - * An administrator reset the user's playtime via command. - */ RESET_COMMAND, - /** - * The user's data was persisted by a scheduled task - * (e.g., automatic save every 5 minutes). - */ - SCHEDULED_SAVE, - - /** - * The user's playtime was reset by a GUI action (e.g., button click). - */ GUI_RESET_CLICK } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java index b3ab8e5..ebc38ec 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java @@ -8,109 +8,19 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -/** - * High-level service for accessing and managing {@link User} data. - *

- * Provides both cache-only (synchronous, safe for main-thread use) - * and asynchronous (database-backed) operations. - *

- * Implementations are expected to handle caching, persistence, and - * consistency automatically. - */ public interface UserService { - /** - * Finds a user by their unique UUID from the in-memory cache only. - *

- * This method is non-blocking and safe to call from the main server thread. - * - * @param uuid the user's UUID - * @return an {@link Optional} containing the user if present in cache, - * or empty if not found - */ - @NotNull Optional findCachedByUuid(@NotNull UUID uuid); + Optional findCachedByUuid(@NotNull UUID uuid); + Optional findCachedByName(@NotNull String name); + Collection getCachedUsers(); - /** - * Finds a user by their name from the in-memory cache only. - *

- * This method is non-blocking and safe to call from the main server thread. - * - * @param name the user's name (case-insensitive, depending on implementation) - * @return an {@link Optional} containing the user if present in cache, - * or empty if not found - */ - @NotNull Optional findCachedByName(@NotNull String name); + CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture> findByName(@NotNull String name); - /** - * Returns an unmodifiable snapshot of all users currently cached in memory. - *

- * This collection reflects a moment-in-time view and is not updated dynamically. - * Safe to call from the main thread. - * - * @return a collection of cached {@link User} objects - */ - @NotNull Collection getCachedUsers(); + CompletableFuture deleteByUuid(@NotNull UUID uuid); + CompletableFuture deleteByName(@NotNull String name); - /** - * Asynchronously finds a user by their UUID, using cache as the primary source - * and the database as fallback. - * - * @param uuid the user's UUID - * @return a {@link CompletableFuture} containing an {@link Optional} user - * when the lookup completes - */ - @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason); - /** - * Asynchronously finds a user by their name, using cache as the primary source - * and the database as fallback. - * - * @param name the user's name (case-insensitive, depending on implementation) - * @return a {@link CompletableFuture} containing an {@link Optional} user - * when the lookup completes - */ - @NotNull CompletableFuture> findByName(@NotNull String name); - - /** - * Retrieves a list of the top users sorted by spent playtime in descending order. - *

- * The result may come from the in-memory - * cached leaderboard or trigger an asynchronous refresh when the cache has expired - * or does not satisfy the requested limit. - *

- * - * @param limit the maximum number of users to return; values ≤ 0 yield an empty list - * @return a {@link CompletableFuture} that completes with a list of users ordered - * by spent time descending, either from cache or freshly queried from the repository - */ - @NotNull CompletableFuture> findTopByPlayTime(int limit); - - /** - * Asynchronously saves a user to the underlying database and updates the cache. - *

- * If the user already exists, their data is updated. - * - * @param user the user to save - * @param reason the reason of save - * @return a {@link CompletableFuture} containing the saved user - */ - @NotNull CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason); - - /** - * Asynchronously deletes a user by their UUID from both the database and cache. - * - * @param uuid the UUID of the user to delete - * @return a {@link CompletableFuture} completing with {@code true} if the user was deleted, - * or {@code false} if no such user existed - */ - @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid); - - /** - * Asynchronously deletes a user by their name from both the database and cache. - * - * @param name the name of the user to delete - * @return a {@link CompletableFuture} completing with {@code true} if the user was deleted, - * or {@code false} if no such user existed - */ - @NotNull CompletableFuture deleteByName(@NotNull String name); + CompletableFuture> findTopByPlayTime(int limit); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java index 2dc8502..117055a 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java @@ -9,219 +9,91 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; -/** - * Immutable value object representing a duration of time measured in milliseconds. - * - *

This record provides convenient conversions between milliseconds, seconds, - * Bukkit ticks (1 tick = 50 ms), and {@link Duration}, as well as arithmetic - * and comparison utilities for working with user playtime or uptime data.

- * - *

Design notes:

- *
    - *
  • This class enforces non-negative values — negative durations are not allowed.
  • - *
  • All operations return new immutable instances; this class is thread-safe and - * safe for concurrent use.
  • - *
  • Overflow conditions in arithmetic methods trigger {@link ArithmeticException} - * to prevent silent wrap-around.
  • - *
- * - * @param millis the milliseconds of time - * - * @see Duration - * @see User - */ public record UserTime(long millis) implements Comparable, Serializable { @Serial private static final long serialVersionUID = 1L; - /** Constant representing zero time. */ public static final UserTime ZERO = new UserTime(0L); - /** Number of milliseconds in a single Bukkit tick (20 ticks = 1 second). */ private static final long MILLIS_PER_TICK = 50L; - - /** Number of milliseconds in one second. */ private static final long MILLIS_PER_SECOND = 1_000L; - /** - * Primary constructor that validates the provided time value. - * - * @param millis total duration in milliseconds (must be ≥ 0) - * @throws IllegalArgumentException if {@code millis} is negative - */ public UserTime { if (millis < 0L) { throw new IllegalArgumentException("UserTime millis cannot be negative"); } } - /** - * Creates a {@code UserTime} from raw milliseconds. - * - * @param millis total milliseconds - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofMillis(long millis) { return new UserTime(millis); } - /** - * Creates a {@code UserTime} from duration. - * - * @param duration a duration - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofDuration(@NotNull Duration duration) { - Objects.requireNonNull(duration, "duration cannot be null"); return ofMillis(duration.toMillis()); } - /** - * Creates a {@code UserTime} from seconds. - * - * @param seconds total seconds - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofSeconds(long seconds) { return new UserTime(seconds * MILLIS_PER_SECOND); } - /** - * Creates a {@code UserTime} from Bukkit ticks (1 tick = 50 ms). - * - * @param ticks total ticks - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofTicks(long ticks) { return new UserTime(ticks * MILLIS_PER_TICK); } - /** - * Creates a {@code UserTime} from a {@link Duration}. - * - * @param duration non-null duration to convert - * @return a new {@code UserTime} instance - * @throws NullPointerException if {@code duration} is null - */ @Contract("_ -> new") public static @NotNull UserTime from(@NotNull Duration duration) { - Objects.requireNonNull(duration, "duration cannot be null"); return new UserTime(duration.toMillis()); } - /** - * Converts this time to whole seconds (truncated). - * - * @return number of seconds contained in this duration - */ @Contract(pure = true) public long toSeconds() { - return TimeUnit.MILLISECONDS.toSeconds(this.millis); + return TimeUnit.MILLISECONDS.toSeconds(millis); } - /** - * Converts this time to Bukkit ticks (1 tick = 50 ms). - * - * @return total number of ticks represented by this time - */ @Contract(pure = true) public int toTicks() { - return Math.toIntExact(this.millis / MILLIS_PER_TICK); + return Math.toIntExact(millis / MILLIS_PER_TICK); } - /** - * Converts this instance to a {@link Duration}. - * - * @return a duration representing the same amount of time - */ @Contract(pure = true) public @NotNull Duration toDuration() { - return Duration.ofMillis(this.millis); + return Duration.ofMillis(millis); } - /** - * Returns whether this time equals zero. - * - * @return {@code true} if this duration represents zero milliseconds - */ @Contract(pure = true) public boolean isZero() { - return this.millis == 0L; + return millis == 0; } - /** - * Adds another {@code UserTime} to this one. - * - * @param other non-null {@code UserTime} to add - * @return new {@code UserTime} representing the sum - * @throws NullPointerException if {@code other} is null - * @throws ArithmeticException if overflow occurs - */ @Contract(pure = true) - public @NotNull UserTime plus(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return new UserTime(Math.addExact(this.millis, other.millis)); - } - - /** - * Subtracts another {@code UserTime} from this one. - * - * @param other non-null {@code UserTime} to subtract - * @return new {@code UserTime} representing the difference - * @throws NullPointerException if {@code other} is null - * @throws ArithmeticException if overflow occurs - */ + public UserTime plus(@NotNull UserTime other) { + return new UserTime(Math.addExact(millis, other.millis)); + } + @Contract(pure = true) - public @NotNull UserTime minus(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return new UserTime(Math.subtractExact(this.millis, other.millis)); + public UserTime minus(@NotNull UserTime other) { + return new UserTime(Math.subtractExact(millis, other.millis)); } - /** - * Returns the smaller of this and the given time. - * - * @param other non-null time to compare - * @return the smaller {@code UserTime} instance - */ @Contract(pure = true) - public @NotNull UserTime min(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return this.millis <= other.millis ? this : other; + public UserTime min(@NotNull UserTime other) { + return millis <= other.millis ? this : other; } - /** - * Returns the larger of this and the given time. - * - * @param other non-null time to compare - * @return the larger {@code UserTime} instance - */ @Contract(pure = true) - public @NotNull UserTime max(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return this.millis >= other.millis ? this : other; + public UserTime max(@NotNull UserTime other) { + return millis >= other.millis ? this : other; } - /** - * Compares this {@code UserTime} with another by their millisecond values. - * - * @param o other {@code UserTime} to compare against - * @return negative if this is less, zero if equal, positive if greater - */ @Override public int compareTo(@NotNull UserTime o) { - return Long.compare(this.millis, o.millis); + return Long.compare(millis, o.millis); } - /** - * Checks equality based on millisecond value. - * - * @param o object to compare - * @return {@code true} if the given object is a {@code UserTime} with the same millisecond value - */ @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; @@ -229,21 +101,11 @@ public boolean equals(Object o) { return compareTo(userTime) == 0; } - /** - * Returns a hash code consistent with {@link #equals(Object)}. - * - * @return hash based on the millisecond value - */ @Override public int hashCode() { return Objects.hashCode(millis); } - /** - * Returns a concise string representation suitable for logging or debugging. - * - * @return string in the format {@code "UserTime{millis=X}"} - */ @Override @NotNull public String toString() { diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java index 4459b8b..1cc880c 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java @@ -29,7 +29,7 @@ public final class UserDeleteEvent extends Event { */ public UserDeleteEvent(@NotNull UserDeleteResult result) { super(ASYNC); - this.result = Objects.requireNonNull(result, "result cannot be null"); + this.result = Objects.requireNonNull(result, "result"); } /** diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserPreSaveEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserPreSaveEvent.java deleted file mode 100644 index 3b4ebd8..0000000 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserPreSaveEvent.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -/** - * Event fired immediately before a {@link User} instance is persisted - * to the database by the plugin. - * - *

This event serves as a pre-save hook, allowing listeners to - * inspect or modify the {@link User} object before it is written to storage. - * Any changes made to the user during this event will be included in the - * final persisted representation.

- * - *

Note: This event is not cancellable. It is strictly intended - * for mutation or inspection of the user object before saving. If cancellation - * behavior is required in the future, a dedicated cancellable event should be introduced.

- * - *

Thread safety: This event is always fired synchronously on the - * main server thread.

- */ -public final class UserPreSaveEvent extends Event { - - private static final HandlerList HANDLERS = new HandlerList(); - private static final boolean ASYNC = false; - - private final User user; - private final UserSaveReason reason; - - /** - * Creates a new {@code UserPreSaveEvent}. - * - * @param user the user about to be saved (non-null) - * @param reason the context in which the save operation was triggered (non-null) - */ - public UserPreSaveEvent(@NotNull User user, @NotNull UserSaveReason reason) { - super(ASYNC); - this.user = Objects.requireNonNull(user, "user cannot be null"); - this.reason = Objects.requireNonNull(reason, "reason cannot be null"); - } - - /** - * Returns the {@link User} instance that will be persisted. - *

Modifying this object will affect the data written to storage.

- * - * @return the user associated with this event - */ - public @NotNull User getUser() { - return this.user; - } - - /** - * Returns the reason for the save operation. - * - * @return a {@link UserSaveReason} describing why the user is being saved - */ - public @NotNull UserSaveReason getReason() { - return this.reason; - } - - /** - * Returns the static handler list for this event type. - * - * @return the handler list of this event. - */ - @Override - public @NotNull HandlerList getHandlers() { - return HANDLERS; - } - - /** - * Returns the static handler list for this event type. - * - * @return the list of handlers for this event. - */ - public static @NotNull HandlerList getHandlerList() { - return HANDLERS; - } -} diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java index 35befe5..78402b6 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java @@ -20,7 +20,7 @@ public final class UserSaveEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); - private static final boolean ASYNC = false; + private static final boolean ASYNC = true; private final User user; private final UserSaveReason reason; @@ -33,8 +33,8 @@ public final class UserSaveEvent extends Event { */ public UserSaveEvent(@NotNull User user, @NotNull UserSaveReason reason) { super(ASYNC); - this.user = Objects.requireNonNull(user, "user cannot be null"); - this.reason = Objects.requireNonNull(reason, "reason cannot be null"); + this.user = user; + this.reason = reason; } /** diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java index bbd2196..ece0e34 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java @@ -1,13 +1,10 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.user.UserService; -import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; @Inject record PlayTimeApiAdapter( - @NotNull UserService userService, - @NotNull PlaytimeService playtimeService -) implements PlayTimeApi { - -} + UserService getUserService, + PlayTimeService getPlayTimeService +) implements PlayTimeApi {} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index 2840e73..1b822bb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -1,12 +1,9 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.injector.ComponentManager; -import com.github.imdmk.playtime.injector.ServiceProcessor; import com.github.imdmk.playtime.injector.priority.AnnotationPriorityProvider; -import com.github.imdmk.playtime.injector.scanner.DependencyScanner; import com.github.imdmk.playtime.platform.logger.BukkitPluginLogger; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.Server; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; @@ -17,14 +14,12 @@ final class PlayTimePlugin { - private static final String PREFIX = "AdvancedPlayTime"; - private static final int PLUGIN_METRICS_ID = 19362; + //private static final String PREFIX = "AdvancedPlayTime"; + //private static final int PLUGIN_METRICS_ID = 19362; private final Injector injector; PlayTimePlugin(@NotNull Plugin plugin) { - Validator.notNull(plugin, "plugin"); - injector = DependencyInjection.createInjector(resources -> { resources.on(Plugin.class).assignInstance(plugin); resources.on(Server.class).assignInstance(plugin.getServer()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java index ad4b8aa..5e28154 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.config; -import com.github.imdmk.playtime.shared.validate.Validator; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import eu.okaeri.configs.serdes.commons.SerdesCommons; @@ -16,9 +15,6 @@ void configure( @NotNull File file, OkaeriSerdesPack... serdesPacks ) { - Validator.notNull(config, "config"); - Validator.notNull(file, "file"); - final YamlSnakeYamlConfigurer configurer = new YamlSnakeYamlConfigurer(YamlFactory.create()); config.withConfigurer(configurer, serdesPacks) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java index 3cd9db2..aa40829 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.config; -import com.github.imdmk.playtime.shared.validate.Validator; import eu.okaeri.configs.ConfigManager; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; @@ -9,8 +8,6 @@ final class ConfigFactory { @NotNull T instantiate(@NotNull Class type) { - Validator.notNull(type, "type"); - try { return ConfigManager.create(type); } catch (OkaeriException e) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java index 1a9bc9c..edf6e6c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.config; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; @@ -11,17 +10,15 @@ final class ConfigLifecycle { private final PluginLogger logger; ConfigLifecycle(@NotNull PluginLogger logger) { - this.logger = Validator.notNull(logger, "logger"); + this.logger = logger; } void initialize(@NotNull OkaeriConfig config) { - Validator.notNull(config, "config"); config.saveDefaults(); load(config); } void load(@NotNull OkaeriConfig config) { - Validator.notNull(config, "config"); try { config.load(true); } catch (OkaeriException e) { @@ -31,7 +28,6 @@ void load(@NotNull OkaeriConfig config) { } void save(@NotNull OkaeriConfig config) { - Validator.notNull(config, "config"); try { config.save(); } catch (OkaeriException e) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java new file mode 100644 index 0000000..2a6cfa8 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java @@ -0,0 +1,13 @@ +package com.github.imdmk.playtime.config; + +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import org.jetbrains.annotations.NotNull; + +public abstract class ConfigSection extends OkaeriConfig { + + public abstract @NotNull OkaeriSerdesPack serdesPack(); + + public abstract @NotNull String fileName(); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index ddfa961..c89de31 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -3,7 +3,6 @@ import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import eu.okaeri.configs.OkaeriConfig; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; @@ -29,7 +28,7 @@ public final class ConfigService { @Inject public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { - this.dataFolder = Validator.notNull(dataFolder, "dataFolder"); + this.dataFolder = dataFolder; this.factory = new ConfigFactory(); this.configurer = new ConfigConfigurer(); @@ -75,7 +74,7 @@ public Set getConfigs() { return Collections.unmodifiableSet(configs); } - public void clearAll() { + public void shutdown() { configs.clear(); byType.clear(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceConnector.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceConnector.java new file mode 100644 index 0000000..6d4dab0 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceConnector.java @@ -0,0 +1,74 @@ +package com.github.imdmk.playtime.database; + +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurer; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.j256.ormlite.jdbc.DataSourceConnectionSource; +import com.j256.ormlite.support.ConnectionSource; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.sql.SQLException; + +final class DataSourceConnector { + + private final PluginLogger logger; + + private final DataSourceFactory dataSourceFactory; + private final DataSourceConfigurer dataSourceConfigurer; + + private volatile HikariDataSource dataSource; + private volatile ConnectionSource connectionSource; + + DataSourceConnector( + @NotNull PluginLogger logger, + @NotNull DataSourceFactory dataSourceFactory, + @NotNull DataSourceConfigurer dataSourceConfigurer + ) { + this.logger = logger; + this.dataSourceFactory = dataSourceFactory; + this.dataSourceConfigurer = dataSourceConfigurer; + } + + synchronized void connect(@NotNull DatabaseConfig config, @NotNull File dataFolder) throws SQLException { + if (dataSource != null || connectionSource != null) { + throw new IllegalStateException("DataSource is already connected"); + } + + final HikariDataSource dataSource = dataSourceFactory.create(config); + + dataSourceConfigurer.configure(dataSource, config, dataFolder); + if (dataSource.getJdbcUrl() == null) { + throw new IllegalStateException("JDBC URL was not set by DataSourceConfigurer"); + } + + final ConnectionSource connectionSource = new DataSourceConnectionSource(dataSource, dataSource.getJdbcUrl()); + + this.dataSource = dataSource; + this.connectionSource = connectionSource; + + logger.info("Connected to %s database.", config.databaseMode); + } + + synchronized void close() { + if (connectionSource != null) { + try { + connectionSource.close(); + } catch (Exception ignored) {} + } + + if (dataSource != null) { + dataSource.close(); + } + + connectionSource = null; + dataSource = null; + } + + @Nullable + ConnectionSource getConnectionSource() { + return connectionSource; + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceFactory.java new file mode 100644 index 0000000..23b3525 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceFactory.java @@ -0,0 +1,44 @@ +package com.github.imdmk.playtime.database; + +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +final class DataSourceFactory { + + static final String POOL_NAME = "playtime-db-pool"; + + static final int MAX_POOL_SIZE = Math.max(4, Runtime.getRuntime().availableProcessors()); + static final int MIN_IDLE = 0; + + static final int CONNECTION_TIMEOUT = 10_000; + static final int IDLE_TIMEOUT = 60_000; + static final int MAX_LIFETIME = 600_000; + + static final Map SOURCE_PROPERTIES = Map.of( + "cachePrepStmts", true, + "prepStmtCacheSize", 250, + "prepStmtCacheSqlLimit", 2048, + "useServerPrepStmts", true + ); + + HikariDataSource create(@NotNull DatabaseConfig config) { + final HikariDataSource dataSource = new HikariDataSource(); + dataSource.setPoolName(POOL_NAME); + + dataSource.setUsername(config.databaseUserName); + dataSource.setPassword(config.databasePassword); + + dataSource.setMaximumPoolSize(MAX_POOL_SIZE); + dataSource.setMinimumIdle(MIN_IDLE); + + dataSource.setConnectionTimeout(CONNECTION_TIMEOUT); + dataSource.setIdleTimeout(IDLE_TIMEOUT); + dataSource.setMaxLifetime(MAX_LIFETIME); + + SOURCE_PROPERTIES.forEach(dataSource::addDataSourceProperty); + return dataSource; + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java new file mode 100644 index 0000000..152954f --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java @@ -0,0 +1,64 @@ +package com.github.imdmk.playtime.database; + +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurer; +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurerFactory; +import com.github.imdmk.playtime.database.library.DriverLibraryLoader; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.j256.ormlite.support.ConnectionSource; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.panda_lang.utilities.inject.annotations.Inject; +import org.panda_lang.utilities.inject.annotations.PostConstruct; + +import java.io.File; +import java.sql.SQLException; + +@Service(priority = Priority.NORMAL) +public final class DatabaseBootstrap { + + private final File dataFolder; + private final DatabaseConfig config; + + private final DriverLibraryLoader libraryLoader; + private final DataSourceConnector dataConnector; + + @Inject + public DatabaseBootstrap( + @NotNull Plugin plugin, + @NotNull File dataFolder, + @NotNull PluginLogger logger, + @NotNull DatabaseConfig config + ) { + this.dataFolder = dataFolder; + this.config = config; + + this.libraryLoader = new DriverLibraryLoader(plugin); + + final DataSourceConfigurer configurer = DataSourceConfigurerFactory.getFor(config.databaseMode); + final DataSourceFactory factory = new DataSourceFactory(); + this.dataConnector = new DataSourceConnector(logger, factory, configurer); + } + + @PostConstruct + public void start() { + libraryLoader.loadFor(config.databaseMode); + + try { + dataConnector.connect(config, dataFolder); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public ConnectionSource getConnection() { + return dataConnector.getConnectionSource(); + } + + public void shutdown() { + dataConnector.close(); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java index 11784b6..6b22e19 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java @@ -1,18 +1,12 @@ package com.github.imdmk.playtime.database; import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; -/** - * Configuration for the database connection layer. - *

- * Supports both embedded (SQLite/H2) and server-based engines (MySQL, MariaDB, PostgreSQL, SQL Server). - * Depending on {@link DatabaseMode}, only a subset of fields is used. - *

- * All unused fields for a given mode are safely ignored by the connector. - */ +@ConfigFile public final class DatabaseConfig extends ConfigSection { @Comment({ @@ -80,12 +74,12 @@ public final class DatabaseConfig extends ConfigSection { public int port = 3306; @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> {}; } @Override - public @NotNull String getFileName() { - return "databaseConfig.yml"; + public @NotNull String fileName() { + return "database.yaml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConnector.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConnector.java deleted file mode 100644 index 8cbc902..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConnector.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.github.imdmk.playtime.database; - -import com.github.imdmk.playtime.database.driver.configurer.DriverConfigurer; -import com.github.imdmk.playtime.database.driver.configurer.DriverConfigurerFactory; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.jdbc.DataSourceConnectionSource; -import com.j256.ormlite.support.ConnectionSource; -import com.zaxxer.hikari.HikariDataSource; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.util.logging.Level; - -final class DatabaseConnector { - - private static final String POOL_NAME = "playtime-db-pool"; - - private static final int DEFAULT_MAX_POOL_SIZE = 4; - private static final int DEFAULT_MIN_IDLE = 0; - - private static final long DEFAULT_CONNECTION_TIMEOUT_MS = 10_000L; - private static final long DEFAULT_IDLE_TIMEOUT_MS = 60_000L; - private static final long DEFAULT_MAX_LIFETIME_MS = 600_000L; - - private static final boolean CACHE_PREP_STMTS = true; - private static final int PREP_STMT_CACHE_SIZE = 250; - private static final int PREP_STMT_CACHE_SQL_LIMIT = 2048; - private static final boolean USE_SERVER_PREP_STMTS = true; - - private static final Level DATA_SOURCE_LOG_LEVEL = Level.SEVERE; - - private final PluginLogger logger; - private final DatabaseConfig config; - private final DriverConfigurer driverConfigurer; - - private volatile HikariDataSource dataSource; - private volatile ConnectionSource connectionSource; - - /** - * Creates a new connector with an explicit {@link DriverConfigurer}. - * Useful for testing or advanced customization. - * - * @param logger the plugin logger (never null) - * @param config the database configuration (never null) - * @param driverConfigurer strategy used to configure the underlying JDBC driver (never null) - */ - DatabaseConnector( - @NotNull PluginLogger logger, - @NotNull DatabaseConfig config, - @NotNull DriverConfigurer driverConfigurer - ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.config = Validator.notNull(config, "config cannot be null"); - this.driverConfigurer = Validator.notNull(driverConfigurer, "driverConfigurer cannot be null"); - } - - /** - * Creates a new connector using the default {@link DriverConfigurer} - * resolved from {@link DriverConfigurerFactory} based on {@link DatabaseConfig#databaseMode}. - * - * @param logger the plugin logger (never null) - * @param config the database configuration (never null) - */ - DatabaseConnector( - @NotNull PluginLogger logger, - @NotNull DatabaseConfig config - ) { - this(logger, config, DriverConfigurerFactory.getFor(config.databaseMode)); - } - - /** - * Establishes a new database connection and initializes the internal Hikari connection pool. - *

- * If already connected, this method throws {@link IllegalStateException}. - * Engine-specific configuration (JDBC URL, file paths, flags) is delegated - * to the configured {@link DriverConfigurer}. - * - * @param dataFolder plugin data folder, used especially for file-based databases (e.g. SQLite/H2) - * @throws SQLException if JDBC or ORMLite initialization fails - * @throws IllegalStateException if a connection is already active - */ - synchronized void connect(@NotNull File dataFolder) throws SQLException { - Validator.notNull(dataFolder, "dataFolder cannot be null"); - - if (dataSource != null || connectionSource != null) { - throw new IllegalStateException("DatabaseConnector is already connected."); - } - - final HikariDataSource ds = createHikariDataSource(); - - try { - // Delegated engine-specific configuration (JDBC URL, engine flags, filesystem prep) - driverConfigurer.configure(ds, config, dataFolder); - - final String jdbcUrl = ds.getJdbcUrl(); - if (jdbcUrl == null || jdbcUrl.isBlank()) { - throw new IllegalStateException("DriverConfigurer did not set JDBC URL for mode " + config.databaseMode); - } - - final ConnectionSource source = new DataSourceConnectionSource(ds, jdbcUrl); - - dataSource = ds; - connectionSource = source; - - logger.info("Connected to %s database.", config.databaseMode); - } catch (SQLException e) { - logger.error(e, "Failed to connect to database"); - closeQuietly(ds); - dataSource = null; - connectionSource = null; - throw e; - } catch (Exception e) { - logger.error(e, "Failed to initialize database"); - closeQuietly(ds); - dataSource = null; - connectionSource = null; - throw new IllegalStateException("Database initialization failed", e); - } - } - - /** - * Closes the active database connection and shuts down the underlying HikariCP pool. - *

- * Safe to call multiple times. Exceptions during close are logged but ignored. - */ - synchronized void close() { - if (connectionSource == null && dataSource == null) { - logger.warn("DatabaseConnector#close() called, but not connected."); - return; - } - - try { - if (connectionSource != null) { - connectionSource.close(); - } - } catch (Exception e) { - logger.error(e, "Failed to close ConnectionSource"); - } - - closeQuietly(dataSource); - - connectionSource = null; - dataSource = null; - - logger.info("Database connection closed successfully."); - } - - /** - * Returns whether this connector is currently connected. - * - * @return {@code true} if both {@link ConnectionSource} and {@link HikariDataSource} are active - */ - boolean isConnected() { - final HikariDataSource ds = dataSource; - return connectionSource != null && ds != null && !ds.isClosed(); - } - - /** - * Returns the current active {@link ConnectionSource}, or {@code null} if not connected. - * - * @return active ORMLite connection source, or {@code null} if disconnected - */ - @Nullable ConnectionSource getConnectionSource() { - return connectionSource; - } - - /** - * Creates and configures a new {@link HikariDataSource} with conservative, engine-agnostic defaults. - *

- * Includes: - *

    - *
  • Moderate pool sizing (max 5, min 1 by default).
  • - *
  • Prepared statement caching (effective for MySQL-family drivers, harmless elsewhere).
  • - *
  • Connection, idle and lifetime timeouts with safe values.
  • - *
- * - * @return configured Hikari data source (not yet started) - */ - private @NotNull HikariDataSource createHikariDataSource() { - final HikariDataSource data = new HikariDataSource(); - data.setPoolName(POOL_NAME); - - data.setMaximumPoolSize(Math.max(DEFAULT_MAX_POOL_SIZE, Runtime.getRuntime().availableProcessors())); - data.setMinimumIdle(DEFAULT_MIN_IDLE); - - data.setUsername(config.databaseUserName); - data.setPassword(config.databasePassword); - - // Reduce noisy driver logging if supported - try { - data.getParentLogger().setLevel(DATA_SOURCE_LOG_LEVEL); - } catch (SQLFeatureNotSupportedException ignored) {} - - // Prepared statement cache (useful for MySQL-family; harmless for others) - data.addDataSourceProperty("cachePrepStmts", CACHE_PREP_STMTS); - data.addDataSourceProperty("prepStmtCacheSize", PREP_STMT_CACHE_SIZE); - data.addDataSourceProperty("prepStmtCacheSqlLimit", PREP_STMT_CACHE_SQL_LIMIT); - data.addDataSourceProperty("useServerPrepStmts", USE_SERVER_PREP_STMTS); - - // Timeout configuration (milliseconds) - data.setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT_MS); - data.setIdleTimeout(DEFAULT_IDLE_TIMEOUT_MS); - data.setMaxLifetime(DEFAULT_MAX_LIFETIME_MS); - - return data; - } - - /** - * Closes the given {@link HikariDataSource} without propagating exceptions. - * - * @param ds data source to close (nullable) - */ - private static void closeQuietly(@Nullable HikariDataSource ds) { - try { - if (ds != null) { - ds.close(); - } - } catch (Exception ignored) {} - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseManager.java deleted file mode 100644 index a8970b0..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseManager.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.imdmk.playtime.database; - -import com.github.imdmk.playtime.database.driver.library.DriverLibraryLoader; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.support.ConnectionSource; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.SQLException; - -public final class DatabaseManager { - - private final Plugin plugin; - private final DatabaseConfig config; - - private final DriverLibraryLoader driverLoader; - private final DatabaseConnector connector; - - public DatabaseManager( - @NotNull Plugin plugin, - @NotNull PluginLogger logger, - @NotNull DatabaseConfig config - ) { - this.plugin = Validator.notNull(plugin, "plugin"); - this.config = Validator.notNull(config, "config"); - - this.driverLoader = new DriverLibraryLoader(plugin); - this.connector = new DatabaseConnector(logger, config); - } - - public void loadDriver() { - driverLoader.loadFor(config.databaseMode); - } - - public void connect() throws SQLException { - connector.connect(plugin.getDataFolder()); - } - - @Nullable - public ConnectionSource getConnection() { - return connector.getConnectionSource(); - } - - public void shutdown() { - if (connector.isConnected()) { - connector.close(); - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java index 3f17504..8a3f01c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java @@ -1,68 +1,5 @@ package com.github.imdmk.playtime.database; -/** - * Enumerates all database engines supported by the plugin. - *

- * Each value represents a distinct JDBC provider and is used to: - *

    - *
  • select and load the correct JDBC driver dynamically,
  • - *
  • apply engine-specific JDBC URL configuration,
  • - *
  • initialize the matching {@code DriverConfigurer} implementation.
  • - *
- *

- * Below each engine is annotated with practical recommendations - * for typical Minecraft server environments. - */ public enum DatabaseMode { - - /** - * MySQL — recommended for most production servers. - *

- * Stable, well-supported, widely hosted, good performance under sustained load. - * Best choice for: medium–large servers, networks, Bungee/Velocity setups. - */ - MYSQL, - - /** - * MariaDB — drop-in MySQL replacement. - *

- * Often faster for reads, lighter resource usage, very stable on Linux hosts. - * Best choice for: self-hosted servers (VPS/dedicated), users preferring open-source MySQL alternatives. - */ - MARIADB, - - /** - * SQLite — file-based embedded database. - *

- * Zero configuration, no external server needed, safe for smaller datasets. - * Best choice for: small servers, testing environments, local development. - * Avoid it for large playtime tables or heavy concurrent write load. - */ - SQLITE, - - /** - * PostgreSQL — robust, enterprise-grade server engine. - *

- * Very strong consistency guarantees, excellent indexing, powerful features. - * Best choice for: large datasets, advanced analytics, servers on modern hosting (e.g., managed PSQL). - */ - POSTGRESQL, - - /** - * H2 — lightweight embedded or file-based engine. - *

- * Faster than SQLite in many scenarios, supports MySQL compatibility mode. - * Best choice for: plugin developers, embedded deployments, users wanting higher performance without external DB. - * Not recommended for: huge datasets or multi-server networks. - */ - H2, - - /** - * SQL Server (MSSQL) — enterprise Microsoft database engine. - *

- * Works well on Windows hosts, strong enterprise tooling. - * Best choice for: Windows-based servers, corporate networks using MSSQL by default. - * Rarely needed for typical Minecraft environments. - */ - SQL + MYSQL, MARIADB, SQLITE, POSTGRESQL, H2, SQL } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurer.java similarity index 73% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurer.java index 3cc8652..3fdb6f8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurer.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; @@ -6,7 +6,8 @@ import java.io.File; -public interface DriverConfigurer { +@FunctionalInterface +public interface DataSourceConfigurer { void configure(@NotNull HikariDataSource dataSource, @NotNull DatabaseConfig config, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java similarity index 60% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurerFactory.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java index c2591a6..762d737 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/DriverConfigurerFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java @@ -1,14 +1,13 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseMode; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import java.util.Map; -public final class DriverConfigurerFactory { +public final class DataSourceConfigurerFactory { - private static final Map CONFIGURER_BY_MODE = Map.of( + private static final Map CONFIGURER_BY_MODE = Map.of( DatabaseMode.MYSQL, new MySQLConfigurer(), DatabaseMode.MARIADB, new MariaDBConfigurer(), DatabaseMode.POSTGRESQL, new PostgreSQLConfigurer(), @@ -17,10 +16,8 @@ DatabaseMode.H2, new H2Configurer(), DatabaseMode.SQL, new SQLConfigurer() ); - public static @NotNull DriverConfigurer getFor(@NotNull DatabaseMode mode) { - Validator.notNull(mode, "mode cannot be null"); - - final DriverConfigurer configurer = CONFIGURER_BY_MODE.get(mode); + public static @NotNull DataSourceConfigurer getFor(@NotNull DatabaseMode mode) { + final DataSourceConfigurer configurer = CONFIGURER_BY_MODE.get(mode); if (configurer == null) { throw new IllegalArgumentException("Unsupported database mode: " + mode); } @@ -28,7 +25,7 @@ DatabaseMode.SQL, new SQLConfigurer() return configurer; } - private DriverConfigurerFactory() { + private DataSourceConfigurerFactory() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/H2Configurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/H2Configurer.java similarity index 89% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/H2Configurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/H2Configurer.java index aa73a61..9b94715 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/H2Configurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/H2Configurer.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; @@ -9,7 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; -final class H2Configurer implements DriverConfigurer { +final class H2Configurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:h2:file:%s" + ";MODE=MySQL" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MariaDBConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MariaDBConfigurer.java similarity index 85% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MariaDBConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MariaDBConfigurer.java index abf6dc4..283af33 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MariaDBConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MariaDBConfigurer.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; @@ -6,7 +6,7 @@ import java.io.File; -final class MariaDBConfigurer implements DriverConfigurer { +final class MariaDBConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:mariadb://%s:%s/%s" + "?useUnicode=true" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MySQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MySQLConfigurer.java similarity index 87% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MySQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MySQLConfigurer.java index 45e37d6..aceb877 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/MySQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MySQLConfigurer.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; @@ -6,7 +6,7 @@ import java.io.File; -final class MySQLConfigurer implements DriverConfigurer { +final class MySQLConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:mysql://%s:%s/%s" + "?useSSL=false" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/PostgreSQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/PostgreSQLConfigurer.java similarity index 83% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/PostgreSQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/PostgreSQLConfigurer.java index c9ac4cd..68e75c7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/PostgreSQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/PostgreSQLConfigurer.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; @@ -6,7 +6,7 @@ import java.io.File; -final class PostgreSQLConfigurer implements DriverConfigurer { +final class PostgreSQLConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:postgresql://%s:%s/%s" + "?sslmode=disable" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLConfigurer.java similarity index 84% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLConfigurer.java index aeb1b6a..c41fb9b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLConfigurer.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; @@ -6,7 +6,7 @@ import java.io.File; -final class SQLConfigurer implements DriverConfigurer { +final class SQLConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:sqlserver://%s:%s" + ";databaseName=%s" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLiteConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLiteConfigurer.java similarity index 89% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLiteConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLiteConfigurer.java index 5c76fda..8fe70cf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/configurer/SQLiteConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLiteConfigurer.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; @@ -9,7 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; -final class SQLiteConfigurer implements DriverConfigurer { +final class SQLiteConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:sqlite:%s"; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraries.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraries.java similarity index 95% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraries.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraries.java index 7dd2a30..aae238c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraries.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraries.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.database.driver.library; +package com.github.imdmk.playtime.database.library; import com.alessiodp.libby.Library; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraryLoader.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraryLoader.java similarity index 83% rename from playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraryLoader.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraryLoader.java index 0c5a139..fb8dd0c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/driver/library/DriverLibraryLoader.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraryLoader.java @@ -1,12 +1,10 @@ -package com.github.imdmk.playtime.database.driver.library; +package com.github.imdmk.playtime.database.library; import com.alessiodp.libby.BukkitLibraryManager; import com.alessiodp.libby.Library; import com.github.imdmk.playtime.database.DatabaseMode; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; import java.util.Map; @@ -23,15 +21,12 @@ public final class DriverLibraryLoader { private final BukkitLibraryManager libraryManager; - @Inject public DriverLibraryLoader(@NotNull Plugin plugin) { this.libraryManager = new BukkitLibraryManager(plugin); this.libraryManager.addMavenCentral(); } public void loadFor(@NotNull DatabaseMode mode) { - Validator.notNull(mode, "mode cannot be null"); - final Library library = LIBRARIES_BY_MODE.get(mode); if (library == null) { throw new IllegalArgumentException("Unsupported database mode: " + mode); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java index afc6e63..fd4431d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java @@ -5,26 +5,10 @@ import java.sql.SQLException; -/** - * Base contract for all repositories. - *

- * Provides lifecycle hooks for database initialization and cleanup. - * Implementations should create their DAO bindings in {@link #start(ConnectionSource)} - * and release resources in {@link #close()}. - */ public interface Repository extends AutoCloseable { - /** - * Initializes repository to the given connection source. - * - * @param source the ORMLite connection source - * @throws SQLException if database initialization fails - */ void start(@NotNull ConnectionSource source) throws SQLException; - /** - * Closes the repository and releases all resources. - */ @Override void close(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryContext.java deleted file mode 100644 index 8e3dd74..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryContext.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.imdmk.playtime.database.repository; - -import com.github.imdmk.playtime.database.repository.ormlite.BaseDaoRepository; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.ExecutorService; - -/** - * Context container providing shared infrastructure resources - * for all repositories. - * - *

Currently encapsulates the {@link ExecutorService} responsible for - * executing asynchronous database operations. This allows repositories - * to offload blocking I/O work while maintaining a unified execution policy.

- * - *

Usage: Injected into repository instances (see {@link BaseDaoRepository}) - * to provide consistent thread management for database access.

- * - *

Threading: The supplied {@code dbExecutor} should be a dedicated, - * bounded thread pool optimized for database I/O tasks — typically sized according - * to connection pool limits or database concurrency capabilities.

- * - * @param dbExecutor the executor service used for running asynchronous database operations (non-null) - * - * @see BaseDaoRepository - */ -public record RepositoryContext(@NotNull ExecutorService dbExecutor) { -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryManager.java deleted file mode 100644 index d364e06..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryManager.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.imdmk.playtime.database.repository; - -import com.github.imdmk.playtime.database.repository.ormlite.BaseDaoRepository; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.support.ConnectionSource; -import org.jetbrains.annotations.NotNull; - -import java.sql.SQLException; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Central coordinator that manages the lifecycle of all registered {@link Repository} instances. - * - *

This class provides thread-safe registration, startup, and shutdown of repositories. - * It acts as the single entry point for initializing all repositories - * once the plugin’s database connection has been established.

- * - *

Thread-safety: Repository registration and iteration are backed by - * {@link CopyOnWriteArrayList}, ensuring safe concurrent reads and registrations.

- * - * @see Repository - * @see BaseDaoRepository - * @see ConnectionSource - */ -public final class RepositoryManager implements AutoCloseable { - - private final PluginLogger logger; - - private final List repositories = new CopyOnWriteArrayList<>(); - - public RepositoryManager(@NotNull PluginLogger logger) { - this.logger = Validator.notNull(logger, "logger must not be null"); - } - - /** - * Registers a single repository instance for lifecycle management. - * - *

If the repository has already been registered, the operation is skipped - * and a warning is logged.

- * - * @param repository non-null repository instance - * @throws NullPointerException if {@code repository} is null - */ - public void register(@NotNull Repository repository) { - Validator.notNull(repository, "repository cannot be null"); - if (repositories.contains(repository)) { - logger.warn("Repository %s already registered — skipping", repository.getClass().getSimpleName()); - return; - } - - repositories.add(repository); - } - - /** - * Registers multiple repository instances at once. - * - * @param repositories one or more non-null repositories - * @throws NullPointerException if {@code repositories} array or any element is null - */ - public void register(@NotNull Repository... repositories) { - Validator.notNull(repositories, "repositories cannot be null"); - for (final Repository repository : repositories) { - this.register(repository); - } - } - - /** - * Starts all registered repositories using the provided {@link ConnectionSource}. - * - *

This method creates required tables and initializes all DAO layers. - * If any repository fails to start, the exception is logged and rethrown, - * stopping further startup to prevent inconsistent state.

- * - * @param connectionSource non-null active database connection source - * @throws SQLException if a repository fails to start - * @throws NullPointerException if {@code connectionSource} is null - */ - public void startAll(@NotNull ConnectionSource connectionSource) throws SQLException { - Validator.notNull(connectionSource, "connectionSource cannot be null"); - for (final Repository repository : repositories) { - try { - repository.start(connectionSource); - } catch (SQLException e) { - logger.error(e, "Failed to start repository: %s", repository.getClass().getSimpleName()); - throw e; - } - } - } - - /** - * Gracefully closes all registered repositories. - * - *

Each repository’s {@link Repository#close()} method is invoked individually. - * Exceptions during closing are caught and logged as warnings, allowing - * all repositories to attempt shutdown even if one fails.

- */ - @Override - public void close() { - for (final Repository repository : repositories) { - try { - repository.close(); - } catch (Exception e) { - logger.warn(e, "Error while closing repository: %s", repository.getClass().getSimpleName()); - } - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepository.java deleted file mode 100644 index 7ddac81..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepository.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.github.imdmk.playtime.database.repository.ormlite; - -import com.github.imdmk.playtime.database.repository.Repository; -import com.github.imdmk.playtime.database.repository.RepositoryContext; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.dao.DaoManager; -import com.j256.ormlite.logger.Level; -import com.j256.ormlite.logger.Logger; -import com.j256.ormlite.support.ConnectionSource; -import com.j256.ormlite.table.TableUtils; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.sql.SQLException; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; - -/** - * Base class for ORMLite-backed repositories that manages DAO lifecycle, - * schema bootstrapping, and asynchronous query execution. - * - *

Responsibilities:

- *
    - *
  • Create required tables for the main entity and optional subclasses.
  • - *
  • Initialize and expose a typed {@link Dao} instance.
  • - *
  • Provide helper methods to run DAO work asynchronously with a bounded timeout.
  • - *
- * - *

Thread-safety: the {@link #dao} reference is {@code volatile} to ensure - * visibility after initialization. Repository implementations should still avoid compound unsynchronized - * operations on the DAO.

- * - * @param entity type handled by the repository - * @param identifier type of the entity - * - * @see Dao - * @see RepositoryContext - * @see TableUtils - */ -public abstract class BaseDaoRepository implements Repository { - - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(6); - - private final PluginLogger logger; - private final RepositoryContext context; - - protected volatile Dao dao; - - @Inject - protected BaseDaoRepository(@NotNull PluginLogger logger, @NotNull RepositoryContext context) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.context = Validator.notNull(context, "context cannot be null"); - Logger.setGlobalLogLevel(Level.ERROR); // Change ORMLITE logging to errors - } - - protected abstract Class entityClass(); - - protected abstract List> entitySubClasses(); - - /** - * Initializes the repository: creates missing tables and registers the DAO. - * - *

Tables for {@link #entityClass()} and all {@link #entitySubClasses()} are created if absent. - * Then a new {@link Dao} is obtained via {@link DaoManager#createDao(ConnectionSource, Class)}.

- * - * @param source active ORMLite connection source - * @throws SQLException if schema creation or DAO initialization fails - */ - @Override - public void start(@NotNull ConnectionSource source) throws SQLException { - for (Class subClass : this.entitySubClasses()) { - TableUtils.createTableIfNotExists(source, subClass); - } - - TableUtils.createTableIfNotExists(source, this.entityClass()); - this.dao = DaoManager.createDao(source, this.entityClass()); - } - - /** - * Closes the repository by unregistering the current DAO from its {@link ConnectionSource}. - * - *

This method is idempotent. If no DAO is set, it returns immediately.

- */ - @Override - public void close() { - final Dao current = this.dao; - if (current == null) { - return; - } - - this.dao = null; - ConnectionSource source = current.getConnectionSource(); - if (source != null) { - DaoManager.unregisterDao(source, current); - } - } - - /** - * Executes the supplied task asynchronously on the repository executor with the default timeout. - * - *

Exceptions thrown by the supplier are logged and rethrown as {@link CompletionException}. - * If the task exceeds timeout, the returned future completes exceptionally - * with a {@link TimeoutException} wrapped in a {@link CompletionException}.

- * - * @param supplier unit of work to execute (non-null) - * @param result type - * @return a future completed with the supplier result or exceptionally on failure/timeout - * @throws NullPointerException if {@code supplier} is null - */ - protected CompletableFuture executeAsync(@NotNull Supplier supplier) { - return executeAsync(supplier, DEFAULT_TIMEOUT); - } - - /** - * Executes the supplied task asynchronously on the repository executor with a custom timeout. - * - *

Behavior:

- *
    - *
  • Runs on {@code context.dbExecutor()}.
  • - *
  • Logs and wraps exceptions into {@link CompletionException}.
  • - *
  • Applies {@link CompletableFuture#orTimeout(long, TimeUnit)} with the provided duration.
  • - *
- * - * @param supplier unit of work to execute (non-null) - * @param timeout maximum execution time (non-null) - * @param result type - * @return a future completed with the supplier result or exceptionally on failure/timeout - * @throws NullPointerException if {@code supplier} or {@code timeout} is null - */ - protected CompletableFuture executeAsync(@NotNull Supplier supplier, @NotNull Duration timeout) { - Validator.notNull(supplier, "supplier cannot be null"); - Validator.notNull(timeout, "timeout cannot be null"); - - return CompletableFuture - .supplyAsync(() -> { - try { - return supplier.get(); - } catch (Exception e) { - logger.error(e, "Async DAO operation failed"); - throw new CompletionException(e); - } - }, this.context.dbExecutor()) - .orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS) - .exceptionally(e -> { - if (e instanceof TimeoutException) { - logger.warn("Async DAO operation timed out after %s ms", timeout.toMillis()); - } else { - logger.error(e, "Async DAO operation failed (outer)"); - } - throw (e instanceof CompletionException) - ? (CompletionException) e - : new CompletionException(e); - }); - } - - /** - * Executes the given runnable asynchronously on the repository executor with the default timeout. - * - * @param runnable task to run (non-null) - * @return a future completed normally on success or exceptionally on failure/timeout - * @throws NullPointerException if {@code runnable} is null - */ - protected CompletableFuture executeAsync(@NotNull Runnable runnable) { - return executeAsync(runnable, DEFAULT_TIMEOUT); - } - - /** - * Executes the given runnable asynchronously on the repository executor with a custom timeout. - * - *

Exceptions thrown by the runnable are logged and propagated as {@link CompletionException}. - * On timeout, the future completes exceptionally with a {@link TimeoutException} wrapped in a - * {@link CompletionException}.

- * - * @param runnable task to run (non-null) - * @param timeout maximum execution time (non-null) - * @return a future completed normally on success or exceptionally on failure/timeout - * @throws NullPointerException if {@code runnable} or {@code timeout} is null - */ - protected CompletableFuture executeAsync(@NotNull Runnable runnable, @NotNull Duration timeout) { - return CompletableFuture - .runAsync(() -> { - try { - runnable.run(); - } catch (Exception e) { - logger.error(e, "Async DAO operation failed"); - throw new CompletionException(e); - } - }, this.context.dbExecutor()) - .orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS) - .exceptionally(e -> { - if (e instanceof TimeoutException) { - logger.warn("Async DAO operation (void) timed out after %s ms", timeout.toMillis()); - } else { - logger.error(e, "Async DAO operation failed (void outer)"); - } - throw (e instanceof CompletionException) - ? (CompletionException) e - : new CompletionException(e); - }); - } - - /** - * Executes work requiring an initialized DAO, failing fast if the repository has not been started. - * - *

Use this to guard synchronous code paths that assume the DAO is ready.

- * - * @param work supplier executed with the current repository state (non-null) - * @param result type - * @return the supplier's result - * @throws IllegalStateException if the DAO is not initialized (e.g. {@link #start(ConnectionSource)} not called) - * @throws NullPointerException if {@code work} is null - */ - protected R withDao(@NotNull Supplier work) { - Dao current = this.dao; - if (current == null) { - throw new IllegalStateException(getClass().getSimpleName() + ": DAO not initialized. Call RepositoryManager.startAll()"); - } - - return work.get(); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java index 00c199b..b0b42ba 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java @@ -5,55 +5,17 @@ import java.util.List; import java.util.stream.Collectors; -/** - * Defines a bidirectional mapper between persistence-layer entities (ORM objects) - * and domain-layer models (business objects). - *

- * This abstraction keeps the repository layer decoupled from the domain layer, - * allowing storage representations to evolve independently of business logic. - * - * @param entity type used for persistence (ORM/DB representation) - * @param domain model type used in business logic - */ public interface EntityMapper { - /** - * Maps a domain model instance into its persistence-layer entity representation. - * - * @param domain the domain object to convert (never null) - * @return the corresponding persistence entity - */ - @NotNull E toEntity(@NotNull D domain); + E toEntity(@NotNull D domain); - /** - * Maps a persistence-layer entity into its domain model representation. - * - * @param entity the persistence entity to convert (never null) - * @return the corresponding domain model - */ - @NotNull D toDomain(@NotNull E entity); + D toDomain(@NotNull E entity); - /** - * Converts a list of persistence entities to domain model objects. - *

- * This is a convenience method for bulk transformations. - * - * @param entities list of entities to convert (never null) - * @return list of mapped domain models - */ - default @NotNull List toDomainList(@NotNull List entities) { + default List toDomainList(@NotNull List entities) { return entities.stream().map(this::toDomain).collect(Collectors.toList()); } - /** - * Converts a list of domain model objects to persistence entities. - *

- * This is a convenience method for bulk transformations. - * - * @param domains list of domain objects to convert (never null) - * @return list of mapped persistence entities - */ - default @NotNull List toEntityList(@NotNull List domains) { + default List toEntityList(@NotNull List domains) { return domains.stream().map(this::toEntity).collect(Collectors.toList()); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java index 75be5a3..a9dce48 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java @@ -1,14 +1,3 @@ package com.github.imdmk.playtime.database.repository.ormlite; -/** - * Marker interface for database entity metadata containers. - * - *

All metadata interfaces (e.g. {@code UserEntityMeta}) should extend this - * interface to indicate that they define static constants describing database - * schema elements such as table and column names.

- * - *

This provides a unified contract for schema metadata used by ORMLite - * entities, repositories, and migration utilities.

- */ -public interface EntityMeta { -} +public interface EntityMeta {} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java new file mode 100644 index 0000000..443649f --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java @@ -0,0 +1,77 @@ +package com.github.imdmk.playtime.database.repository.ormlite; + +import com.github.imdmk.playtime.database.repository.Repository; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.logger.Level; +import com.j256.ormlite.logger.Logger; +import com.j256.ormlite.support.ConnectionSource; +import com.j256.ormlite.table.TableUtils; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public abstract class OrmLiteRepository + implements Repository { + + private static final Duration EXECUTE_TIMEOUT = Duration.ofSeconds(3); + + protected final PluginLogger logger; + protected final TaskScheduler scheduler; + + protected volatile Dao dao; + + protected OrmLiteRepository(@NotNull PluginLogger logger, @NotNull TaskScheduler scheduler) { + this.logger = logger; + this.scheduler = scheduler; + Logger.setGlobalLogLevel(Level.ERROR); // Change ORMLITE logging to errors + } + + protected abstract Class entityClass(); + + protected abstract List> entitySubClasses(); + + @Override + public void start(@NotNull ConnectionSource source) throws SQLException { + for (final Class subClass : this.entitySubClasses()) { + TableUtils.createTableIfNotExists(source, subClass); + } + + TableUtils.createTableIfNotExists(source, this.entityClass()); + this.dao = DaoManager.createDao(source, this.entityClass()); + } + + @Override + public void close() { + final Dao current = this.dao; + if (current == null) { + return; + } + + this.dao = null; + final ConnectionSource connection = current.getConnectionSource(); + if (connection != null) { + DaoManager.unregisterDao(connection, current); + } + } + + protected CompletableFuture execute(@NotNull Supplier supplier) { + final CompletableFuture future = new CompletableFuture<>(); + this.scheduler.runAsync(() -> { + try { + future.complete(supplier.get()); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + + return future.orTimeout(EXECUTE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationConfig.java deleted file mode 100644 index 7386c46..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationConfig.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.github.imdmk.playtime.feature.migration; - -import com.github.imdmk.playtime.config.ConfigSection; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; - -public final class MigrationConfig extends ConfigSection { - - @Comment({ - "#", - "# Enables a one-time automatic migration on the first plugin startup.", - "#", - "# How it works:", - "# - When set to true, the plugin will perform a full server migration on startup.", - "# - After the migration completes successfully, the plugin will automatically set this value to false", - "# to prevent running the same migration again on the next startup.", - "# - Set this back to true manually only if you know what you are doing and want to re-run the initial migration.", - "#" - }) - public boolean initialServerMigrationEnabled = true; - - @Comment({ - "#", - "# Maximum allowed execution time for a single migration task.", - "#", - "# If a specific migration step (e.g. processing a batch of players) exceeds this duration,", - "# it will be treated as timed-out and can be cancelled or failed.", - "#" - }) - public Duration migrationTaskTimeout = Duration.ofSeconds(5); - - @Comment({ - "#", - "# Global timeout for the entire migration process.", - "#", - "# This is a hard upper limit for all migration tasks combined. If the full migration does not finish", - "# within this time window, the process will be considered failed or aborted.", - "#" - }) - public Duration migrationGlobalTimeout = Duration.ofMinutes(2); - - @Comment({ - "#", - "# Keep-alive interval for long-running migrations.", - "#", - "# Used to periodically signal that the migration is still active and progressing,", - "# preventing it from being treated as stalled when processing large datasets.", - "#" - }) - public Duration migrationKeepAliveInterval = Duration.ofMinutes(1); - - @Comment({ - "#", - "# Maximum number of migration tasks that can run concurrently.", - "#", - "# This controls how many player/segment migrations can be processed in parallel.", - "#", - "# Recommendations:", - "# - Low values (1–2) are safer for small or heavily loaded servers.", - "# - Higher values (4+) speed up migration but may increase CPU/IO usage.", - "#" - }) - public int migrationMaxConcurrency = 3; - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> { - // No custom serializers required for this config. - }; - } - - @Override - public @NotNull String getFileName() { - return "migrationConfig.yml"; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationModule.java deleted file mode 100644 index 9e02c03..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationModule.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.imdmk.playtime.feature.migration; - -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.migrator.RepositoryPlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.BukkitPlayerProvider; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.feature.migration.runner.BlockingMigrationRunner; -import com.github.imdmk.playtime.infrastructure.module.Module; -import org.bukkit.Server; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class MigrationModule implements Module { - - private PlayerMigrator migrator; - private PlayerProvider provider; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(PlayerMigrator.class).assignInstance(() -> migrator); - resources.on(PlayerProvider.class).assignInstance(() -> provider); - } - - @Override - public void init(@NotNull Injector injector) {} - - @Override - public void afterRegister(@NotNull Plugin plugin, @NotNull Server server, @NotNull Injector injector) { - this.migrator = injector.newInstance(RepositoryPlayerMigrator.class); - this.provider = injector.newInstance(BukkitPlayerProvider.class); - - final BlockingMigrationRunner migrationRunner = injector.newInstance(BlockingMigrationRunner.class); - migrationRunner.execute(); - } - - @Override - public int order() { - return 10; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationResult.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationResult.java deleted file mode 100644 index 9209cda..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.imdmk.playtime.feature.migration; - -import java.time.Duration; - -/** - * Immutable summary of a completed migration run. - * - *

This record captures aggregated statistics such as:

- *
    - *
  • total players processed,
  • - *
  • successful migrations,
  • - *
  • failed migrations,
  • - *
  • total elapsed time.
  • - *
- * - *

Instances of this record are typically created by a - * {@code MigrationRunner} implementation after completing the migration workflow.

- * - * @param total total number of players considered - * @param successful number of successful migrations - * @param failed number of failed migrations - * @param took total duration of the migration process - */ -public record MigrationResult(int total, int successful, int failed, Duration took) { - - /** - * Returns an empty result representing a migration that processed - * no players and took zero time. - * - * @return a zero-valued {@code MigrationResult} - */ - public static MigrationResult empty() { - return new MigrationResult(0, 0, 0, Duration.ZERO); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/ConfigMigrationListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/ConfigMigrationListener.java deleted file mode 100644 index 2285ee5..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/ConfigMigrationListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.listener; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; - -public final class ConfigMigrationListener implements MigrationListener { - - private final MigrationConfig config; - - public ConfigMigrationListener(@NotNull MigrationConfig config) { - this.config = Validator.notNull(config, "config cannot be null"); - } - - @Override - public void onEnd(@NotNull MigrationResult result) { - config.initialServerMigrationEnabled = false; - config.save(); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/LoggerMigrationListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/LoggerMigrationListener.java deleted file mode 100644 index 6a7b97f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/LoggerMigrationListener.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.listener; - -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -public final class LoggerMigrationListener implements MigrationListener { - - private final PluginLogger logger; - - private volatile int completed; - private volatile int total; - - public LoggerMigrationListener(@NotNull PluginLogger logger) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - } - - @Override - public void onStart(int total) { - this.total = total; - this.completed = 0; - logger.info("Starting first-time migration of %d players...", total); - } - - @Override - public void onSuccess(@NotNull OfflinePlayer player) { - incrementAndLogProgress(); - } - - @Override - public void onFailed(@NotNull OfflinePlayer player, @NotNull Throwable throwable) { - logger.warn("Migration failed for %s: %s", player.getUniqueId(), throwable.getMessage()); - incrementAndLogProgress(); - } - - @Override - public void onEnd(@NotNull MigrationResult result) { - logger.info("Migration ended: success=%d, failed=%d, took=%sms", result.successful(), result.failed(), result.took().toMillis()); - } - - private void incrementAndLogProgress() { - int done = completed + 1; - int total = Math.max(1, this.total); - int percent = (int) ((done * 100L) / total); - - if (percent % 5 == 0 || done == total) { - logger.info("Migration progress: %d%% (%d/%d)", percent, done, total); - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/MigrationListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/MigrationListener.java deleted file mode 100644 index 6fcd36a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/MigrationListener.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.listener; - -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -/** - * Listener interface for receiving callbacks during a playtime data migration process. - * - *

This listener allows external components to observe and react to the - * lifecycle of a migration operation – from start, through per-player - * results, to the final completion summary.

- * - *

All methods are optional; implementations may override only the events - * they are interested in.

- */ -public interface MigrationListener { - - /** - * Called when the migration process begins. - * - * @param total total number of players scheduled for migration - */ - default void onStart(int total) {} - - /** - * Called when a player's data has been migrated successfully. - * - * @param player the offline player whose migration completed successfully - */ - default void onSuccess(@NotNull OfflinePlayer player) {} - - /** - * Called when a player's migration fails due to an unexpected error. - * - * @param player the offline player whose migration failed - * @param throwable the exception that caused the failure - */ - default void onFailed(@NotNull OfflinePlayer player, @NotNull Throwable throwable) {} - - /** - * Called when the migration process has completed for all players. - * - * @param result summary of the migration, including counts of successes, failures, - * and total processed players - */ - default void onEnd(@NotNull MigrationResult result) {} -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/PlayerMigrator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/PlayerMigrator.java deleted file mode 100644 index 7dbffd6..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/PlayerMigrator.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.migrator; - -import com.github.imdmk.playtime.user.User; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.CompletableFuture; - -/** - * Strategy interface responsible for migrating playtime data for a single - * {@link OfflinePlayer} into the plugin’s internal {@link User} domain model. - * - *

Implementations of this interface define how legacy or external data sources - * (e.g., Bukkit statistics API, flat files, third-party plugins, SQL tables) - * are translated into the unified User format used by the PlayTime system.

- * - *

Async contract:
- * The migration operation must be non-blocking and executed asynchronously. - * All heavy computation and I/O must run off the main server thread. - * The returned {@link CompletableFuture} represents the result of the migration.

- * - *

This interface is commonly used by bulk migration processes that iterate - * through all stored players and invoke this migrator per user.

- */ -@FunctionalInterface -public interface PlayerMigrator { - - /** - * Migrates playtime data for the given {@link OfflinePlayer}. - * - * @param player the offline player whose data should be migrated (never null) - * @return a future completing with the migrated {@link User} instance, - * or completing exceptionally if the migration fails - */ - CompletableFuture migrate(@NotNull OfflinePlayer player); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/RepositoryPlayerMigrator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/RepositoryPlayerMigrator.java deleted file mode 100644 index fbaaba5..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/RepositoryPlayerMigrator.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.migrator; - -import com.github.imdmk.playtime.shared.validate.Validator; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserFactory; -import com.github.imdmk.playtime.user.repository.UserRepository; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.concurrent.CompletableFuture; - -public final class RepositoryPlayerMigrator implements PlayerMigrator { - - private final UserRepository userRepository; - private final UserFactory userFactory; - - @Inject - public RepositoryPlayerMigrator( - @NotNull UserRepository userRepository, - @NotNull UserFactory userFactory - ) { - this.userRepository = Validator.notNull(userRepository, "userRepository cannot be null"); - this.userFactory = Validator.notNull(userFactory, "userFactory cannot be null"); - } - - @Override - public CompletableFuture migrate(@NotNull OfflinePlayer player) { - final User user = userFactory.createFrom(player); - return userRepository.save(user); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/BukkitPlayerProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/BukkitPlayerProvider.java deleted file mode 100644 index 2381519..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/BukkitPlayerProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.provider; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.OfflinePlayer; -import org.bukkit.Server; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -public final class BukkitPlayerProvider implements PlayerProvider { - - private final Server server; - - @Inject - public BukkitPlayerProvider(@NotNull Server server) { - this.server = Validator.notNull(server, "server cannot be null"); - } - - @Override - public @NotNull @Unmodifiable Collection getAllPlayers() { - return List.copyOf(Arrays.asList(server.getOfflinePlayers())); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/PlayerProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/PlayerProvider.java deleted file mode 100644 index 73826e8..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/PlayerProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.provider; - -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; - -/** - * Provides access to all players that should participate in a migration run. - * - *

Typical implementations include: - *

    - *
  • fetching all known {@link OfflinePlayer} instances from Bukkit,
  • - *
  • loading players from external storage,
  • - *
  • filtering players based on custom eligibility rules.
  • - *
- * - *

The contract guarantees that the returned collection is non-null, - * but may be empty.

- */ -public interface PlayerProvider { - - /** - * Returns a collection of all players eligible for migration. - * - * @return a non-null collection containing zero or more {@link OfflinePlayer} instances - */ - @NotNull Collection getAllPlayers(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/AsyncMigrationRunner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/AsyncMigrationRunner.java deleted file mode 100644 index f961edf..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/AsyncMigrationRunner.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.feature.migration.listener.ConfigMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.LoggerMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; -import org.panda_lang.utilities.inject.annotations.PostConstruct; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -public final class AsyncMigrationRunner - implements MigrationRunner>, AutoCloseable { - - private static final String EXECUTOR_THREAD_NAME = "playtime-async-migration-"; - - private final PluginLogger logger; - private final MigrationConfig config; - private final PlayerProvider provider; - private final PlayerMigrator migrator; - - private ExecutorService executor; - - @Inject - public AsyncMigrationRunner( - @NotNull PluginLogger logger, - @NotNull MigrationConfig config, - @NotNull PlayerProvider provider, - @NotNull PlayerMigrator migrator - ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.config = Validator.notNull(config, "config cannot be null"); - this.provider = Validator.notNull(provider, "provider cannot be null"); - this.migrator = Validator.notNull(migrator, "migrator cannot be null"); - } - - @PostConstruct - void postConstruct() { - this.executor = createNewExecutor(config.migrationMaxConcurrency, config.migrationKeepAliveInterval); - } - - @Override - public CompletableFuture execute() { - var runner = new MigrationRunnerImpl(config, provider, migrator, listeners()); - return CompletableFuture.supplyAsync(runner::execute, executor); - } - - @Override - public List listeners() { - return List.of( - new ConfigMigrationListener(config), - new LoggerMigrationListener(logger) - ); - } - - @Override - public void close() { - executor.shutdown(); - try { - if (!executor.awaitTermination(15, TimeUnit.SECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - - private ExecutorService createNewExecutor(int maxConcurrency, Duration keepAlive) { - return new ThreadPoolExecutor( - maxConcurrency, maxConcurrency, - keepAlive.toMillis(), TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), - newThreadFactory(), - new ThreadPoolExecutor.CallerRunsPolicy() - ); - } - - private ThreadFactory newThreadFactory() { - return new ThreadFactory() { - - private final ThreadFactory base = Executors.defaultThreadFactory(); - private final AtomicInteger seq = new AtomicInteger(1); - - @Override - public Thread newThread(@NotNull Runnable r) { - Thread thread = base.newThread(r); - thread.setName(EXECUTOR_THREAD_NAME + seq.getAndIncrement()); - thread.setDaemon(true); - return thread; - } - }; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/BlockingMigrationRunner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/BlockingMigrationRunner.java deleted file mode 100644 index 5a2ae6c..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/BlockingMigrationRunner.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.feature.migration.listener.ConfigMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.LoggerMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.List; - -public final class BlockingMigrationRunner implements MigrationRunner { - - private final PluginLogger logger; - private final MigrationConfig config; - private final PlayerProvider provider; - private final PlayerMigrator migrator; - - @Inject - public BlockingMigrationRunner( - @NotNull PluginLogger logger, - @NotNull MigrationConfig config, - @NotNull PlayerProvider provider, - @NotNull PlayerMigrator migrator - ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.config = Validator.notNull(config, "config cannot be null"); - this.provider = Validator.notNull(provider, "provider cannot be null"); - this.migrator = Validator.notNull(migrator, "migrator cannot be null"); - } - - @Override - public MigrationResult execute() { - final var runner = new MigrationRunnerImpl(config, provider, migrator, listeners()); - return runner.execute(); - } - - @Override - public List listeners() { - return List.of( - new ConfigMigrationListener(config), - new LoggerMigrationListener(logger) - ); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunner.java deleted file mode 100644 index 6e2175d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunner.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; - -import java.util.List; - -/** - * Executes a migration process according to a specific strategy and - * dispatches events to registered {@link MigrationListener} instances. - * - *

A {@code MigrationRunner} typically orchestrates:

- *
    - *
  • retrieving players from a {@code PlayerProvider},
  • - *
  • delegating work to a {@code PlayerMigrator},
  • - *
  • collecting results and computing statistics,
  • - *
  • notifying listeners about progress.
  • - *
- * - *

Implementations define whether execution is synchronous or asynchronous, - * and establish the threading model used for callbacks.

- * - * @param the type returned upon completion (e.g. {@code MigrationResult}) - */ -public interface MigrationRunner { - - /** - * Executes the migration process. - * - * @return a result object summarizing the completed migration - */ - T execute(); - - /** - * Returns all listeners that will receive migration callbacks. - * - * @return an immutable or defensive-copied list of listeners - */ - List listeners(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunnerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunnerImpl.java deleted file mode 100644 index e463420..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunnerImpl.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.google.common.base.Stopwatch; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - -final class MigrationRunnerImpl { - - private final MigrationConfig config; - private final PlayerProvider provider; - private final PlayerMigrator migrator; - private final List listeners; - - MigrationRunnerImpl( - @NotNull MigrationConfig config, - @NotNull PlayerProvider provider, - @NotNull PlayerMigrator migrator, - @NotNull List listeners - ) { - this.config = Validator.notNull(config, "config cannot be null"); - this.provider = Validator.notNull(provider, "provider cannot be null"); - this.migrator = Validator.notNull(migrator, "migrator cannot be null"); - this.listeners = Validator.notNull(listeners, "listeners cannot be null"); - } - - MigrationResult execute() { - if (!config.initialServerMigrationEnabled) { - return MigrationResult.empty(); - } - - final Stopwatch stopwatch = Stopwatch.createStarted(); - - final Collection players = provider.getAllPlayers(); - final int total = players.size(); - - listenersForEach(l -> l.onStart(total)); - if (total == 0) { - final MigrationResult empty = MigrationResult.empty(); - listenersForEach(l -> l.onEnd(empty)); - return empty; - } - - final AtomicInteger success = new AtomicInteger(); - final AtomicInteger failed = new AtomicInteger(); - final AtomicInteger inflight = new AtomicInteger(total); - - final Semaphore limiter = new Semaphore(config.migrationMaxConcurrency); - final CompletableFuture allDone = new CompletableFuture<>(); - - for (final OfflinePlayer player : players) { - limiter.acquireUninterruptibly(); - - migrator.migrate(player) - .orTimeout(config.migrationTaskTimeout.toMillis(), TimeUnit.MILLISECONDS) - .whenComplete((u, e) -> { - try { - if (e == null) { - success.incrementAndGet(); - listenersForEach(l -> l.onSuccess(player)); - } else { - failed.incrementAndGet(); - listenersForEach(l -> l.onFailed(player, e)); - } - } finally { - limiter.release(); - if (inflight.decrementAndGet() == 0) { - allDone.complete(null); - } - } - }); - } - - allDone.orTimeout(config.migrationGlobalTimeout.toMillis(), TimeUnit.MILLISECONDS).join(); - - final Duration took = stopwatch.stop().elapsed(); - final MigrationResult result = new MigrationResult(total, success.get(), failed.get(), took); - - listenersForEach(l -> l.onEnd(result)); - - return result; - } - - private void listenersForEach(@NotNull Consumer listenerConsumer) { - for (final var listener : listeners) { - listenerConsumer.accept(listener); - } - } - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java index f9d48c2..450179a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java @@ -1,7 +1,8 @@ package com.github.imdmk.playtime.feature.playtime; -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.UserTime; import org.bukkit.OfflinePlayer; import org.bukkit.Server; @@ -11,108 +12,48 @@ import java.util.UUID; -/** - * Implementation of {@link PlaytimeService} that retrieves and modifies player playtime data - * directly from the Bukkit {@link Server} statistics API. - *

- * This service operates exclusively on the primary (main) thread of the Bukkit server. - * Any access attempt from a non-primary thread will result in an {@link UnsupportedOperationException}. - *

- * The playtime is based on {@link Statistic#PLAY_ONE_MINUTE}, which internally stores values - * in Minecraft ticks (20 ticks = 1 second). The {@link UserTime} abstraction is used to - * convert between ticks and higher-level time units. - *

- * Thread-safety note: Bukkit statistic access is not thread-safe. - * Always ensure that invocations are done synchronously on the main thread. - */ -final class BukkitPlayTimeService implements PlaytimeService { +@Service(priority = Priority.LOWEST) +final class BukkitPlayTimeService implements PlayTimeService { private static final Statistic PLAYTIME_STATISTIC = Statistic.PLAY_ONE_MINUTE; - private static final UserTime ZERO_TIME = UserTime.ZERO; private final Server server; @Inject BukkitPlayTimeService(@NotNull Server server) { - this.server = Validator.notNull(server, "server"); + this.server = server; } - /** - * Retrieves the total playtime of the specified player. - * - * @param uuid the UUID of the target player (must not be {@code null}) - * @return a {@link UserTime} instance representing the player’s total playtime, - * or {@link UserTime#ZERO} if the player has never joined or has zero ticks recorded - * @throws UnsupportedOperationException if called from a non-primary thread - * @throws NullPointerException if {@code uuid} is {@code null} - */ @Override public @NotNull UserTime getTime(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid cannot be null"); - - if (!isPrimaryThread()) { - throw new UnsupportedOperationException( - "BukkitPlaytimeService#getTime must be called from the primary thread." - ); - } + checkPrimaryThread(); int timeTicks = getOffline(uuid).getStatistic(PLAYTIME_STATISTIC); if (timeTicks <= 0) { - return ZERO_TIME; + return UserTime.ZERO; } return UserTime.ofTicks(timeTicks); } - /** - * Sets the total playtime of the specified player to the given value. - * - * @param uuid the UUID of the target player (must not be {@code null}) - * @param time the desired new total playtime (must not be {@code null}) - * @throws UnsupportedOperationException if called from a non-primary thread - * @throws NullPointerException if any argument is {@code null} - */ @Override public void setTime(@NotNull UUID uuid, @NotNull UserTime time) { - Validator.notNull(uuid, "uuid cannot be null"); - Validator.notNull(time, "time cannot be null"); - - if (!isPrimaryThread()) { - throw new UnsupportedOperationException( - "BukkitPlaytimeService#setTime must be called from the primary thread." - ); - } - + checkPrimaryThread(); getOffline(uuid).setStatistic(PLAYTIME_STATISTIC, time.toTicks()); } - /** - * Resets the total playtime of the specified player to zero. - * - * @param uuid the UUID of the target player - * @throws UnsupportedOperationException if called from a non-primary thread - */ @Override public void resetTime(@NotNull UUID uuid) { - setTime(uuid, ZERO_TIME); + setTime(uuid, UserTime.ZERO); } - /** - * Retrieves the {@link OfflinePlayer} instance associated with the given UUID. - * - * @param uuid the player's UUID (must not be {@code null}) - * @return the corresponding {@link OfflinePlayer} handle - */ - private @NotNull OfflinePlayer getOffline(@NotNull UUID uuid) { + private OfflinePlayer getOffline(UUID uuid) { return server.getOfflinePlayer(uuid); } - /** - * Checks whether the current execution is happening on the primary (main) Bukkit thread. - * - * @return {@code true} if running on the main thread, otherwise {@code false} - */ - private boolean isPrimaryThread() { - return server.isPrimaryThread(); + private void checkPrimaryThread() { + if (!server.isPrimaryThread()) { + throw new UnsupportedOperationException("BukkitPlaytimeService must be called from the primary thread."); + } } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeModule.java deleted file mode 100644 index bad056f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeModule.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime; - -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.feature.playtime.command.TimeCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeResetAllCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeResetCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeSetCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeTopCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeTopInvalidateCommand; -import com.github.imdmk.playtime.feature.playtime.gui.PlayTimeTopGui; -import com.github.imdmk.playtime.feature.playtime.listener.PlayTimeSaveListener; -import com.github.imdmk.playtime.feature.playtime.placeholder.PlayTimePlaceholder; -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.GuiPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.ListenerPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.PlaceholderPhase; -import com.github.imdmk.playtime.user.UserFactory; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class PlayTimeModule implements Module { - - private PlaytimeService playtimeService; - private UserFactory userFactory; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(PlaytimeService.class).assignInstance(() -> this.playtimeService); - resources.on(UserFactory.class).assignInstance(() -> this.userFactory); - } - - @Override - public void init(@NotNull Injector injector) { - this.playtimeService = injector.newInstance(BukkitPlayTimeService.class); - this.userFactory = injector.newInstance(PlayTimeUserFactory.class); - } - - @Override - public CommandPhase commands(@NotNull Injector injector) { - return builder -> builder.commands( - injector.newInstance(TimeCommand.class), - injector.newInstance(TimeSetCommand.class), - injector.newInstance(TimeTopCommand.class), - injector.newInstance(TimeResetCommand.class), - injector.newInstance(TimeResetAllCommand.class), - injector.newInstance(TimeTopInvalidateCommand.class) - ); - } - - @Override - public ListenerPhase listeners(@NotNull Injector injector) { - return builder -> builder.register( - injector.newInstance(PlayTimeSaveListener.class) - ); - } - - @Override - public GuiPhase guis(@NotNull Injector injector) { - return guiRegistry -> guiRegistry.register(injector.newInstance(PlayTimeTopGui.class)); - } - - @Override - public PlaceholderPhase placeholders(@NotNull Injector injector) { - return adapter -> adapter.register( - injector.newInstance(PlayTimePlaceholder.class) - ); - } - - @Override - public int order() { - return -1; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java index 03b7552..30e00e0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.feature.playtime; -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserFactory; import com.github.imdmk.playtime.user.UserTime; @@ -15,34 +14,34 @@ /** * Concrete implementation of {@link UserFactory} that constructs {@link User} instances - * using data retrieved from the {@link PlaytimeService}. + * using data retrieved from the {@link PlayTimeService}. * *

This factory supports both online and offline players, resolving their unique identifiers, * last known names, and total recorded playtime from the underlying service.

* - *

Dependency: {@link PlaytimeService} is injected at runtime and must be available + *

Dependency: {@link PlayTimeService} is injected at runtime and must be available * before this factory is used.

* * @see User - * @see PlaytimeService + * @see PlayTimeService * @see UserFactory */ public final class PlayTimeUserFactory implements UserFactory { private static final String UNKNOWN_PLAYER_NAME_FORMAT = "Unknown:%s"; - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; @Inject - public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { - this.playtimeService = Validator.notNull(playtimeService, "playtimeService"); + public PlayTimeUserFactory(@NotNull PlayTimeService playtimeService) { + this.playtimeService = playtimeService; } /** * Creates a {@link User} instance from an online {@link Player}. * *

The user's UUID and current name are taken directly from the live {@link Player} object, - * and their total playtime is resolved via the {@link PlaytimeService}.

+ * and their total playtime is resolved via the {@link PlayTimeService}.

* * @param player non-null online player instance * @return new {@link User} representing the given player and their current playtime @@ -50,12 +49,9 @@ public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { */ @Override public @NotNull User createFrom(@NotNull Player player) { - Validator.notNull(player, "player cannot be null"); - final UUID uuid = player.getUniqueId(); final String name = player.getName(); final UserTime time = playtimeService.getTime(uuid); - return new User(uuid, name, time); } @@ -64,7 +60,7 @@ public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { * *

If the player's name cannot be resolved (e.g. first join or data missing), * a default placeholder name {@code "Unknown"} is used instead. - * The total playtime is fetched from {@link PlaytimeService} based on the player's UUID.

+ * The total playtime is fetched from {@link PlayTimeService} based on the player's UUID.

* * @param player non-null offline player instance * @return new {@link User} representing the offline player and their playtime data @@ -72,12 +68,9 @@ public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { */ @Override public @NotNull User createFrom(@NotNull OfflinePlayer player) { - Validator.notNull(player, "player cannot be null"); - final UUID uuid = player.getUniqueId(); final String name = Optional.ofNullable(player.getName()).orElse(UNKNOWN_PLAYER_NAME_FORMAT.formatted(uuid)); final UserTime time = playtimeService.getTime(uuid); - return new User(uuid, name, time); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java index b97e2f0..8638cfa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java @@ -1,9 +1,8 @@ package com.github.imdmk.playtime.feature.playtime.command; -import com.github.imdmk.playtime.PlaytimeService; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; import dev.rollczi.litecommands.annotations.argument.Arg; @@ -20,15 +19,15 @@ public final class TimeCommand { private final MessageService messageService; - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; @Inject public TimeCommand( @NotNull MessageService messageService, - @NotNull PlaytimeService playtimeService + @NotNull PlayTimeService playtimeService ) { - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); + this.messageService = messageService; + this.playtimeService = playtimeService; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java index 734463e..b41f98e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java @@ -1,10 +1,9 @@ package com.github.imdmk.playtime.feature.playtime.command; -import com.github.imdmk.playtime.PlaytimeService; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -32,7 +31,7 @@ public final class TimeResetAllCommand { private final Server server; private final PluginLogger logger; private final MessageService messageService; - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; private final UserService userService; private final UserRepository userRepository; private final TaskScheduler taskScheduler; @@ -42,18 +41,18 @@ public TimeResetAllCommand( @NotNull Server server, @NotNull PluginLogger logger, @NotNull MessageService messageService, - @NotNull PlaytimeService playtimeService, + @NotNull PlayTimeService playtimeService, @NotNull UserService userService, @NotNull UserRepository userRepository, @NotNull TaskScheduler taskScheduler ) { - this.server = Validator.notNull(server, "server cannot be null"); - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); - this.userRepository = Validator.notNull(userRepository, "userRepository cannot be null"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); + this.server = server; + this.logger = logger; + this.messageService = messageService; + this.playtimeService = playtimeService; + this.userService = userService; + this.userRepository = userRepository; + this.taskScheduler = taskScheduler; } @Execute @@ -83,7 +82,7 @@ void resetAll(@Context CommandSender sender) { }); } - private CompletableFuture resetUser(@NotNull User user) { + private CompletableFuture resetUser(User user) { user.setPlaytime(UserTime.ZERO); return userService.save(user, SAVE_REASON); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java index a48e233..c230f07 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -33,9 +32,9 @@ public TimeResetCommand( @NotNull MessageService messageService, @NotNull UserService userService ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); + this.logger = logger; + this.messageService = messageService; + this.userService = userService; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java index 4b01fa6..0abb4e8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java @@ -1,9 +1,9 @@ package com.github.imdmk.playtime.feature.playtime.command; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -26,17 +26,20 @@ public final class TimeSetCommand { private final PluginLogger logger; private final MessageService messageService; + private final PlayTimeService playTimeService; private final UserService userService; @Inject public TimeSetCommand( @NotNull PluginLogger logger, @NotNull MessageService messageService, + @NotNull PlayTimeService playTimeService, @NotNull UserService userService ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); + this.logger = logger; + this.messageService = messageService; + this.playTimeService = playTimeService; + this.userService = userService; } @Execute @@ -45,6 +48,7 @@ void setPlaytime(@Context CommandSender sender, @Arg @Async User target, @Arg Du final UserTime newTime = UserTime.ofDuration(normalizedTime); target.setPlaytime(newTime); + playTimeService.setTime(target.getUuid(), newTime); userService.save(target, UserSaveReason.SET_COMMAND) .thenAccept(v -> messageService.create() diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java index 6c2ca89..fd81b7c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java @@ -4,7 +4,6 @@ import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.gui.view.GuiOpener; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.UserService; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; @@ -32,10 +31,10 @@ public TimeTopCommand( @NotNull UserService userService, @NotNull GuiOpener guiOpener ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); - this.guiOpener = Validator.notNull(guiOpener, "guiOpener cannot be null"); + this.logger = logger; + this.messageService = messageService; + this.userService = userService; + this.guiOpener = guiOpener; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java index 78f5dc9..97dde2e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.feature.playtime.command; import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.top.TopUsersCache; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; @@ -23,8 +22,8 @@ public TimeTopInvalidateCommand( @NotNull MessageService messageService, @NotNull TopUsersCache topUsersCache ) { - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.topUsersCache = Validator.notNull(topUsersCache, "topUsersCache cannot be null"); + this.messageService = messageService; + this.topUsersCache = topUsersCache; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java index 004cc9e..ba9cea3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java @@ -18,7 +18,6 @@ import com.github.imdmk.playtime.platform.gui.view.ParameterizedGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -26,6 +25,7 @@ import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.builder.item.SkullBuilder; import dev.triumphteam.gui.guis.BaseGui; +import dev.triumphteam.gui.guis.GuiItem; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; @@ -61,28 +61,22 @@ public PlayTimeTopGui( @NotNull UserService userService ) { super(navigationBarConfig, taskScheduler, GUI_RENDERER, RENDER_OPTIONS); - this.server = Validator.notNull(server, "server"); - this.topGuiConfig = Validator.notNull(topGuiConfig, "playtimeTopGuiConfig cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); + this.server = server; + this.topGuiConfig = topGuiConfig; + this.messageService = messageService; + this.userService = userService; } @Override public @NotNull BaseGui createGui(@NotNull Player viewer, @NotNull List users) { - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(users, "users cannot be null"); return GuiFactory.build(topGuiConfig, BaseGui::disableAllInteractions); } @Override public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull List users) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(users, "users cannot be null"); - if (topGuiConfig.fillBorder) { - final var border = ItemGuiTransformer.toGuiItem(topGuiConfig.borderItem); - gui.getFiller().fillBorder(border); + final GuiItem borderItem = ItemGuiTransformer.toGuiItem(topGuiConfig.borderItem); + gui.getFiller().fillBorder(borderItem); } placeExit(gui, viewer, e -> gui.close(viewer)); @@ -92,14 +86,14 @@ public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull placePrevious(gui, viewer); } - final var context = RenderContext.defaultContext(viewer); - final var item = resolveItemFor(viewer, context); + final RenderContext context = RenderContext.defaultContext(viewer); + final ItemGui item = resolveItemFor(viewer, context); for (int i = 0; i < users.size(); i++) { final User user = users.get(i); final int position = i + 1; - final var placeholders = createPlaceholders(user, position); + final AdventurePlaceholders placeholders = createPlaceholders(user, position); final Consumer onClick = (click) -> { if (click.getClick() != topGuiConfig.resetClickType) { @@ -131,9 +125,8 @@ public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull } private ItemGui resolveItemFor(Player viewer, RenderContext context) { - final var adminItem = topGuiConfig.playerEntryAdminItem; - final var item = topGuiConfig.playerEntryItem; - + final ItemGui adminItem = topGuiConfig.playerEntryAdminItem; + final ItemGui item = topGuiConfig.playerEntryItem; return ITEM_VARIANT_RESOLVER.resolve(viewer, context, List.of(adminItem), item); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java index fd0bab0..76f5605 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.feature.playtime.gui; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import com.github.imdmk.playtime.platform.adventure.AdventureComponents; import com.github.imdmk.playtime.platform.gui.GuiType; import com.github.imdmk.playtime.platform.gui.config.ConfigurableGui; @@ -14,6 +15,7 @@ import java.util.Collections; +@ConfigFile public final class PlayTimeTopGuiConfig extends OkaeriConfig implements ConfigurableGui { @Comment({ diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java new file mode 100644 index 0000000..eecdad2 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java @@ -0,0 +1,42 @@ +package com.github.imdmk.playtime.feature.playtime.listener; + +import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.Controller; +import com.github.imdmk.playtime.user.User; +import com.github.imdmk.playtime.user.UserSaveReason; +import com.github.imdmk.playtime.user.UserService; +import com.github.imdmk.playtime.user.UserTime; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.UUID; + +@Controller +public final class PlayTimeSaveController implements Listener { + + private final UserService userService; + private final PlayTimeService playtimeService; + + @Inject + public PlayTimeSaveController( + @NotNull UserService userService, + @NotNull PlayTimeService playtimeService + ) { + this.userService = userService; + this.playtimeService = playtimeService; + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + final UUID uuid = event.getPlayer().getUniqueId(); + + final User user = userService.findCachedByUuid(uuid).orElseThrow(); + final UserTime time = playtimeService.getTime(uuid); + + user.setPlaytime(time); + userService.save(user, UserSaveReason.PLAYER_LEAVE); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveListener.java deleted file mode 100644 index d1d0a78..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveListener.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.listener; - -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.UserPreSaveEvent; -import com.github.imdmk.playtime.UserSaveEvent; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserTime; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.UUID; - -public final class PlayTimeSaveListener implements Listener { - - private final PlaytimeService playtimeService; - - @Inject - public PlayTimeSaveListener(@NotNull PlaytimeService playtimeService) { - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onUserPreSave(UserPreSaveEvent event) { - final User user = event.getUser(); - final UUID uuid = user.getUuid(); - final UserSaveReason reason = event.getReason(); - - if (reason == UserSaveReason.PLAYER_LEAVE) { - final UserTime currentPlaytime = playtimeService.getTime(uuid); - user.setPlaytime(currentPlaytime); - } - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserSave(UserSaveEvent event) { - final User user = event.getUser(); - playtimeService.setTime(user.getUuid(), user.getPlaytime()); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java index 1f3a1f3..42560a0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java @@ -1,9 +1,9 @@ package com.github.imdmk.playtime.feature.playtime.placeholder; -import com.github.imdmk.playtime.PlaytimeService; +import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.Placeholder; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -11,13 +11,14 @@ import java.util.UUID; +@Placeholder public final class PlayTimePlaceholder implements PluginPlaceholder { - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; @Inject - public PlayTimePlaceholder(@NotNull PlaytimeService playtimeService) { - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); + public PlayTimePlaceholder(@NotNull PlayTimeService playtimeService) { + this.playtimeService = playtimeService; } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java index 7a5dbaf..8045c0d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java @@ -4,7 +4,6 @@ import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; @@ -30,10 +29,10 @@ public ReloadCommand( @NotNull TaskScheduler taskScheduler, @NotNull MessageService messageService ) { - this.logger = Validator.notNull(logger, "logger"); - this.configService = Validator.notNull(configService, "configManager"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler"); - this.messageService = Validator.notNull(messageService, "messageService"); + this.logger = logger; + this.configService = configService; + this.taskScheduler = taskScheduler; + this.messageService = messageService; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadModule.java deleted file mode 100644 index d3b6dbc..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadModule.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.feature.reload; - -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class ReloadModule implements Module { - - @Override - public void bind(@NotNull Resources resources) {} - - @Override - public void init(@NotNull Injector injector) {} - - @Override - public CommandPhase commands(@NotNull Injector injector) { - return builder -> builder.commands(injector.newInstance(ReloadCommand.class)); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java index cf1c1f9..9f27cec 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -1,11 +1,11 @@ package com.github.imdmk.playtime.injector; +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.priority.PriorityProvider; import com.github.imdmk.playtime.injector.processor.ComponentProcessor; import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; import com.github.imdmk.playtime.injector.processor.FunctionalComponentProcessor; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Injector; @@ -19,15 +19,15 @@ public final class ComponentManager { private final Injector injector; - private final ComponentContainer container; + private final ComponentQueue container; private final ComponentScanner scanner; private final Map, ComponentProcessor> processors = new ConcurrentHashMap<>(); public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) { - this.injector = Validator.notNull(injector, "injector"); + this.injector = injector; - this.container = new ComponentContainer(DEFAULT_PRIORITY); + this.container = new ComponentQueue(DEFAULT_PRIORITY); this.scanner = new ComponentScanner(basePackage); } @@ -60,6 +60,12 @@ public ComponentManager onProcess( return addProcessor(new FunctionalComponentProcessor<>(annotation, targetType, consumer)); } + public ComponentManager onProcess( + @NotNull ComponentFunctional consumer + ) { + return onProcess(NoneAnnotation.class, Object.class, consumer); + } + public void scanAll() { for (final Class annotation : processors.keySet()) { scanner.scan(annotation).forEach(container::add); @@ -68,7 +74,7 @@ public void scanAll() { public void processAll() { for (final ComponentProcessor processor : processors.values()) { - container.consume(processor.annotation()) + container.drain(processor.annotation()) .forEach(component -> process(processor, component)); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentContainer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java similarity index 81% rename from playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentContainer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java index bf3b4cb..7706750 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentContainer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.priority.PriorityProvider; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; @@ -14,7 +13,7 @@ import java.util.List; import java.util.Map; -final class ComponentContainer { +final class ComponentQueue { private final Object lock = new Object(); @@ -22,7 +21,7 @@ final class ComponentContainer { private PriorityProvider priorityProvider; - ComponentContainer(@NotNull PriorityProvider priorityProvider) { + ComponentQueue(@NotNull PriorityProvider priorityProvider) { setPriorityProvider(priorityProvider); for (final Priority priority : Priority.values()) { @@ -30,11 +29,11 @@ final class ComponentContainer { } } - void setPriorityProvider(PriorityProvider priorityProvider) { - this.priorityProvider = Validator.notNull(priorityProvider, "priorityProvider"); + void setPriorityProvider(@NotNull PriorityProvider priorityProvider) { + this.priorityProvider = priorityProvider; } - void add(Component component) { + void add(@NotNull Component component) { final Priority priority = this.priorityProvider.apply(component); synchronized (this.lock) { @@ -48,7 +47,7 @@ void add(Component component) { } } - List> consume(Class annotation) { + List> drain(@NotNull Class annotation) { final List> result = new ArrayList<>(); synchronized (this.lock) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java index 16a4b5a..a342719 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java @@ -1,10 +1,11 @@ package com.github.imdmk.playtime.injector; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; import org.jetbrains.annotations.NotNull; import org.reflections.Reflections; import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; import java.util.Set; import java.util.stream.Collectors; @@ -13,12 +14,21 @@ final class ComponentScanner { private final Reflections reflections; ComponentScanner(@NotNull String basePackage) { - Validator.notNull(basePackage, "basePackage"); this.reflections = new Reflections(basePackage); } Set> scan(@NotNull Class annotation) { - Validator.notNull(annotation, "annotation"); + if (annotation == NoneAnnotation.class) { + return reflections.getSubTypesOf(Object.class).stream() + .filter(type -> !type.isInterface()) + .filter(type -> !Modifier.isAbstract(type.getModifiers())) + .map(type -> new Component<>( + type, + NoneAnnotation.INSTANCE + )) + .collect(Collectors.toSet()); + } + return reflections.getTypesAnnotatedWith(annotation).stream() .map(type -> new Component<>( type, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java index f5f4688..e481087 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java @@ -11,10 +11,6 @@ @Target(ElementType.TYPE) public @interface ConfigFile { - Priority priority() default Priority.LOWEST; - - String name(); - - Class[] serdes() default {}; + Priority priority() default Priority.LOW; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Listener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java similarity index 82% rename from playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Listener.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java index 59962f4..235b889 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Listener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.injector.annotations; -import com.github.imdmk.playtime.injector.Component; import com.github.imdmk.playtime.injector.priority.Priority; import java.lang.annotation.ElementType; @@ -10,7 +9,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -public @interface Listener { +public @interface Controller { Priority priority() default Priority.HIGHEST; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java new file mode 100644 index 0000000..42dad5a --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.injector.annotations; + +import java.lang.annotation.Annotation; + +public class NoneAnnotation implements Annotation { + + public static final NoneAnnotation INSTANCE = new NoneAnnotation(); + + @Override + public Class annotationType() { + return NoneAnnotation.class; + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java new file mode 100644 index 0000000..ca19620 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Placeholder { + + Priority priority() default Priority.HIGHEST; + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java new file mode 100644 index 0000000..08e1c1d --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Repository { + + Priority priority() default Priority.HIGH; + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java index a96c2eb..c8c3945 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java @@ -11,6 +11,6 @@ @Target(ElementType.TYPE) public @interface Task { - Priority priority() default Priority.NORMAL; + Priority priority() default Priority.HIGHEST; } \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java index 3f3d9db..e327c94 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.injector.priority; import com.github.imdmk.playtime.injector.Component; +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -11,6 +12,10 @@ public final class AnnotationPriorityProvider implements PriorityProvider { public Priority apply(Component component) { final Class componentClass = component.type(); + if (component.annotation().annotationType() == NoneAnnotation.class) { + return Priority.HIGHEST; + } + for (final Annotation annotation : componentClass.getAnnotations()) { try { final Method method = annotation.annotationType().getMethod("priority"); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java index 6a464a1..df64683 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.injector.processor; import com.github.imdmk.playtime.injector.Component; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; @@ -11,9 +10,6 @@ public abstract class AbstractComponentProcessor
@Override public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { - Validator.notNull(component, "component"); - Validator.notNull(context, "context"); - final Object instance = context.injector().newInstance(component.type()); this.handle(instance, component.annotation(), context); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java index ef9659e..e29ee84 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java @@ -1,12 +1,7 @@ package com.github.imdmk.playtime.injector.processor; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Injector; public record ComponentProcessorContext(@NotNull Injector injector) { - - public ComponentProcessorContext { - Validator.notNull(injector, "injector"); - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java index 59f2c14..059312f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.injector.Component; import com.github.imdmk.playtime.injector.ComponentFunctional; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; @@ -19,9 +18,9 @@ public FunctionalComponentProcessor( @NotNull Class targetType, @NotNull ComponentFunctional consumer ) { - this.annotationType = Validator.notNull(annotationType, "annotationType"); - this.targetType = Validator.notNull(targetType, "targetType"); - this.consumer = Validator.notNull(consumer, "consumer"); + this.annotationType = annotationType; + this.targetType = targetType; + this.consumer = consumer; } @Override @@ -32,11 +31,7 @@ public FunctionalComponentProcessor( @Override @SuppressWarnings("unchecked") public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { - Validator.notNull(component, "component"); - Validator.notNull(context, "context"); - final Object instance = context.injector().newInstance(component.type()); - if (!targetType.isInstance(instance)) { return; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java new file mode 100644 index 0000000..cc2288b --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -0,0 +1,64 @@ +package com.github.imdmk.playtime.injector.subscriber; + +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Injector; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service(priority = Priority.LOWEST) +public final class LocalPublisher implements Publisher { + + private final Map, List> subscribers = new HashMap<>(); + + private final Injector injector; + + @Inject + public LocalPublisher(@NotNull Injector injector) { + this.injector = injector; + } + + @Override + public void subscribe(@NotNull Object instance) { + for (final Method method : instance.getClass().getDeclaredMethods()) { + final Subscribe subscribe = method.getAnnotation(Subscribe.class); + if (subscribe == null) { + continue; + } + + final Class eventType = subscribe.event(); + method.setAccessible(true); + + subscribers + .computeIfAbsent(eventType, k -> new ArrayList<>()) + .add(new SubscriberMethod(instance, method)); + } + } + + @Override + public E publish(@NotNull E event) { + final List list = subscribers.get(event.getClass()); + if (list == null) { + return event; + } + + for (final SubscriberMethod subscriber : list) { + final Object instance = subscriber.instance(); + final Method method = subscriber.method(); + injector.invokeMethod(method, instance, event); + } + + return event; + } + + private record SubscriberMethod(@NotNull Object instance, @NotNull Method method) { + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Publisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Publisher.java new file mode 100644 index 0000000..c89eefc --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Publisher.java @@ -0,0 +1,12 @@ +package com.github.imdmk.playtime.injector.subscriber; + +import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; +import org.jetbrains.annotations.NotNull; + +public interface Publisher { + + void subscribe(@NotNull Object subscriber); + + E publish(@NotNull E event); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Subscribe.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Subscribe.java new file mode 100644 index 0000000..ac76356 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Subscribe.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector.subscriber; + +import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Subscribe { + + Class event(); + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeInitializeEvent.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeInitializeEvent.java new file mode 100644 index 0000000..22389b9 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeInitializeEvent.java @@ -0,0 +1,4 @@ +package com.github.imdmk.playtime.injector.subscriber.event; + +public final class PlayTimeInitializeEvent extends SubscribeEvent { +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeShutdownEvent.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeShutdownEvent.java new file mode 100644 index 0000000..f05458c --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeShutdownEvent.java @@ -0,0 +1,4 @@ +package com.github.imdmk.playtime.injector.subscriber.event; + +public final class PlayTimeShutdownEvent extends SubscribeEvent { +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/SubscribeEvent.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/SubscribeEvent.java new file mode 100644 index 0000000..353ea75 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/SubscribeEvent.java @@ -0,0 +1,4 @@ +package com.github.imdmk.playtime.injector.subscriber.event; + +public abstract class SubscribeEvent { +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java index 0167806..1b8b7a3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java @@ -6,10 +6,12 @@ import com.github.imdmk.playtime.config.ConfigSection; import com.github.imdmk.playtime.feature.playtime.messages.ENPlayTimeMessages; import com.github.imdmk.playtime.feature.reload.messages.ENReloadMessages; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; +@ConfigFile public final class MessageConfig extends ConfigSection { @Comment({ @@ -89,14 +91,14 @@ public final class MessageConfig extends ConfigSection { public ENReloadMessages reloadMessages = new ENReloadMessages(); @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> registry.register( new MultificationSerdesPack(NoticeResolverDefaults.createRegistry()) ); } @Override - public @NotNull String getFileName() { + public @NotNull String fileName() { return "messageConfig.yml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java index 6cf5568..53d0abf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java @@ -2,100 +2,44 @@ import com.eternalcode.multification.adventure.AudienceConverter; import com.eternalcode.multification.bukkit.BukkitMultification; -import com.eternalcode.multification.notice.provider.NoticeProvider; import com.eternalcode.multification.translation.TranslationProvider; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; +import com.github.imdmk.playtime.platform.adventure.AdventureComponents; import net.kyori.adventure.platform.AudienceProvider; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.ComponentSerializer; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; -/** - * Central message service for the PlayTime plugin, bridging plugin messages with - * the Adventure and MiniMessage APIs through {@link BukkitMultification}. - * - *

This implementation provides a high-level abstraction for sending messages, - * notices, and components to Bukkit {@link CommandSender}s, automatically converting - * them into Adventure audiences via {@link AudienceProvider}.

- * - *

Features:

- *
    - *
  • Uses {@link MessageConfig} as the single translation source (locale-agnostic).
  • - *
  • Serializes and deserializes Adventure {@link Component}s using {@link MiniMessage}.
  • - *
  • Converts Bukkit {@link CommandSender}s into Adventure audiences automatically.
  • - *
  • Supports both {@link Player} and console senders transparently.
  • - *
- * - *

Thread-safety: Message sending is thread-safe and may be performed - * off the main thread. Underlying Adventure components are immutable and safe for reuse.

- * - * @see MessageConfig - * @see BukkitMultification - * @see MiniMessage - * @see AudienceProvider - * @see NoticeProvider - */ +@Service(priority = Priority.LOW) public final class MessageService extends BukkitMultification { - private static final MiniMessage DEFAULT_MINI_MESSAGE = MiniMessage.miniMessage(); - private final MessageConfig messageConfig; private final AudienceProvider audienceProvider; - private final MiniMessage miniMessage; - - public MessageService( - @NotNull MessageConfig messageConfig, - @NotNull AudienceProvider audienceProvider, - @NotNull MiniMessage miniMessage - ) { - this.messageConfig = Validator.notNull(messageConfig, "messageConfig"); - this.audienceProvider = Validator.notNull(audienceProvider, "audienceProvider"); - this.miniMessage = Validator.notNull(miniMessage, "miniMessage"); - } - public MessageService(@NotNull MessageConfig messageConfig, @NotNull BukkitAudiences bukkitAudiences) { - this(messageConfig, bukkitAudiences, DEFAULT_MINI_MESSAGE); + @Inject + public MessageService(@NotNull Plugin plugin, @NotNull MessageConfig messageConfig) { + this.messageConfig = messageConfig; + this.audienceProvider = BukkitAudiences.create(plugin); } - public MessageService(@NotNull MessageConfig messageConfig, @NotNull Plugin plugin) { - this(messageConfig, BukkitAudiences.create(plugin), DEFAULT_MINI_MESSAGE); - } - - /** - * Returns a translation provider that always returns the same {@link MessageConfig} instance, - * ignoring locale differences. - * - * @return locale-agnostic translation provider - */ @Override protected @NotNull TranslationProvider translationProvider() { return provider -> messageConfig; } - /** - * Returns the {@link MiniMessage}-based component serializer. - * - * @return component serializer for text serialization/deserialization - */ @Override protected @NotNull ComponentSerializer serializer() { - return miniMessage; + return AdventureComponents.miniMessage(); } - /** - * Converts Bukkit {@link CommandSender}s into Adventure audiences - * using the configured {@link AudienceProvider}. - * - *

Players are mapped to player audiences, while other senders - * (e.g., console or command blocks) are mapped to {@link AudienceProvider#console()}.

- * - * @return non-null audience converter - */ @Override protected @NotNull AudienceConverter audienceConverter() { return sender -> { @@ -106,30 +50,8 @@ public MessageService(@NotNull MessageConfig messageConfig, @NotNull Plugin plug }; } - /** - * Sends a localized or static notice message to the specified Bukkit {@link CommandSender}. - * - *

The notice is resolved through the active {@link MessageConfig} - * and rendered using {@link MiniMessage} formatting.

- * - * @param sender non-null Bukkit command sender (player, console, etc.) - * @param notice non-null notice provider bound to {@link MessageConfig} - * @throws NullPointerException if {@code sender} or {@code notice} is null - */ - public void send(@NotNull CommandSender sender, @NotNull NoticeProvider notice) { - Validator.notNull(sender, "sender"); - Validator.notNull(notice, "notice"); - create().viewer(sender).notice(notice).send(); - } - - /** - * Shuts down the underlying {@link AudienceProvider} to release Adventure resources. - * - *

This should be called during plugin disable to avoid memory leaks or - * lingering references to the plugin classloader.

- */ + @Subscribe(event = PlayTimeShutdownEvent.class) public void shutdown() { - Validator.notNull(audienceProvider, "audienceProvider cannot be null"); audienceProvider.close(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/ComponentSerializer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponentSerializer.java similarity index 91% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/ComponentSerializer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponentSerializer.java index f4ce438..6ecaad9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/ComponentSerializer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponentSerializer.java @@ -7,7 +7,7 @@ import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; -public final class ComponentSerializer implements ObjectSerializer { +public final class AdventureComponentSerializer implements ObjectSerializer { @Override public boolean supports(@NotNull Class type) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java index 9de1aea..5639c0a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.adventure; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.format.TextDecoration; @@ -11,21 +10,6 @@ import java.util.Collection; import java.util.List; -/** - * Utilities for working with Adventure {@link Component}s via MiniMessage. - * Platform-agnostic (no Bukkit types). Thread-safe and stateless. - * - *

Notes: - *

    - *
  • All returned collections are unmodifiable.
  • - *
  • Accepts {@link CharSequence} for flexibility.
  • - *
- * - *
- *   Component c = AdventureComponents.text("<red>Hello");
- *   Component plain = AdventureComponents.withoutItalics(c);
- * 
- */ public final class AdventureComponents { private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); @@ -34,26 +18,11 @@ private AdventureComponents() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Deserializes a MiniMessage-formatted text into a {@link Component}. - * - * @param text the MiniMessage-formatted text - * @return the deserialized component - */ - public static @NotNull Component text(@NotNull CharSequence text) { - Validator.notNull(text, "text"); + public static Component text(@NotNull CharSequence text) { return MINI_MESSAGE.deserialize(text.toString()); } - /** - * Deserializes multiple MiniMessage-formatted texts into a list of {@link Component}s. - * - * @param texts array of MiniMessage-formatted texts - * @return an unmodifiable list of deserialized components - */ - public static @NotNull List text(@NotNull CharSequence... texts) { - Validator.notNull(texts, "texts"); - + public static List text(@NotNull CharSequence... texts) { final List out = new ArrayList<>(texts.length); for (CharSequence text : texts) { out.add(MINI_MESSAGE.deserialize(text.toString())); @@ -62,121 +31,59 @@ private AdventureComponents() { return List.copyOf(out); } - /** - * Deserializes a collection of MiniMessage-formatted texts into {@link Component}s. - * - * @param texts iterable of MiniMessage-formatted texts - * @return an unmodifiable list of deserialized components - */ - public static @NotNull List text(@NotNull Iterable texts) { - Validator.notNull(texts, "texts"); - + public static List text(@NotNull Iterable texts) { final List out = new ArrayList<>(); for (CharSequence text : texts) { - Validator.notNull(text, "texts contains null element"); out.add(MINI_MESSAGE.deserialize(text.toString())); } return List.copyOf(out); } - /** - * Returns a copy of the given component with italics disabled. - * - * @param component the source component - * @return a new component without italics - */ - public static @NotNull Component withoutItalics(@NotNull Component component) { - Validator.notNull(component, "component"); + public static Component withoutItalics(@NotNull Component component) { return component.decoration(TextDecoration.ITALIC, false); } - /** - * Deserializes a MiniMessage-formatted text and removes italics. - * - * @param text the MiniMessage-formatted text - * @return a deserialized component without italics - */ - public static @NotNull Component withoutItalics(@NotNull CharSequence text) { + public static Component withoutItalics(@NotNull CharSequence text) { return withoutItalics(text(text)); } - /** - * Converts a {@link ComponentLike} into a {@link Component} and removes italics. - * - * @param like the source component-like object - * @return a new component without italics - */ - public static @NotNull Component withoutItalics(@NotNull ComponentLike like) { - Validator.notNull(like, "component"); + public static Component withoutItalics(@NotNull ComponentLike like) { return like.asComponent().decoration(TextDecoration.ITALIC, false); } - /** - * Disables italics for all given components. - * - * @param strings iterable of strings objects - * @return an unmodifiable list of components with italics disabled - */ - public static @NotNull List withoutItalics(@NotNull String... strings) { - Validator.notNull(strings, "components cannot be null"); - + public static List withoutItalics(@NotNull String... strings) { final List out = new ArrayList<>(); for (final String string : strings) { - Validator.notNull(string, "components contains null element"); out.add(withoutItalics(string)); } return List.copyOf(out); } - /** - * Serializes a {@link Component} into a MiniMessage-formatted string. - * - * @param component the component to serialize - * @return the serialized MiniMessage string - */ - public static @NotNull String serialize(@NotNull Component component) { - Validator.notNull(component, "component cannot be null"); + public static String serialize(@NotNull Component component) { return MINI_MESSAGE.serialize(component); } - /** - * Serializes multiple components into MiniMessage-formatted strings. - * - * @param components collection of component-like objects - * @return an unmodifiable list of serialized strings - */ - public static @NotNull List serialize(@NotNull Collection components) { - Validator.notNull(components, "components cannot be null"); - + public static List serialize(@NotNull Collection components) { final List out = new ArrayList<>(components.size()); for (final ComponentLike component : components) { - Validator.notNull(component, "components contains null element"); out.add(MINI_MESSAGE.serialize(component.asComponent())); } return List.copyOf(out); } - /** - * Serializes multiple components and joins them with the given delimiter. - * - * @param components collection of component-like objects - * @param delimiter string separator between serialized components - * @return a single joined MiniMessage string - */ - public static @NotNull String serializeJoined(@NotNull Collection components, - @NotNull CharSequence delimiter) { - Validator.notNull(components, "components cannot be null"); - Validator.notNull(delimiter, "delimiter cannot be null"); - + public static String serializeJoined(@NotNull Collection components, @NotNull CharSequence delimiter) { final List serialized = new ArrayList<>(components.size()); for (final ComponentLike component : components) { - Validator.notNull(component, "components contains null element"); serialized.add(MINI_MESSAGE.serialize(component.asComponent())); } return String.join(delimiter, serialized); } + + public static MiniMessage miniMessage() { + return MINI_MESSAGE; + } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java index 744887d..2988ba1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java @@ -1,8 +1,6 @@ package com.github.imdmk.playtime.platform.adventure; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextReplacementConfig; import org.jetbrains.annotations.NotNull; import java.util.Comparator; @@ -10,75 +8,36 @@ import java.util.Map; import java.util.stream.Collectors; -/** - * Utility for applying {@link AdventurePlaceholders} to {@link Component} trees or plain strings. - *

Stateless and thread-safe.

- */ public final class AdventureFormatter { private AdventureFormatter() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - /** - * Applies placeholders to a plain string and returns a formatted {@link Component}. - * - * @param input plain text input - * @param placeholders placeholders to apply - * @return formatted component - */ - public static @NotNull Component format(@NotNull String input, @NotNull AdventurePlaceholders placeholders) { - Validator.notNull(input, "input"); + public static Component format(@NotNull String input, @NotNull AdventurePlaceholders placeholders) { return format(AdventureComponents.text(input), placeholders); } - /** - * Applies placeholders to each {@link Component} in a list. - * - * @param components list of components - * @param placeholders placeholders to apply - * @return formatted components - */ - public static @NotNull List format(@NotNull List components, @NotNull AdventurePlaceholders placeholders) { - Validator.notNull(components, "components"); + public static List format(@NotNull List components, @NotNull AdventurePlaceholders placeholders) { return components.stream() .map(component -> format(component, placeholders)) .collect(Collectors.toList()); } - /** - * Applies placeholders to a single {@link Component}. - * - * @param input component to format - * @param placeholders placeholders to apply - * @return formatted component - */ - public static @NotNull Component format(@NotNull Component input, @NotNull AdventurePlaceholders placeholders) { - Validator.notNull(input, "input"); - Validator.notNull(placeholders, "placeholders"); - + public static Component format(@NotNull Component input, @NotNull AdventurePlaceholders placeholders) { if (placeholders.isEmpty()) { return input; } // Sort keys by descending length to avoid substring overlap - List> ordered = placeholders.asMap().entrySet().stream() + final List> ordered = placeholders.asMap().entrySet().stream() .sorted(Comparator.>comparingInt(e -> e.getKey().length()).reversed()) - .collect(Collectors.toList()); - - Component out = input; - for (final Map.Entry e : ordered) { - var key = e.getKey(); - var replacement = e.getValue(); - - var config = TextReplacementConfig.builder() - .matchLiteral(key) - .replacement(replacement) - .build(); - - out = out.replaceText(config); - } + .toList(); - return out; + return input.replaceText(builder -> { + for (var entry : ordered) { + builder.matchLiteral(entry.getKey()).replacement(entry.getValue()); + } + }); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java index 5a7926c..7481fec 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.adventure; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -10,13 +9,6 @@ import java.util.LinkedHashMap; import java.util.Map; -/** - * Immutable container mapping literal placeholder keys to Adventure {@link Component} values. - *

- * Instances are created via the {@link Builder}. Once built, the mapping is read-only. - *

- * Thread-safety: Fully immutable and safe for concurrent use. - */ public final class AdventurePlaceholders { private static final AdventurePlaceholders EMPTY = new AdventurePlaceholders(Map.of()); @@ -24,130 +16,59 @@ public final class AdventurePlaceholders { private final Map map; private AdventurePlaceholders(@NotNull Map map) { - Validator.notNull(map, "map"); this.map = Collections.unmodifiableMap(map); } - /** - * Returns an unmodifiable view of all placeholder mappings. - * - * @return unmodifiable placeholder map - */ @Unmodifiable - @NotNull public Map asMap() { return map; } - /** - * Returns the number of registered placeholders. - * - * @return placeholder count - */ public int size() { return map.size(); } - /** - * Checks if the placeholder map is empty. - * - * @return {@code true} if no placeholders are defined - */ public boolean isEmpty() { return map.isEmpty(); } - /** - * Returns a shared immutable empty instance. - * - * @return empty placeholder container - */ - public static @NotNull AdventurePlaceholders empty() { + public static AdventurePlaceholders empty() { return EMPTY; } - /** - * Creates a new builder for {@link AdventurePlaceholders}. - * - * @return new builder instance - */ - public static @NotNull Builder builder() { + public static Builder builder() { return new Builder(); } - /** - * Fluent builder for {@link AdventurePlaceholders}. - */ public static final class Builder { private final Map entries = new LinkedHashMap<>(); - /** - * Adds a literal → component mapping. - * - * @param key literal placeholder key - * @param value replacement component - * @return this builder for chaining - */ @Contract("_,_ -> this") - public @NotNull Builder with(@NotNull String key, @NotNull Component value) { - Validator.notNull(key, "key"); - Validator.notNull(value, "value"); + public Builder with(@NotNull String key, @NotNull Component value) { this.entries.put(key, value); return this; } - /** - * Adds a literal → plain text mapping (converted to {@link Component#text(String)}). - * - * @param key literal placeholder key - * @param value replacement text - * @return this builder for chaining - */ @Contract("_,_ -> this") - public @NotNull Builder with(@NotNull String key, @NotNull String value) { - Validator.notNull(key, "key cannot be null"); - Validator.notNull(value, "value cannot be null"); + public Builder with(@NotNull String key, @NotNull String value) { this.entries.put(key, Component.text(value)); return this; } - /** - * Adds all entries from another {@link AdventurePlaceholders}. - * - * @param other another placeholder container - * @return this builder for chaining - */ @Contract("_ -> this") - public @NotNull Builder with(@NotNull AdventurePlaceholders other) { - Validator.notNull(other, "other cannot be null"); + public Builder with(@NotNull AdventurePlaceholders other) { this.entries.putAll(other.asMap()); return this; } - /** - * Adds a placeholder using any object value. - * The value is converted to plain text via {@link String#valueOf(Object)}. - * - * @param key placeholder key - * @param value object to convert and insert - * @return this builder for chaining - * @throws NullPointerException if key or value is null - */ @Contract("_,_ -> this") - public @NotNull Builder with(@NotNull String key, @NotNull Object value) { - Validator.notNull(key, "key cannot be null"); - Validator.notNull(value, "value cannot be null"); + public Builder with(@NotNull String key, @NotNull Object value) { this.entries.put(key, Component.text(String.valueOf(value))); return this; } - /** - * Builds an immutable {@link AdventurePlaceholders} instance. - * - * @return immutable placeholder container - */ - public @NotNull AdventurePlaceholders build() { + public AdventurePlaceholders build() { if (this.entries.isEmpty()) { return EMPTY; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java new file mode 100644 index 0000000..f8a6203 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java @@ -0,0 +1,34 @@ +package com.github.imdmk.playtime.platform.event; + +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import org.bukkit.Server; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@Service(priority = Priority.LOW) +public final class BukkitEventCaller implements EventCaller { + + private final Server server; + private final TaskScheduler scheduler; + + @Inject + public BukkitEventCaller(@NotNull Server server, @NotNull TaskScheduler scheduler) { + this.server = server; + this.scheduler = scheduler; + } + + @Override + public E callEvent(@NotNull E event) { + if (event.isAsynchronous() || server.isPrimaryThread()) { + server.getPluginManager().callEvent(event); + return event; + } + + scheduler.runSync(() -> server.getPluginManager().callEvent(event)); + return event; + } + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/EventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/EventCaller.java new file mode 100644 index 0000000..4de83c9 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/EventCaller.java @@ -0,0 +1,10 @@ +package com.github.imdmk.playtime.platform.event; + +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; + +public interface EventCaller { + + E callEvent(@NotNull E event); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitEventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitEventCaller.java deleted file mode 100644 index 5a7a3a5..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitEventCaller.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.imdmk.playtime.platform.events; - -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.Server; -import org.bukkit.event.Event; -import org.jetbrains.annotations.NotNull; - -/** - * Utility wrapper for safely firing Bukkit {@link Event}s. - * Ensures that synchronous events are always fired on the primary server thread. - */ -public final class BukkitEventCaller { - - private final Server server; - private final TaskScheduler scheduler; - - public BukkitEventCaller(@NotNull Server server, @NotNull TaskScheduler scheduler) { - this.server = Validator.notNull(server, "server cannot be null"); - this.scheduler = Validator.notNull(scheduler, "scheduler cannot be null"); - } - - /** - * Calls the specified Bukkit event ensuring correct thread usage: - *

    - *
  • Asynchronous events are fired on the current thread;
  • - *
  • Synchronous events are fired on the primary server thread.
  • - *
- */ - public T callEvent(@NotNull T event) { - Validator.notNull(event, "event cannot be null"); - - if (event.isAsynchronous()) { - server.getPluginManager().callEvent(event); - return event; - } - - if (server.isPrimaryThread()) { - server.getPluginManager().callEvent(event); - } else { - scheduler.runSync(() -> server.getPluginManager().callEvent(event)); - } - - return event; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitListenerRegistrar.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitListenerRegistrar.java deleted file mode 100644 index 10f1f0b..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitListenerRegistrar.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.imdmk.playtime.platform.events; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.event.Listener; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; - -/** - * Utility component responsible for registering Bukkit {@link Listener}s. - *

- * This registrar provides two registration modes: - *

    - *
  • Direct registration of pre-instantiated listener objects.
  • - *
  • Automatic instantiation and field injection through {@link Injector}.
  • - *
- *

- * All listeners are registered using the plugin's {@link org.bukkit.plugin.PluginManager}. - */ -public final class BukkitListenerRegistrar { - - private final Plugin plugin; - - /** - * Creates a new registrar for the given Bukkit plugin. - * - * @param plugin the plugin instance used for listener registration - * @throws NullPointerException if the plugin is null - */ - public BukkitListenerRegistrar(@NotNull Plugin plugin) { - this.plugin = Validator.notNull(plugin, "plugin cannot be null"); - } - - /** - * Registers the provided listener instances with the Bukkit {@link org.bukkit.plugin.PluginManager}. - * - * @param listeners the listener instances to register - * @throws NullPointerException if the listeners array or any listener is null - */ - public void register(@NotNull Listener... listeners) { - Validator.notNull(listeners, "listeners cannot be null"); - for (final Listener listener : listeners) { - plugin.getServer().getPluginManager().registerEvents(listener, plugin); - } - } - - /** - * Creates and registers listeners using the provided {@link Injector}. - *

- * Each listener class is instantiated and its dependencies are injected automatically. - * - * @param injector the dependency injector to use for listener instantiation - * @param listeners the listener classes to create and register - * @throws NullPointerException if the injector, the listener array, or any class is null - */ - @SafeVarargs - public final void register(@NotNull Injector injector, @NotNull Class... listeners) { - Validator.notNull(injector, "injector cannot be null"); - Validator.notNull(listeners, "listeners cannot be null"); - - for (final Class listenerClass : listeners) { - register(injector.newInstance(listenerClass)); - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiModule.java deleted file mode 100644 index c2ebe85..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiModule.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime.platform.gui; - -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.platform.gui.render.GuiRenderer; -import com.github.imdmk.playtime.platform.gui.render.TriumphGuiRenderer; -import com.github.imdmk.playtime.platform.gui.view.GuiOpener; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class GuiModule implements Module { - - private GuiOpener guiOpener; - private GuiRenderer guiRenderer; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(GuiOpener.class).assignInstance(() -> this.guiOpener); - resources.on(GuiRenderer.class).assignInstance(() -> this.guiRenderer); - } - - @Override - public void init(@NotNull Injector injector) { - this.guiOpener = injector.newInstance(GuiOpener.class); - this.guiRenderer = injector.newInstance(TriumphGuiRenderer.class); - } - - @Override - public int order() { - return 10; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java index c996139..e920d4a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.gui; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -10,26 +9,13 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -/** - * Thread-safe registry of {@link IdentifiableGui}, keyed by normalized id. - * Invariant: at most one GUI per concrete class (maintained class index). - */ public final class GuiRegistry { private final Map byId = new ConcurrentHashMap<>(); private final Map, IdentifiableGui> byClass = new ConcurrentHashMap<>(); - /** - * Registers (or replaces) GUI by its normalized identifier. - * Also updates class index (one instance per class). - * - * @return previously registered GUI under the same id, or {@code null}. - */ - @Nullable - public IdentifiableGui register(@NotNull IdentifiableGui gui) { - Validator.notNull(gui, "gui cannot be null"); - final String id = normalize(Validator.notNull(gui.getId(), "gui identifier cannot be null")); - + public void register(@NotNull IdentifiableGui gui) { + final String id = normalize(gui.getId()); final IdentifiableGui previous = byId.put(id, gui); // maintain class index (assume single instance per class) @@ -40,18 +26,10 @@ public IdentifiableGui register(@NotNull IdentifiableGui gui) { if (previous != null && previous.getClass() != type) { byClass.compute(previous.getClass(), (k, current) -> current == previous ? null : current); } - return previous; } - /** - * Registers GUI only if absent under the same id. - * - * @return {@code true} if registered, {@code false} if id existed. - */ public boolean registerIfAbsent(@NotNull IdentifiableGui gui) { - Validator.notNull(gui, "gui cannot be null"); - final String id = normalize(Validator.notNull(gui.getId(), "gui identifier cannot be null")); - + final String id = normalize(gui.getId()); final IdentifiableGui existing = byId.putIfAbsent(id, gui); if (existing == null) { // we won the race; update class index @@ -61,12 +39,9 @@ public boolean registerIfAbsent(@NotNull IdentifiableGui gui) { return false; } - /** - * Unregisters GUI by id. Updates class index if pointing to same instance. - */ @Nullable public IdentifiableGui unregister(@NotNull String id) { - final String key = normalize(Validator.notNull(id, "id cannot be null")); + final String key = normalize(id); final IdentifiableGui removed = byId.remove(key); if (removed != null) { byClass.compute(removed.getClass(), (k, current) -> current == removed ? null : current); @@ -74,40 +49,30 @@ public IdentifiableGui unregister(@NotNull String id) { return removed; } - /** - * Case-insensitive lookup by id (whitespace-insensitive). - */ @Nullable public IdentifiableGui getById(@NotNull String id) { - final String key = normalize(Validator.notNull(id, "id cannot be null")); + final String key = normalize(id); return byId.get(key); } - /** - * O(1) exact type lookup. Assumes at most one instance per class. - */ @Nullable + @SuppressWarnings("unchecked") public T getByClass(@NotNull Class type) { - Validator.notNull(type, "type cannot be null"); final IdentifiableGui gui = byClass.get(type); - @SuppressWarnings("unchecked") - final T cast = (T) gui; - return cast; + return (T) gui; } public boolean isRegistered(@NotNull String id) { - final String key = normalize(Validator.notNull(id, "id cannot be null")); + final String key = normalize(id); return byId.containsKey(key); } - /** Immutable snapshot of normalized ids. */ @Unmodifiable public Set ids() { return Set.copyOf(byId.keySet()); } - /** Current strategy: trim + lowercased (Locale.ROOT). */ - private static String normalize(@NotNull String id) { + private static String normalize(String id) { final String trimmed = id.trim(); return trimmed.toLowerCase(Locale.ROOT); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java index a17c8e5..1dd71ac 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java @@ -1,33 +1,8 @@ package com.github.imdmk.playtime.platform.gui; -/** - * Defines the supported GUI layout types within the plugin. - * - *

Each type represents a different interaction model for displaying items.

- */ public enum GuiType { - - /** - * A fixed-size GUI without pagination or scrolling. - * Suitable for simple static interfaces. - */ STANDARD, - - /** - * A multipage GUI used for displaying large sets of items. - * Provides navigation between pages. - */ PAGINATED, - - /** - * A GUI that supports vertical scrolling. - * Ideal for lists of items exceeding the visible height. - */ SCROLLING_VERTICAL, - - /** - * A GUI that supports horizontal scrolling. - * Useful for side-by-side item navigation. - */ SCROLLING_HORIZONTAL } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java index f9f0a9d..cacc917 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java @@ -1,18 +1,9 @@ package com.github.imdmk.playtime.platform.gui; import org.jetbrains.annotations.NotNull; -/** - * Represents a GUI component that can be uniquely identified by a string identifier. - *

- * Useful for registering and retrieving GUI instances by their identifier. - */ @FunctionalInterface public interface IdentifiableGui { - /** - * Returns the unique identifier for this GUI. - * - * @return the non-null unique identifier string - */ @NotNull String getId(); + } \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java index a15678f..44a0e4e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java @@ -2,27 +2,13 @@ import com.github.imdmk.playtime.platform.gui.GuiType; import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNull; -/** - * Represents a configurable GUI loaded from configuration. - * Implementations should provide all basic GUI metadata and content definitions. - */ public interface ConfigurableGui { - /** - * @return GUI title as Adventure {@link Component} - */ - @NotNull Component title(); + Component title(); - /** - * @return GUI type (e.g. {@link GuiType#STANDARD}, {@link GuiType#PAGINATED}) - */ - @NotNull GuiType type(); + GuiType type(); - /** - * @return GUI rows - */ int rows(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java index 4b08026..f1069b1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java @@ -2,7 +2,8 @@ import com.github.imdmk.playtime.config.ConfigSection; import com.github.imdmk.playtime.feature.playtime.gui.PlayTimeTopGuiConfig; -import com.github.imdmk.playtime.platform.adventure.ComponentSerializer; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; +import com.github.imdmk.playtime.platform.adventure.AdventureComponentSerializer; import com.github.imdmk.playtime.platform.gui.item.ItemGuiSerializer; import com.github.imdmk.playtime.platform.serdes.EnchantmentSerializer; import com.github.imdmk.playtime.platform.serdes.SoundSerializer; @@ -10,6 +11,7 @@ import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; +@ConfigFile public final class GuiConfig extends ConfigSection { @Comment({"#", "# Playtime top GUI", "#"}) @@ -19,9 +21,9 @@ public final class GuiConfig extends ConfigSection { public NavigationBarConfig navigationBar = new NavigationBarConfig(); @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> { - registry.register(new ComponentSerializer()); + registry.register(new AdventureComponentSerializer()); registry.register(new ItemGuiSerializer()); registry.register(new EnchantmentSerializer()); registry.register(new SoundSerializer()); @@ -29,7 +31,7 @@ public final class GuiConfig extends ConfigSection { } @Override - public @NotNull String getFileName() { + public @NotNull String fileName() { return "guiConfig.yml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java index 8e828c1..c3755a5 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java @@ -6,12 +6,6 @@ import eu.okaeri.configs.annotation.Comment; import org.bukkit.Material; -/** - * Configuration for navigation items used in paginated GUIs. - *

- * Defines visual representation and behavior for navigation controls: - * next, previous, and exit buttons displayed in inventory-based interfaces. - */ public final class NavigationBarConfig extends OkaeriConfig { @Comment({ @@ -100,4 +94,5 @@ public final class NavigationBarConfig extends OkaeriConfig { " " )) .build(); + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java index e5cfff8..d901870 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java @@ -1,39 +1,20 @@ package com.github.imdmk.playtime.platform.gui.factory; import com.github.imdmk.playtime.platform.gui.GuiType; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.builder.gui.BaseGuiBuilder; import dev.triumphteam.gui.components.ScrollType; import dev.triumphteam.gui.guis.Gui; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; -/** - * Factory for creating TriumphGUI {@link BaseGuiBuilder} instances - * based on a provided {@link GuiType}. - *

- * Supports standard, paginated, and scrolling (vertical/horizontal) GUIs. - */ public final class GuiBuilderFactory { private GuiBuilderFactory() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Returns a TriumphGUI builder matching the given {@link GuiType}. - * - * @param type the GUI type - * @param rows the GUI rows - * @return a new {@link BaseGuiBuilder} instance for the given type - * @throws IllegalArgumentException if {@code type} is {@code null} - */ - @Contract(pure = true) - public static @NotNull BaseGuiBuilder forType(@NotNull GuiType type, int rows) { - Validator.notNull(type, "type cannot be null"); - + public static BaseGuiBuilder forType(@NotNull GuiType type, int rows) { return switch (type) { case STANDARD -> Gui.gui().rows(rows); case PAGINATED -> Gui.paginated().rows(rows); @@ -42,20 +23,8 @@ private GuiBuilderFactory() { }; } - /** - * Creates and immediately customizes a TriumphGUI builder. - * - * @param type the GUI type - * @param rows the GUI rows - * @param editConsumer consumer for post-creation customization (e.g., size, disableAllInteractions) - * @return a modified {@link BaseGuiBuilder} instance - * @throws IllegalArgumentException if {@code type} or {@code editConsumer} is {@code null} - */ - public static @NotNull BaseGuiBuilder forType(@NotNull GuiType type, int rows, @NotNull Consumer> editConsumer) { - Validator.notNull(type, "type cannot be null"); - Validator.notNull(editConsumer, "editConsumer cannot be null"); - - BaseGuiBuilder builder = forType(type, rows); + public static BaseGuiBuilder forType(@NotNull GuiType type, int rows, @NotNull Consumer> editConsumer) { + final BaseGuiBuilder builder = forType(type, rows); editConsumer.accept(builder); return builder; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java index cee734d..190904a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java @@ -1,54 +1,25 @@ package com.github.imdmk.playtime.platform.gui.factory; import com.github.imdmk.playtime.platform.gui.config.ConfigurableGui; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; -/** - * Factory for creating {@link BaseGui} instances from {@link ConfigurableGui}. - *

- * Responsibilities: - *

    - *
  • Delegate to {@link GuiBuilderFactory} based on configured type,
  • - *
  • Apply base attributes (e.g. title),
  • - *
  • Optionally allow post-creation customization via a {@link Consumer}.
  • - *
- */ public final class GuiFactory { private GuiFactory() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Builds a GUI instance from the provided configuration. - * - * @param config the GUI configuration - * @return a new {@link BaseGui} instance - * @throws IllegalArgumentException if the GUI type is unsupported - */ - public static @NotNull BaseGui build(@NotNull ConfigurableGui config) { - Validator.notNull(config, "config cannot be null"); + public static BaseGui build(@NotNull ConfigurableGui config) { return GuiBuilderFactory.forType(config.type(), config.rows()) .title(config.title()) .create(); } - /** - * Builds and immediately customizes a GUI using the provided consumer. - * - * @param config the GUI configuration - * @param editConsumer consumer to modify the GUI instance before returning - * @return the configured {@link BaseGui} - */ - public static @NotNull BaseGui build(@NotNull ConfigurableGui config, @NotNull Consumer editConsumer) { - Validator.notNull(config, "config cannot be null"); - Validator.notNull(editConsumer, "editConsumer cannot be null"); - - BaseGui gui = build(config); + public static BaseGui build(@NotNull ConfigurableGui config, @NotNull Consumer editConsumer) { + final BaseGui gui = build(config); editConsumer.accept(gui); return gui; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java index f9f967b..c0db2da 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.gui.item; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; @@ -16,12 +15,6 @@ import java.util.List; import java.util.Map; -/** - * Immutable data model representing a GUI item definition. - *

- * Pure data – no logic, no rendering. - * Used to describe items in configuration and GUI assembly layers. - */ public record ItemGui( @NotNull Material material, @NotNull Component name, @@ -34,10 +27,6 @@ public record ItemGui( ) { public ItemGui { - Validator.notNull(material, "material cannot be null"); - Validator.notNull(name, "name cannot be null"); - Validator.notNull(lore, "lore cannot be null"); - lore = List.copyOf(lore); if (enchantments != null) { @@ -67,21 +56,20 @@ public static final class Builder { @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder material(@NotNull Material material) { - this.material = Validator.notNull(material, "material cannot be null"); + this.material = material; return this; } @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder name(@NotNull Component name) { - this.name = Validator.notNull(name, "name cannot be null"); + this.name = name; return this; } @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder lore(@NotNull List lore) { - Validator.notNull(lore, "lore cannot be null"); this.lore = List.copyOf(lore); return this; } @@ -117,9 +105,6 @@ public Builder requiredPermission(@Nullable String permission) { @CheckReturnValue @Contract(value = "_, _ -> this", mutates = "this") public Builder addEnchantment(@NotNull Enchantment enchantment, @NotNull Integer level) { - Validator.notNull(enchantment, "enchantment cannot be null"); - Validator.notNull(level, "level cannot be null"); - Map newEnchantments = new HashMap<>(this.enchantments); newEnchantments.put(enchantment, level); this.enchantments = Map.copyOf(newEnchantments); @@ -129,8 +114,6 @@ public Builder addEnchantment(@NotNull Enchantment enchantment, @NotNull Integer @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder addFlags(@NotNull ItemFlag... toAdd) { - Validator.notNull(toAdd, "flags cannot be null"); - List newFlags = new ArrayList<>(this.flags); Collections.addAll(newFlags, toAdd); this.flags = List.copyOf(newFlags); @@ -140,15 +123,12 @@ public Builder addFlags(@NotNull ItemFlag... toAdd) { @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder appendLore(@NotNull Component... lines) { - Validator.notNull(lines, "lines cannot be null"); - List newLore = new ArrayList<>(this.lore); Collections.addAll(newLore, lines); this.lore = List.copyOf(newLore); return this; } - @NotNull public ItemGui build() { return new ItemGui( this.material, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java index 5eacf92..d48aac2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java @@ -24,22 +24,22 @@ public void serialize(@NotNull ItemGui item, @NotNull SerializationData data, @N data.add("name", item.name(), Component.class); data.addCollection("lore", item.lore(), Component.class); - var slot = item.slot(); + final var slot = item.slot(); if (slot != null) { data.add("slot", slot, Integer.class); } - var enchantments = item.enchantments(); + final var enchantments = item.enchantments(); if (enchantments != null && !enchantments.isEmpty()) { data.addAsMap("enchantments", item.enchantments(), Enchantment.class, Integer.class); } - var flags = item.flags(); + final var flags = item.flags(); if (flags != null && !flags.isEmpty()) { data.addCollection("flags", flags, ItemFlag.class); } - var permission = item.requiredPermission(); + final var permission = item.requiredPermission(); if (permission != null && !permission.isBlank()) { data.add("permission", permission, String.class); } @@ -47,14 +47,14 @@ public void serialize(@NotNull ItemGui item, @NotNull SerializationData data, @N @Override public ItemGui deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { - var material = data.get("material", Material.class); - var name = data.get("name", Component.class); - var lore = data.getAsList("lore", Component.class); + final var material = data.get("material", Material.class); + final var name = data.get("name", Component.class); + final var lore = data.getAsList("lore", Component.class); - var slot = data.get("slot", Integer.class); - var enchantments = data.getAsMap("enchantments", Enchantment.class, Integer.class); - var flags = data.getAsList("flags", ItemFlag.class); - var permission = data.get("permission", String.class); + final var slot = data.get("slot", Integer.class); + final var enchantments = data.getAsMap("enchantments", Enchantment.class, Integer.class); + final var flags = data.getAsList("flags", ItemFlag.class); + final var permission = data.get("permission", String.class); return new ItemGui( material, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java index 50b9954..35b309d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.gui.item; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.builder.item.ItemBuilder; import dev.triumphteam.gui.components.GuiAction; @@ -12,74 +11,31 @@ import java.util.function.Consumer; -/** - * Stateless utility that converts {@link ItemGui} definitions into Triumph {@link GuiItem}s. - * - *

Thread-safety: Pure transformation; prefer main thread for Bukkit objects.

- */ public final class ItemGuiTransformer { private ItemGuiTransformer() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - /** - * Creates a {@link GuiItem} with a no-op click handler. - * - * @param item item definition (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if {@code item} is {@code null} - */ - public static @NotNull GuiItem toGuiItem(@NotNull ItemGui item) { + public static GuiItem toGuiItem(@NotNull ItemGui item) { return toGuiItem(item, (e) -> {}, (b) -> {}); } - /** - * Creates a {@link GuiItem} wiring a {@link GuiAction} click handler. - * - * @param item item (non-null) - * @param onClick click handler (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if any argument is {@code null} - */ - public static @NotNull GuiItem toGuiItem(@NotNull ItemGui item, @NotNull GuiAction onClick) { + public static GuiItem toGuiItem(@NotNull ItemGui item, @NotNull GuiAction onClick) { return toGuiItem(item, onClick, (b) -> {}); } - /** - * Creates a {@link GuiItem} wiring a standard {@link Consumer} click handler. - * Convenience overload that adapts to Triumph's {@link GuiAction}. - * - * @param item item (non-null) - * @param onClick click handler (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if any argument is {@code null} - */ - public static @NotNull GuiItem toGuiItem(@NotNull ItemGui item, @NotNull Consumer onClick) { + public static GuiItem toGuiItem(@NotNull ItemGui item, @NotNull Consumer onClick) { return toGuiItem(item, onClick::accept, (b) -> {}); } - /** - * Creates a {@link GuiItem} with handler and optional builder editor. - * - * @param item item (non-null) - * @param onClick click handler (non-null) - * @param builderEditor item builder editor (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if any argument is {@code null} - */ - public static @NotNull GuiItem toGuiItem( + public static GuiItem toGuiItem( @NotNull ItemGui item, @NotNull GuiAction onClick, @NotNull Consumer> builderEditor ) { - Validator.notNull(item, "item cannot be null"); - Validator.notNull(onClick, "onClick cannot be null"); - Validator.notNull(builderEditor, "builderEditor cannot be null"); - - final Material material = item.material(); - final BaseItemBuilder builder = - material == Material.PLAYER_HEAD ? ItemBuilder.skull() : ItemBuilder.from(material); + final var material = item.material(); + final var builder = material == Material.PLAYER_HEAD ? ItemBuilder.skull() : ItemBuilder.from(material); builder.name(item.name()); builder.lore(item.lore()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java index ce50b59..a6c8051 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java @@ -1,44 +1,11 @@ package com.github.imdmk.playtime.platform.gui.item; import com.github.imdmk.playtime.platform.gui.render.RenderContext; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.entity.HumanEntity; import org.jetbrains.annotations.NotNull; -/** - * Resolves which {@link ItemGui} variant should be displayed to a viewer - * based on their permission state. - * - *

This implementation iterates through candidate items in order and returns - * the first one that either: - *

    - *
  • Has no required permission ({@code requiredPermission() == null}), or
  • - *
  • Has a permission that the viewer possesses, as determined by - * {@link RenderContext#permissionEvaluator()}.
  • - *
- * If no candidate matches, a predefined fallback item is returned.

- * - *

Usage: Typically used by GUI renderers to determine which - * item variant to display for users with different roles or permission levels.

- * - *

Thread-safety: This resolver is stateless and thread-safe.

- * - * @see ItemGui - * @see ItemVariantResolver - * @see RenderContext - */ public final class ItemVariantPermissionResolver implements ItemVariantResolver { - /** - * Resolves the first matching {@link ItemGui} variant visible to the given viewer. - * - * @param viewer the player or entity viewing the GUI (non-null) - * @param context current rendering context, providing permission evaluation (non-null) - * @param candidates ordered list of possible item variants (non-null) - * @param fallback default item to return if no candidate matches (non-null) - * @return the first permitted item variant, or {@code fallback} if none are allowed - * @throws NullPointerException if any argument is null - */ @Override public ItemGui resolve( @NotNull HumanEntity viewer, @@ -46,11 +13,6 @@ public ItemGui resolve( @NotNull Iterable candidates, @NotNull ItemGui fallback ) { - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(context, "context cannot be null"); - Validator.notNull(candidates, "candidates cannot be null"); - Validator.notNull(fallback, "fallback cannot be null"); - for (final ItemGui item : candidates) { if (item == null) { continue; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java index 163011d..328a13c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java @@ -4,35 +4,8 @@ import org.bukkit.entity.HumanEntity; import org.jetbrains.annotations.NotNull; -/** - * Defines a strategy for selecting which {@link ItemGui} variant - * should be displayed to a specific viewer during GUI rendering. - * - *

Implementations of this interface encapsulate different - * resolution logics — e.g., by permission, by user state, - * by contextual conditions, or by custom business rules.

- * - *

The resolver is typically used within GUI frameworks to decide - * which visual representation of an item (variant) to render for a given player.

- * - *

Thread-safety: Implementations should be stateless and thread-safe.

- * - * @see ItemGui - * @see RenderContext - * @see ItemVariantPermissionResolver - */ public interface ItemVariantResolver { - /** - * Resolves the most appropriate {@link ItemGui} variant to display. - * - * @param viewer the player or entity viewing the GUI (non-null) - * @param context the current rendering context providing permission checks, locale, etc. (non-null) - * @param candidates iterable collection of possible item variants, evaluated in order (non-null) - * @param fallback default item variant to use if none match (non-null) - * @return the resolved item variant, never {@code null} (at least {@code fallback}) - * @throws NullPointerException if any parameter is null - */ ItemGui resolve(@NotNull HumanEntity viewer, @NotNull RenderContext context, @NotNull Iterable candidates, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java index 2f6fcb8..6015bfa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java @@ -9,10 +9,6 @@ import java.util.function.Consumer; -/** - * Renders and places {@link ItemGui} into {@link BaseGui} instances. - * Invoke only on the Bukkit main thread. - */ public interface GuiRenderer { @Contract(mutates = "param1") @@ -26,10 +22,6 @@ default void setItem(@NotNull BaseGui gui, setItem(gui, slot, item, context, options, onClick, b -> {}); } - /** - * Sets the item in a specific slot (overwrites existing content). - * Supports per-slot customization via {@code builderEditor}. - */ @Contract(mutates = "param1") void setItem(@NotNull BaseGui gui, int slot, @@ -81,10 +73,6 @@ default void addItem(@NotNull BaseGui gui, addItem(gui, item, context, options, onClick, b -> {}); } - /** - * Adds the item to the next free slot. - * Supports per-slot customization via {@code builderEditor}. - */ @Contract(mutates = "param1") void addItem(@NotNull BaseGui gui, @NotNull ItemGui item, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java index ee7cb96..4ce4519 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java @@ -1,19 +1,6 @@ package com.github.imdmk.playtime.platform.gui.render; -/** - * Defines how a GUI element should behave when the viewer lacks - * the required permission to interact with or view the item. - */ public enum NoPermissionPolicy { - - /** - * The item is completely hidden and not placed in the GUI. - */ HIDE, - - /** - * The item is still visible but interaction is disabled. - * Clicking it will trigger the "onDenied" consumer if provided. - */ DISABLE } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java index 2c27ab3..f7e5a3c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java @@ -3,23 +3,8 @@ import org.bukkit.entity.HumanEntity; import org.jetbrains.annotations.NotNull; -/** - * Strategy interface for checking player permissions. - *

- * This abstraction allows GUIs and renderers to remain independent - * from the underlying permission system (e.g. Bukkit, Vault, LuckPerms). - *

- * Implementations should be thread-safe if evaluated asynchronously. - */ @FunctionalInterface public interface PermissionEvaluator { - /** - * Checks whether the given human entity possesses the specified permission. - * - * @param entity the entity being checked (non-null) - * @param permission the permission node (non-null) - * @return {@code true} if the player has the permission; {@code false} otherwise - */ boolean has(@NotNull HumanEntity entity, @NotNull String permission); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java index 1369aa4..f34a6ef 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java @@ -4,29 +4,12 @@ import org.bukkit.permissions.Permissible; import org.jetbrains.annotations.NotNull; -/** - * Immutable context used during GUI item rendering. - *

- * Encapsulates the viewer and the permission evaluation strategy, - * ensuring renderers remain stateless and easily testable. - *

- * Thread-safety: This record is immutable and thread-safe - * as long as the underlying {@link PermissionEvaluator} implementation is thread-safe. - * - * @param viewer the player for whom the GUI is being rendered - * @param permissionEvaluator the strategy used to check permissions - */ public record RenderContext( @NotNull Player viewer, @NotNull PermissionEvaluator permissionEvaluator ) { - /** - * Creates a default context that checks if player has permission. - * - * @return the default {@link RenderContext} instance - */ - public static @NotNull RenderContext defaultContext(@NotNull Player viewer) { + public static RenderContext defaultContext(@NotNull Player viewer) { return new RenderContext(viewer, Permissible::hasPermission); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java index 03a8d30..ccfaffa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java @@ -5,36 +5,16 @@ import java.util.function.Consumer; -/** - * Rendering options that define how permission handling - * and denied interactions are processed during GUI rendering. - * - * @param policy how to handle items when the viewer lacks permission - * @param onDenied consumer called when a denied item is clicked - *

- * Thread-safety: This record is immutable and thread-safe, - * provided that the supplied {@link Consumer} implementation is thread-safe. - */ public record RenderOptions( @NotNull NoPermissionPolicy policy, @NotNull Consumer onDenied ) { - /** - * Creates a default option that disables unauthorized items silently. - * - * @return the default {@link RenderOptions} instance - */ - public static @NotNull RenderOptions defaultDenySilently() { + public static RenderOptions defaultDenySilently() { return new RenderOptions(NoPermissionPolicy.DISABLE, e -> {}); } - /** - * Creates a default option that hides unauthorized items completely. - * - * @return the default {@link RenderOptions} instance - */ - public static @NotNull RenderOptions defaultHide() { + public static RenderOptions defaultHide() { return new RenderOptions(NoPermissionPolicy.HIDE, e -> {}); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java index 5fbbc70..3ce9477 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.platform.gui.item.ItemGui; import com.github.imdmk.playtime.platform.gui.item.ItemGuiTransformer; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.components.GuiAction; import dev.triumphteam.gui.guis.BaseGui; @@ -12,117 +11,64 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; -/** - * Default {@link GuiRenderer} implementation using the Triumph GUI API. - *

- * Responsible for rendering {@link ItemGui} objects into a {@link BaseGui}, - * applying permission policies and wiring click handlers. - * - *

Behavior: - *

    - *
  • If {@link NoPermissionPolicy#HIDE} → item is not rendered (returns {@code null}).
  • - *
  • If {@link NoPermissionPolicy#DISABLE} → click is blocked (cancelled) silently.
  • - *
  • Otherwise → executes provided click handler.
  • - *
- */ public final class TriumphGuiRenderer implements GuiRenderer { - /** - * Creates a new renderer instance. - *

Renderer is stateless and may be safely reused.

- * - * @return new {@link TriumphGuiRenderer} instance - */ public static TriumphGuiRenderer newRenderer() { return new TriumphGuiRenderer(); } - /** - * Places a rendered {@link ItemGui} into the specified GUI slot. - *

If the item should be hidden (policy {@code HIDE}), it will not be placed.

- * - * @param gui target GUI - * @param slot target slot index - * @param item GUI item definition - * @param context render context - * @param options render options - * @param onClick click action to execute if allowed - * @param builderEditor optional builder customization - */ @Override @Contract(mutates = "param1") public void setItem( - @NotNull final BaseGui gui, - final int slot, - @NotNull final ItemGui item, - @NotNull final RenderContext context, - @NotNull final RenderOptions options, - @NotNull final Consumer onClick, - @NotNull final Consumer> builderEditor + @NotNull BaseGui gui, + int slot, + @NotNull ItemGui item, + @NotNull RenderContext context, + @NotNull RenderOptions options, + @NotNull Consumer onClick, + @NotNull Consumer> builderEditor ) { - validateArgs(gui, item, context, options, onClick, builderEditor); - - final GuiItem guiItem = buildGuiItem(item, context, options, onClick, builderEditor); - if (guiItem != null) { - gui.setItem(slot, guiItem); + final GuiItem builtItem = buildGuiItem(item, context, options, onClick, builderEditor); + if (builtItem != null) { + gui.setItem(slot, builtItem); } } - /** - * Adds a rendered {@link ItemGui} to the GUI at the next available position. - *

If the item should be hidden (policy {@code HIDE}), it will not be added.

- * - * @param gui target GUI - * @param item GUI item definition - * @param context render context - * @param options render options - * @param onClick click action to execute if allowed - * @param builderEditor optional builder customization - */ @Override @Contract(mutates = "param1") public void addItem( - @NotNull final BaseGui gui, - @NotNull final ItemGui item, - @NotNull final RenderContext context, - @NotNull final RenderOptions options, - @NotNull final Consumer onClick, - @NotNull final Consumer> builderEditor + @NotNull BaseGui gui, + @NotNull ItemGui item, + @NotNull RenderContext context, + @NotNull RenderOptions options, + @NotNull Consumer onClick, + @NotNull Consumer> builderEditor ) { - validateArgs(gui, item, context, options, onClick, builderEditor); - - final GuiItem guiItem = buildGuiItem(item, context, options, onClick, builderEditor); - if (guiItem != null) { - gui.addItem(guiItem); + final GuiItem builtItem = buildGuiItem(item, context, options, onClick, builderEditor); + if (builtItem != null) { + gui.addItem(builtItem); } } - /** - * Builds a {@link GuiItem} based on the given item definition and context. - *

- * Permission logic: - *

    - *
  • If the viewer lacks permission and policy is {@code HIDE}, returns {@code null}.
  • - *
  • If the viewer lacks permission and policy is {@code DISABLE}, click is blocked silently.
  • - *
- * - * @return a built {@link GuiItem}, or {@code null} if hidden - */ - private @Nullable GuiItem buildGuiItem( - @NotNull final ItemGui item, - @NotNull final RenderContext context, - @NotNull final RenderOptions options, - @NotNull final Consumer onClick, - @NotNull final Consumer> builderEditor + private static GuiItem buildGuiItem( + ItemGui item, + RenderContext context, + RenderOptions options, + Consumer onClick, + Consumer> builderEditor ) { final String requiredPerm = item.requiredPermission(); + final boolean allowedForViewerNow = hasPermission(requiredPerm, context, context.viewer()); + if (!allowedForViewerNow && options.policy() == NoPermissionPolicy.HIDE) { + return null; + } + final GuiAction clickHandler = event -> { - if (!has(requiredPerm, context, event.getWhoClicked())) { + if (!hasPermission(requiredPerm, context, event.getWhoClicked())) { event.setCancelled(true); event.setResult(Event.Result.DENY); options.onDenied().accept(event); @@ -132,46 +78,10 @@ public void addItem( onClick.accept(event); }; - final boolean allowedForViewerNow = has(requiredPerm, context, context.viewer()); - if (!allowedForViewerNow && options.policy() == NoPermissionPolicy.HIDE) { - return null; - } - return ItemGuiTransformer.toGuiItem(item, clickHandler, builderEditor); } - /** - * Checks if the given entity has the required permission. - * - * @param permission permission string or {@code null} - * @param context render context - * @param entity entity to check - * @return {@code true} if allowed, otherwise {@code false} - */ - private static boolean has(@Nullable final String permission, - @NotNull final RenderContext context, - @NotNull final HumanEntity entity) { + private static boolean hasPermission(String permission, RenderContext context, HumanEntity entity) { return permission == null || context.permissionEvaluator().has(entity, permission); } - - /** - * Ensures all arguments are non-null. - * - * @throws NullPointerException if any argument is {@code null} - */ - private static void validateArgs( - final BaseGui gui, - final ItemGui item, - final RenderContext context, - final RenderOptions options, - final Consumer onClick, - final Consumer> builderEditor - ) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(item, "item cannot be null"); - Validator.notNull(context, "context cannot be null"); - Validator.notNull(options, "options cannot be null"); - Validator.notNull(onClick, "onClick cannot be null"); - Validator.notNull(builderEditor, "builderEditor cannot be null"); - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java index 17d6264..2a6599b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java @@ -1,12 +1,9 @@ package com.github.imdmk.playtime.platform.gui.view; -import com.github.imdmk.playtime.platform.gui.config.GuiConfig; import com.github.imdmk.playtime.platform.gui.config.NavigationBarConfig; import com.github.imdmk.playtime.platform.gui.render.GuiRenderer; -import com.github.imdmk.playtime.platform.gui.render.RenderContext; import com.github.imdmk.playtime.platform.gui.render.RenderOptions; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; @@ -14,18 +11,6 @@ import java.util.function.Consumer; -/** - * Thin base for GUI implementations. - *

- * Responsibilities: - *

    - *
  • Provide navigation helpers (Next/Previous/Exit),
  • - *
  • Hold shared collaborators: {@link GuiConfig}, {@link TaskScheduler}, - * {@link GuiRenderer}, {@link RenderContext}, {@link RenderOptions}.
  • - *
- * - * Threading: All methods are expected to be called on the Bukkit main thread. - */ public abstract class AbstractGui { protected final NavigationBarConfig config; @@ -35,22 +20,16 @@ public abstract class AbstractGui { private final NavigationBar navigationBar; - /** - * @param config GUI config (visual defaults, nav items, etc.) - * @param taskScheduler scheduler for short, sync GUI updates - * @param renderer renderer that places items and enforces permission policy - * @param renderOptions render options (no-permission policy, onDenied) - */ protected AbstractGui( @NotNull NavigationBarConfig config, @NotNull TaskScheduler taskScheduler, @NotNull GuiRenderer renderer, @NotNull RenderOptions renderOptions ) { - this.config = Validator.notNull(config, "config cannot be null"); - this.scheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); - this.renderer = Validator.notNull(renderer, "renderer cannot be null"); - this.renderOptions = Validator.notNull(renderOptions, "renderOptions cannot be null"); + this.config = config; + this.scheduler = taskScheduler; + this.renderer = renderer; + this.renderOptions = renderOptions; this.navigationBar = new NavigationBar( this.config, @@ -60,44 +39,15 @@ protected AbstractGui( ); } - /** - * Places the "Next" control if the GUI is paginated. - * - * @param gui target GUI - * @param viewer target viewer - */ protected void placeNext(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); navigationBar.setNext(gui, viewer); } - /** - * Places the "Previous" control if the GUI is paginated. - * - * @param gui target GUI - * @param viewer target viewer - */ protected void placePrevious(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); navigationBar.setPrevious(gui, viewer); } - /** - * Places the "Exit" control. - * - * @param gui target GUI - * @param viewer target viewer - * @param exit action to run on click - */ - protected void placeExit( - @NotNull BaseGui gui, - @NotNull Player viewer, - @NotNull Consumer exit) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(exit, "exit cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); + protected void placeExit(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull Consumer exit) { navigationBar.setExit(gui, viewer, exit); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java index 5558fb9..5b604fb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java @@ -1,12 +1,5 @@ package com.github.imdmk.playtime.platform.gui.view; -/** - * Provides predefined slot positions for common GUI navigation controls - * (Next, Previous, Exit) depending on the GUI height (3–6 rows). - *

- * Each constant represents the index of an inventory slot where - * navigation buttons should be placed. - */ final class GridSlots { private static final int ROW_3_NEXT = 25; @@ -29,12 +22,6 @@ private GridSlots() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Returns the inventory slot index for the "Next Page" button. - * - * @param rows number of GUI rows (3–6) - * @return slot index for the next-page control - */ static int next(int rows) { return switch (rows) { case 3 -> ROW_3_NEXT; @@ -45,12 +32,6 @@ static int next(int rows) { }; } - /** - * Returns the inventory slot index for the "Previous Page" button. - * - * @param rows number of GUI rows (3–6) - * @return slot index for the previous-page control - */ static int previous(int rows) { return switch (rows) { case 3 -> ROW_3_PREVIOUS; @@ -61,12 +42,6 @@ static int previous(int rows) { }; } - /** - * Returns the inventory slot index for the "Exit" button. - * - * @param rows number of GUI rows (3–6) - * @return slot index for the exit control - */ static int exit(int rows) { return switch (rows) { case 3 -> ROW_3_EXIT; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java index 4670266..319f05c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java @@ -3,23 +3,11 @@ import com.github.imdmk.playtime.platform.gui.GuiRegistry; import com.github.imdmk.playtime.platform.gui.IdentifiableGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -/** - * Opens GUIs by id or class on the Bukkit main thread. - * - *

Responsibilities:

- *
    - *
  • Lookup GUIs via {@link GuiRegistry},
  • - *
  • Invoke {@code BaseGui#open(Player)} on the main thread using {@link TaskScheduler}.
  • - *
- * - * Thread-safety: Safe to call from any thread. Actual GUI operations are marshalled to the main thread. - */ public final class GuiOpener { private final GuiRegistry registry; @@ -27,110 +15,85 @@ public final class GuiOpener { @Inject public GuiOpener(@NotNull GuiRegistry registry, @NotNull TaskScheduler taskScheduler) { - this.registry = Validator.notNull(registry, "registry cannot be null"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); + this.registry = registry; + this.taskScheduler = taskScheduler; } - /** - * Opens a non-parameterized GUI by its concrete class. - * - * @throws IllegalArgumentException if GUI is not registered or not a {@link SimpleGui} - */ public void open( @NotNull Class type, - @NotNull Player viewer) { - Validator.notNull(type, "type cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - - IdentifiableGui gui = require(type); + @NotNull Player viewer + ) { + final IdentifiableGui gui = require(type); if (!(gui instanceof SimpleGui simpleGui)) { throw wrongType(type.getName(), gui, "SimpleGui"); } - BaseGui baseGui = simpleGui.createGui(); + final BaseGui baseGui = simpleGui.createGui(); simpleGui.prepareItems(baseGui, viewer); taskScheduler.runSync(() -> baseGui.open(viewer)); } - /** - * Opens a parameterized GUI by its concrete class. - * - * @throws IllegalArgumentException if GUI is not registered or not a {@link ParameterizedGui} - */ @SuppressWarnings("unchecked") - public void open(@NotNull Class> type, - @NotNull Player viewer, - @NotNull T parameter) { - Validator.notNull(type, "type cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(parameter, "parameter cannot be null"); - - IdentifiableGui gui = require(type); + public void open( + @NotNull Class> type, + @NotNull Player viewer, + @NotNull T parameter + ) { + final IdentifiableGui gui = require(type); if (!(gui instanceof ParameterizedGui paramGui)) { throw wrongType(type.getName(), gui, "ParameterizedGui"); } - ParameterizedGui typed = (ParameterizedGui) paramGui; - BaseGui baseGui = typed.createGui(viewer, parameter); + final ParameterizedGui typed = (ParameterizedGui) paramGui; + final BaseGui baseGui = typed.createGui(viewer, parameter); + typed.prepareItems(baseGui, viewer, parameter); taskScheduler.runSync(() -> baseGui.open(viewer)); } - /** - * Opens a non-parameterized GUI by id for the given player. - * - * @throws IllegalArgumentException if id is unknown or GUI is not a {@link SimpleGui} - */ + public void open( @NotNull String id, - @NotNull Player viewer) { - Validator.notNull(id, "id cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - - IdentifiableGui gui = require(id); + @NotNull Player viewer + ) { + final IdentifiableGui gui = require(id); if (!(gui instanceof SimpleGui simpleGui)) { throw wrongType(id, gui, "SimpleGui"); } - BaseGui baseGui = simpleGui.createGui(); + final BaseGui baseGui = simpleGui.createGui(); + simpleGui.prepareItems(baseGui, viewer); taskScheduler.runSync(() -> baseGui.open(viewer)); } - /** - * Opens a parameterized GUI by id for the given player. - * - * @throws IllegalArgumentException if id is unknown or GUI is not a {@link ParameterizedGui} - */ @SuppressWarnings("unchecked") public void open( @NotNull String id, @NotNull Player viewer, - @NotNull T parameter) { - Validator.notNull(id, "id cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(parameter, "parameter cannot be null"); - - IdentifiableGui gui = require(id); + @NotNull T parameter + ) { + final IdentifiableGui gui = require(id); if (!(gui instanceof ParameterizedGui paramGui)) { throw wrongType(id, gui, "ParameterizedGui"); } - ParameterizedGui typed = (ParameterizedGui) paramGui; - BaseGui baseGui = typed.createGui(viewer, parameter); + final ParameterizedGui typed = (ParameterizedGui) paramGui; + final BaseGui baseGui = typed.createGui(viewer, parameter); + typed.prepareItems(baseGui, viewer, parameter); taskScheduler.runSync(() -> baseGui.open(viewer)); } - private @NotNull IdentifiableGui require(@NotNull String id) { - IdentifiableGui gui = registry.getById(id); + private IdentifiableGui require(String id) { + final IdentifiableGui gui = registry.getById(id); if (gui == null) { throw new IllegalArgumentException("No GUI registered under id '" + id + "'"); } return gui; } - private @NotNull IdentifiableGui require(@NotNull Class type) { - IdentifiableGui gui = registry.getByClass(type); + private IdentifiableGui require(Class type) { + final IdentifiableGui gui = registry.getByClass(type); if (gui == null) { throw new IllegalArgumentException("No GUI registered for class '" + type.getName() + "'"); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java index 843cd10..9d330b4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java @@ -5,7 +5,6 @@ import com.github.imdmk.playtime.platform.gui.render.RenderContext; import com.github.imdmk.playtime.platform.gui.render.RenderOptions; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import dev.triumphteam.gui.guis.PaginatedGui; import org.bukkit.entity.Player; @@ -15,116 +14,75 @@ import java.time.Duration; import java.util.function.Consumer; -/** - * Places navigation controls (Next, Previous, Exit) into Triumph GUIs. - *

- * Responsibilities: - *

    - *
  • Compute target slots via {@link GridSlots},
  • - *
  • Delegate permission/policy enforcement to {@link GuiRenderer},
  • - *
  • Provide short-lived feedback (e.g., "no next/previous") and restore original items.
  • - *
- * - * Threading: All methods are expected to be called on the Bukkit main thread. - * The class is stateless w.r.t. rendering; it holds only injected collaborators. - */ final class NavigationBar { - private static final Duration RESTORE_DELAY = Duration.ofSeconds(1); + private static final Duration DELAY = Duration.ofSeconds(1); private final NavigationBarConfig config; private final TaskScheduler scheduler; private final GuiRenderer renderer; private final RenderOptions renderOptions; - /** - * @param config navigation bar config (items, etc.) - * @param scheduler scheduler for short delayed updates - * @param renderer GUI renderer enforcing permission policy - * @param renderOptions render options (no-permission policy, onDenied) - */ NavigationBar( @NotNull NavigationBarConfig config, @NotNull TaskScheduler scheduler, @NotNull GuiRenderer renderer, @NotNull RenderOptions renderOptions ) { - this.config = Validator.notNull(config, "config cannot be null"); - this.renderer = Validator.notNull(renderer, "renderer cannot be null"); - this.scheduler = Validator.notNull(scheduler, "scheduler cannot be null"); - this.renderOptions = Validator.notNull(renderOptions, "renderOptions cannot be null"); + this.config = config; + this.renderer = renderer; + this.scheduler = scheduler; + this.renderOptions = renderOptions; } - /** - * Places the "Next page" button if {@code gui} is paginated. - */ void setNext(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - if (!(gui instanceof PaginatedGui paginated)) { return; } - final var context = RenderContext.defaultContext(viewer); - final var slot = GridSlots.next(gui.getRows()); + final RenderContext context = RenderContext.defaultContext(viewer); + final int slot = GridSlots.next(gui.getRows()); final Consumer onClick = event -> { if (!paginated.next()) { renderer.setItem(gui, event.getSlot(), config.noNextItem, context, renderOptions, this::noop); - restoreLater(() -> setNext(gui, viewer)); + runLater(() -> setNext(gui, viewer)); } }; renderer.setItem(gui, slot, config.nextItem, context, renderOptions, onClick); } - /** - * Places the "Previous page" button if {@code gui} is paginated. - */ void setPrevious(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - if (!(gui instanceof PaginatedGui paginated)) { return; } - final var context = RenderContext.defaultContext(viewer); - final var slot = GridSlots.previous(gui.getRows()); + final RenderContext context = RenderContext.defaultContext(viewer); + final int slot = GridSlots.previous(gui.getRows()); final Consumer onClick = event -> { if (!paginated.previous()) { renderer.setItem(gui, event.getSlot(), config.noPreviousItem, context, renderOptions, this::noop); - restoreLater(() -> setPrevious(gui, viewer)); + runLater(() -> setPrevious(gui, viewer)); } }; renderer.setItem(gui, slot, config.previousItem, context, renderOptions, onClick); } - /** - * Places the "Exit" button which triggers the provided action. - */ void setExit( @NotNull BaseGui gui, @NotNull Player viewer, - @NotNull Consumer exit) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(exit, "exit cannot be null"); - - final var context = RenderContext.defaultContext(viewer); - final var slot = GridSlots.exit(gui.getRows()); + @NotNull Consumer exit + ) { + final RenderContext context = RenderContext.defaultContext(viewer); + final int slot = GridSlots.exit(gui.getRows()); renderer.setItem(gui, slot, config.exitItem, context, renderOptions, exit); } - /** - * Schedules a short delayed restore action (e.g., after showing "no next/previous"). - */ - private void restoreLater(@NotNull Runnable restoreAction) { - Validator.notNull(restoreAction, "restoreAction cannot be null"); - scheduler.runLaterSync(restoreAction, RESTORE_DELAY); + private void runLater(Runnable runnable) { + scheduler.runLaterSync(runnable, DELAY); } private void noop(@NotNull InventoryClickEvent e) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java index 8a8c924..bda1ecd 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java @@ -5,29 +5,10 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -/** - * Represents a generic GUI that requires a parameter to be initialized and populated. - * Acts as a template for all GUIs that are parameterized, defining a default open lifecycle. - * - * @param the type of parameter used for populating the GUI - */ public interface ParameterizedGui extends IdentifiableGui { - /** - * Creates a new instance of the GUI. - * - * @param viewer the player viewing the GUI - * @param parameter the parameter used to customize the GUI - * @return the initialized {@link BaseGui} instance - */ BaseGui createGui(@NotNull Player viewer, @NotNull T parameter); - /** - * Prepares and populates the GUI with core content based on the parameter. - * - * @param gui the GUI to populate - * @param viewer the player viewing the GUI - * @param parameter the context parameter - */ void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull T parameter); + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java index fa235d1..0144eaf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java @@ -5,24 +5,9 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -/** - * Represents a simple GUI that does not require a parameter to be created or populated. - * Defines a standard lifecycle for opening such GUIs. - */ public interface SimpleGui extends IdentifiableGui { - /** - * Creates a new instance of the GUI. - * - * @return the initialized {@link BaseGui} instance - */ BaseGui createGui(); - /** - * Prepares and populates the GUI with its core content. - * - * @param gui the GUI to populate - * @param viewer the player viewing the GUI - */ void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java index f454964..4c13fb1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.platform.litecommands; import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invalidusage.InvalidUsageHandler; @@ -15,7 +14,7 @@ public final class InvalidUsageHandlerImpl implements InvalidUsageHandler invocation, Notice notice, ResultHandlerChain chain) { - messageService.send(invocation.sender(), n -> notice); + messageService.create() + .viewer(invocation.sender()) + .notice(n -> notice) + .send(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java index 5623ca7..f15ea04 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java @@ -1,56 +1,21 @@ package com.github.imdmk.playtime.platform.logger; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; -/** - * Bukkit-specific implementation of {@link PluginLogger} delegating to a standard - * {@link java.util.logging.Logger} obtained from a Bukkit {@link Plugin}. - * - *

This class provides formatted and structured logging methods for common log levels - * (INFO, WARNING, DEBUG, SEVERE) with support for formatted messages and throwable logging.

- * - *

Design notes:

- *
    - *
  • Acts as a lightweight adapter to bridge the internal plugin logging interface with Bukkit’s logger.
  • - *
  • Formatting uses {@link String#format(Locale, String, Object...)} with {@link Locale#ROOT} to ensure locale safety.
  • - *
  • Supports overloaded methods for flexible log message creation, including formatted and exception-based variants.
  • - *
- * - *

Thread-safety: Delegates to the underlying {@link Logger}, which is thread-safe for concurrent use.

- * - * @see PluginLogger - * @see Plugin#getLogger() - * @see Logger - */ public final class BukkitPluginLogger implements PluginLogger { /** Backing {@link java.util.logging.Logger} provided by Bukkit. */ private final Logger logger; - /** - * Creates a new {@code BukkitPluginLogger} wrapping an existing {@link Logger}. - * - * @param logger non-null backing logger instance - * @throws NullPointerException if {@code logger} is null - */ - public BukkitPluginLogger(@NotNull Logger logger) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - } - - /** - * Creates a new {@code BukkitPluginLogger} bound to the given Bukkit {@link Plugin}. - * - * @param plugin non-null Bukkit plugin instance - * @throws NullPointerException if {@code plugin} is null - */ + @Inject public BukkitPluginLogger(@NotNull Plugin plugin) { - this(plugin.getLogger()); + this.logger = plugin.getLogger(); } @Override @@ -108,17 +73,7 @@ public void error(@NotNull Throwable throwable, @NotNull String message, @NotNul logger.log(Level.SEVERE, format(message, args), throwable); } - /** - * Formats a message using {@link String#format(Locale, String, Object...)} with {@link Locale#ROOT}. - * - * @param message format string (non-null) - * @param args format arguments (non-null) - * @return formatted message - * @throws NullPointerException if {@code message} or {@code args} is null - */ - private String format(@NotNull String message, @NotNull Object... args) { - Validator.notNull(message, "message cannot be null"); - Validator.notNull(args, "args cannot be null"); + private String format(String message, Object... args) { return String.format(Locale.ROOT, message, args); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java index b86e551..3f4afda 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java @@ -5,30 +5,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** - * Plugin-level abstraction for a single placeholder. - *

- * This interface is framework-agnostic and does not depend on PlaceholderAPI. - * Implementations can be adapted to any placeholder platform. - */ public interface PluginPlaceholder { - /** - * Unique identifier of the placeholder set. - * Example: "playtime" - */ @NotNull String identifier(); - /** - * Called for online players, if supported by the underlying platform. - */ default @Nullable String onRequest(@NotNull Player player, @NotNull String params) { return null; } - /** - * Called for offline players, if supported by the underlying platform. - */ default @Nullable String onRequest(@NotNull OfflinePlayer player, @NotNull String params) { return null; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java index e1fb145..42e8421 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java @@ -3,31 +3,14 @@ import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import org.jetbrains.annotations.NotNull; -/** - * Strategy for registering {@link PluginPlaceholder} instances - * against a concrete placeholder platform (e.g. PlaceholderAPI), - * or acting as a no-op implementation when the platform is not present. - */ public interface PlaceholderAdapter { - /** - * @return {@code true} if the underlying placeholder platform is available. - */ boolean isAvailable(); - /** - * Registers the given placeholder if the platform is available. - */ void register(@NotNull PluginPlaceholder placeholder); - /** - * Unregisters the given placeholder, if it was registered. - */ void unregister(@NotNull PluginPlaceholder placeholder); - /** - * Unregisters all placeholders managed by this registrar. - */ void unregisterAll(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java index baa0e53..35c230f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java @@ -1,59 +1,23 @@ package com.github.imdmk.playtime.platform.placeholder.adapter; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.Server; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; -/** - * Factory responsible for creating the appropriate {@link PlaceholderAdapter} - * implementation based on runtime plugin availability. - * - *

This class detects whether PlaceholderAPI is installed and enabled on the server. - * Depending on its presence, it returns either:

- * - *
    - *
  • {@link PlaceholderAPIAdapter} – full integration with PlaceholderAPI;
  • - *
  • {@link NoopPlaceholderAdapter} – a no-operation fallback that safely disables - * placeholder support without causing errors.
  • - *
- * - *

This allows the plugin to offer optional PlaceholderAPI integration without requiring it - * as a hard dependency, while keeping all placeholder logic abstracted behind - * the {@link PlaceholderAdapter} interface.

- * - *

Thread-safety: The factory contains no mutable state and is fully thread-safe.

- */ public final class PlaceholderAdapterFactory { private static final String PLACEHOLDER_API_NAME = "PlaceholderAPI"; - /** - * Creates a {@link PlaceholderAdapter} appropriate for the current server environment. - * - *

If PlaceholderAPI is detected and enabled, a {@link PlaceholderAPIAdapter} is returned. - * Otherwise, a {@link NoopPlaceholderAdapter} is provided, which safely performs no operations.

- * - * @param plugin the owning plugin instance; must not be null - * @param server the Bukkit server instance; must not be null - * @param logger the plugin logger for diagnostic output; must not be null - * @return a fully initialized placeholder adapter suitable for the environment - * @throws NullPointerException if any argument is null - */ public static PlaceholderAdapter createFor( @NotNull Plugin plugin, @NotNull Server server, @NotNull PluginLogger logger ) { - Validator.notNull(plugin, "plugin cannot be null"); - Validator.notNull(server, "server cannot be null"); - Validator.notNull(logger, "logger cannot be null"); - - boolean isEnabled = server.getPluginManager().isPluginEnabled(PLACEHOLDER_API_NAME); + final boolean isEnabled = server.getPluginManager().isPluginEnabled(PLACEHOLDER_API_NAME); if (isEnabled) { - logger.info("PlaceholderAPI detected — using PlaceholderApiAdapter."); - return new PlaceholderAPIAdapter(plugin, logger); + logger.info("PlaceholderAPI detected — using PlaceholderAdapterImpl."); + return new PlaceholderAdapterImpl(plugin, logger); } logger.info("PlaceholderAPI not found — using NoopPlaceholderAdapter."); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAPIAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java similarity index 85% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAPIAdapter.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java index 5be739c..75c8bfe 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAPIAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; -import com.github.imdmk.playtime.shared.validate.Validator; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -13,16 +12,16 @@ import java.util.HashMap; import java.util.Map; -final class PlaceholderAPIAdapter implements PlaceholderAdapter { +final class PlaceholderAdapterImpl implements PlaceholderAdapter { private final Plugin plugin; private final PluginLogger logger; private final Map expansions = new HashMap<>(); - PlaceholderAPIAdapter(@NotNull Plugin plugin, @NotNull PluginLogger logger) { - this.plugin = Validator.notNull(plugin, "plugin cannot be null"); - this.logger = Validator.notNull(logger, "logger cannot be null"); + PlaceholderAdapterImpl(@NotNull Plugin plugin, @NotNull PluginLogger logger) { + this.plugin = plugin; + this.logger = logger; } @Override @@ -32,8 +31,6 @@ public boolean isAvailable() { @Override public void register(@NotNull PluginPlaceholder placeholder) { - Validator.notNull(placeholder, "placeholder cannot be null"); - if (expansions.containsKey(placeholder)) { logger.warn("Placeholder with name %s is already registered!", placeholder.identifier()); return; @@ -47,8 +44,6 @@ public void register(@NotNull PluginPlaceholder placeholder) { @Override public void unregister(@NotNull PluginPlaceholder placeholder) { - Validator.notNull(placeholder, "placeholder cannot be null"); - final PlaceholderExpansion expansion = expansions.remove(placeholder); if (expansion != null) { expansion.unregister(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java index 2c07b97..f0a2c1d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java @@ -1,147 +1,84 @@ package com.github.imdmk.playtime.platform.scheduler; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; import java.time.Duration; -/** - * {@link TaskScheduler} implementation backed by the Bukkit {@link BukkitScheduler}. - * - *

Provides a clean, Duration-based API for scheduling synchronous and asynchronous - * tasks, including delayed and repeating executions.

- * - *

All time values are expressed using {@link Duration} and internally converted - * to Minecraft ticks (1 tick = 50 ms).

- * - *

Thread-safety: This class is thread-safe. It holds only immutable - * references to {@link Plugin} and {@link BukkitScheduler}.

- */ +@Service(priority = Priority.LOWEST) public final class BukkitTaskScheduler implements TaskScheduler { - /** Number of milliseconds per Minecraft tick. */ private static final long MILLIS_PER_TICK = 50L; private final Plugin plugin; private final BukkitScheduler scheduler; + @Inject public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler scheduler) { - this.plugin = Validator.notNull(plugin, "plugin cannot be null"); - this.scheduler = Validator.notNull(scheduler, "scheduler cannot be null"); + this.plugin = plugin; + this.scheduler = scheduler; } @Override public @NotNull BukkitTask runSync(@NotNull Runnable runnable) { - Validator.notNull(runnable, "runnable cannot be null"); return scheduler.runTask(plugin, runnable); } - @Override - public @NotNull BukkitTask runSync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return scheduler.runTask(plugin, task); - } - @Override public @NotNull BukkitTask runAsync(@NotNull Runnable runnable) { - Validator.notNull(runnable, "runnable cannot be null"); return scheduler.runTaskAsynchronously(plugin, runnable); } @Override - public @NotNull BukkitTask runAsync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return scheduler.runTaskAsynchronously(plugin, task); - } - - @Override - public @NotNull BukkitTask runLaterAsync(@NotNull Runnable runnable, @NotNull Duration delay) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); + public @NotNull BukkitTask runLaterAsync( + @NotNull Runnable runnable, + @NotNull Duration delay + ) { return scheduler.runTaskLaterAsynchronously(plugin, runnable, toTicks(delay)); } @Override - public @NotNull BukkitTask runLaterAsync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runLaterAsync(task, task.delay()); - } - - @Override - public @NotNull BukkitTask runLaterSync(@NotNull Runnable runnable, @NotNull Duration delay) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); + public @NotNull BukkitTask runLaterSync( + @NotNull Runnable runnable, + @NotNull Duration delay + ) { return scheduler.runTaskLater(plugin, runnable, toTicks(delay)); } - @Override - public @NotNull BukkitTask runLaterSync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runLaterSync(task, task.delay()); - } - @Override public @NotNull BukkitTask runTimerSync( @NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period ) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); - Validator.notNull(period, "period cannot be null"); - return scheduler.runTaskTimer(plugin, runnable, toTicks(delay), toTicks(period)); } - @Override - public @NotNull BukkitTask runTimerSync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runTimerSync(task, task.delay(), task.period()); - } - @Override public @NotNull BukkitTask runTimerAsync( @NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period ) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); - Validator.notNull(period, "period cannot be null"); - return scheduler.runTaskTimerAsynchronously(plugin, runnable, toTicks(delay), toTicks(period)); } - @Override - public @NotNull BukkitTask runTimerAsync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runTimerAsync(task, task.delay(), task.period()); - } - @Override public void cancelTask(int taskId) { scheduler.cancelTask(taskId); } @Override - public void shutdown() { + public void cancelAllTasks() { scheduler.cancelTasks(plugin); } - /** - * Converts the given duration to Minecraft ticks. - *

- * Fractions are truncated. Negative durations return {@code 0}. - * - * @param duration duration to convert; must not be null - * @return number of ticks (≥ 0) - */ - private static int toTicks(@NotNull Duration duration) { - Validator.notNull(duration, "duration cannot be null"); - + private static int toTicks(Duration duration) { long ticks = duration.toMillis() / MILLIS_PER_TICK; return ticks <= 0 ? 0 : (int) ticks; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/PluginTask.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/PluginTask.java deleted file mode 100644 index 21ea995..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/PluginTask.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.imdmk.playtime.platform.scheduler; - -import java.time.Duration; - -/** - * Represents a declarative task definition used by the {@link TaskScheduler}. - * - *

A {@code PluginTask} bundles together:

- *
    - *
  • the executable logic ({@link #run()}),
  • - *
  • a delay before the first execution ({@link #delay()}),
  • - *
  • an optional repeating period ({@link #period()}).
  • - *
- * - *

Instances are consumed by scheduler methods that accept {@link PluginTask}, - * allowing tasks to be declared as self-contained objects instead of passing - * raw parameters into every scheduling call.

- * - *

Repeating vs. non-repeating:

- *
    - *
  • If {@link #period()} returns {@code Duration.ZERO}, the task is executed once after the delay.
  • - *
  • If {@link #period()} is greater than zero, the task is executed repeatedly.
  • - *
- * - *

Threading: Whether the task runs synchronously or asynchronously - * depends solely on the {@link TaskScheduler} method used (e.g., {@code runTimerSync}, {@code runTimerAsync}).

- */ -public interface PluginTask extends Runnable { - - /** - * The task logic to be executed by the scheduler. - *

- * Called either once (if {@link #period()} is zero) or repeatedly - * (if {@link #period()} is greater than zero), depending on how - * the task is scheduled. - *

- */ - @Override - void run(); - - /** - * Returns the delay before the first execution. - * - *

A zero delay means the task should run immediately.

- * - * @return the initial delay, never {@code null} - */ - Duration delay(); - - /** - * Returns the repeat period for this task. - * - *

If this returns {@code Duration.ZERO}, the task is treated as - * a one-shot task and will not repeat after the first execution.

- * - *

If the value is greater than zero, the scheduler executes the - * task repeatedly with this interval.

- * - * @return the repeat interval, never {@code null} - */ - Duration period(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java index dde36b9..42ca5f1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java @@ -5,148 +5,21 @@ import java.time.Duration; -/** - * Abstraction layer over the Bukkit {@link org.bukkit.scheduler.BukkitScheduler}, - * providing a clean, consistent API for scheduling synchronous and asynchronous tasks - * using either raw {@link Runnable} instances or declarative {@link PluginTask} objects. - * - *

Threading rules:

- *
    - *
  • Sync methods execute on the main server thread.
  • - *
  • Async methods execute off the main thread and must not access Bukkit API objects that require sync.
  • - *
- * - *

Delay & period units: All {@code Duration} values are converted to - * Minecraft ticks (1 tick = 50ms).

- * - *

PluginTask usage: All overloads accepting {@link PluginTask} - * automatically use the task's declared delay and period.

- */ public interface TaskScheduler { - /** - * Executes the given runnable immediately on the main server thread. - * - * @param runnable non-null logic to execute - * @return the task handle - */ BukkitTask runSync(@NotNull Runnable runnable); - /** - * Executes the given {@link PluginTask} immediately on the main server thread. - * - *

{@link PluginTask#delay()} and {@link PluginTask#period()} are ignored; - * this method always runs instantly.

- * - * @param task non-null task instance - * @return the task handle - */ - BukkitTask runSync(@NotNull PluginTask task); - - /** - * Executes the given runnable immediately on a separate thread. - * - * @param runnable non-null logic to execute asynchronously - * @return the task handle - */ BukkitTask runAsync(@NotNull Runnable runnable); - /** - * Executes the given {@link PluginTask} immediately on a separate thread. - * - *

{@link PluginTask#delay()} and {@link PluginTask#period()} are ignored; - * this method always runs instantly.

- * - * @param task non-null task instance - * @return the task handle - */ - BukkitTask runAsync(@NotNull PluginTask task); - - /** - * Executes the runnable asynchronously after the given delay. - * - * @param runnable task logic - * @param delay delay before execution (converted to ticks) - * @return the task handle - */ BukkitTask runLaterAsync(@NotNull Runnable runnable, @NotNull Duration delay); - /** - * Executes the {@link PluginTask} asynchronously after {@link PluginTask#delay()}. - * - *

Runs once unless {@link PluginTask#period()} is non-zero.

- * - * @param task task definition - * @return the task handle - */ - BukkitTask runLaterAsync(@NotNull PluginTask task); - - /** - * Executes the runnable synchronously after the given delay. - * - * @param runnable task logic - * @param delay delay before execution (converted to ticks) - * @return the task handle - */ BukkitTask runLaterSync(@NotNull Runnable runnable, @NotNull Duration delay); - /** - * Executes the {@link PluginTask} synchronously after {@link PluginTask#delay()}. - * - *

Runs once unless {@link PluginTask#period()} is non-zero.

- * - * @param task task definition - * @return the task handle - */ - BukkitTask runLaterSync(@NotNull PluginTask task); - - /** - * Schedules a synchronous repeating task. - * - * @param runnable logic to execute - * @param delay initial delay before the first run - * @param period time between runs - * @return the created repeating task - */ BukkitTask runTimerSync(@NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period); - /** - * Schedules a synchronous repeating {@link PluginTask} using its delay/period. - * - * @param task task definition - * @return the created repeating task - */ - BukkitTask runTimerSync(@NotNull PluginTask task); - - /** - * Schedules an asynchronous repeating task. - * - * @param runnable logic to execute - * @param delay initial delay before the first execution - * @param period time between consecutive executions - * @return the created repeating task - */ BukkitTask runTimerAsync(@NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period); - /** - * Schedules an asynchronous repeating {@link PluginTask} using its delay/period. - * - * @param task task definition - * @return the created repeating task - */ - BukkitTask runTimerAsync(@NotNull PluginTask task); - - /** - * Cancels a scheduled task via its Bukkit ID. - * - * @param taskId scheduler task ID - */ void cancelTask(int taskId); - /** - * Cancels all tasks created for the associated plugin. - * - *

Called during plugin shutdown.

- */ - void shutdown(); + void cancelAllTasks(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java index dd3f53c..f29cf2e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java @@ -7,21 +7,8 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; -/** - * Defines formatting strategies for converting a {@link Duration} into - * a human-readable string. - *

- * Each style provides its own implementation of {@link #format(Duration)}. - * The underlying logic splits the duration into days, hours, minutes and seconds - * and then renders only non-zero units in a style-specific way. - */ public enum DurationFormatStyle { - /** - * Compact representation using short unit abbreviations. - *

- * Example: {@code 30d 30m 3s} - */ COMPACT { @Override public String format(@NotNull Duration duration) { @@ -30,12 +17,6 @@ public String format(@NotNull Duration duration) { Separator.SPACE); } }, - - /** - * Long form with full unit names, separated by spaces. - *

- * Example: {@code 30 days 30 minutes 3 seconds} - */ LONG { @Override public String format(@NotNull Duration duration) { @@ -44,12 +25,6 @@ public String format(@NotNull Duration duration) { Separator.SPACE); } }, - - /** - * Long form with {@code " and "} between units. - *

- * Example: {@code 30 days and 30 minutes and 3 seconds} - */ LONG_WITH_AND { @Override public String format(@NotNull Duration duration) { @@ -58,12 +33,6 @@ public String format(@NotNull Duration duration) { Separator.AND); } }, - - /** - * Natural language-like form using commas between units. - *

- * Example: {@code 30 days, 30 minutes, 3 seconds} - */ NATURAL { @Override public String format(@NotNull Duration duration) { @@ -73,42 +42,20 @@ public String format(@NotNull Duration duration) { } }; - /** - * Formats the given {@link Duration} using this style. - *

- * The duration is first decomposed into days, hours, minutes and seconds, - * and only non-zero units are included in the output. - * - * @param duration the duration to format; must not be {@code null} - * @return formatted duration according to this style (never {@code null}) - */ public abstract String format(@NotNull Duration duration); - /** - * Joins non-zero units of the given duration using the provided formatter - * and separator. - * - * @param duration duration to format - * @param valueFormatter function converting (unit, value) → string - * @param separator separator strategy - * @return formatted string, or empty string if all units are zero - */ protected static String formatWith( @NotNull Duration duration, @NotNull BiFunction valueFormatter, @NotNull Separator separator ) { final Map parts = DurationSplitter.split(duration); - return parts.entrySet().stream() .filter(e -> e.getValue() > 0) .map(e -> valueFormatter.apply(e.getKey(), e.getValue())) .collect(Collectors.joining(separator.value())); } - /** - * Separator strategies used between formatted units. - */ protected enum Separator { SPACE(" "), @@ -121,12 +68,6 @@ protected enum Separator { this.value = value; } - /** - * Returns the underlying separator string. - * - * @return separator value - */ - @NotNull public String value() { return value; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java index 58f9476..7d37232 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java @@ -1,36 +1,20 @@ package com.github.imdmk.playtime.shared.time; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import java.time.Duration; import java.util.EnumMap; import java.util.Map; -/** - * Utility class responsible for splitting a {@link Duration} - * into its component units (days, hours, minutes, seconds). - *

- * This keeps the extraction logic in a single place, shared across - * different formatters. - */ public final class DurationSplitter { private DurationSplitter() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Splits the given duration into ordered units: days, hours, minutes, seconds. - * - * @param duration the duration to split (non-null) - * @return map of {@link DurationUnit} to its value in the given duration - */ - public static @NotNull Map split(@NotNull Duration duration) { - Validator.notNull(duration, "duration"); - - EnumMap parts = new EnumMap<>(DurationUnit.class); - for (DurationUnit unit : DurationUnit.ORDERED) { + public static Map split(@NotNull Duration duration) { + final EnumMap parts = new EnumMap<>(DurationUnit.class); + for (final DurationUnit unit : DurationUnit.ORDERED) { parts.put(unit, unit.extract(duration)); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java index 590694e..5f6356e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java @@ -55,12 +55,12 @@ public int extract(@NotNull Duration duration) { public abstract int extract(@NotNull Duration duration); - public @NotNull String getAbbreviation() { + public String getAbbreviation() { return abbreviation; } - public @NotNull String toDisplayName(int value) { - String word = (value == 1 ? singular : plural); + public String toDisplayName(int value) { + final String word = (value == 1 ? singular : plural); return DISPLAY_NAME_FORMAT.formatted(value, word); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java index f8ef205..8ca9ed4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java @@ -1,59 +1,24 @@ package com.github.imdmk.playtime.shared.time; -import com.github.imdmk.playtime.shared.validate.Validator; import org.jetbrains.annotations.NotNull; import java.time.Duration; -/** - * Utility class providing human-readable formatting helpers for {@link Duration}. - *

- * Supports multiple predefined {@link DurationFormatStyle} strategies. - * Zero or negative durations are normalized to {@code "<1s"}. - *

- * This class is stateless apart from the configurable default style. - */ public final class Durations { - /** Upper bound for any clamped duration (10 years). */ private static final Duration MAX_NORMALIZED_DURATION = Duration.ofDays(3650); - - /** Returned when the duration is zero or negative. */ private static final String LESS_THAN_SECOND = "<1s"; - - /** Default style used when no explicit format style is provided. */ private static DurationFormatStyle DEFAULT_FORMAT_STYLE = DurationFormatStyle.NATURAL; private Durations() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - /** - * Formats the given duration using {@link #DEFAULT_FORMAT_STYLE}. - *

- * Zero or negative durations return {@code "<1s"}. - * - * @param duration the duration to format (non-null) - * @return formatted duration string (never {@code null}) - */ - public static @NotNull String format(@NotNull Duration duration) { + public static String format(@NotNull Duration duration) { return format(duration, DEFAULT_FORMAT_STYLE); } - /** - * Formats the given duration using the specified {@link DurationFormatStyle}. - *

- * Zero or negative durations return {@code "<1s"}. - * - * @param duration the duration to format (non-null) - * @param style formatting strategy (non-null) - * @return human-readable duration string (never {@code null}) - * @throws IllegalArgumentException if duration or style are {@code null} - */ - public static @NotNull String format(@NotNull Duration duration, @NotNull DurationFormatStyle style) { - Validator.notNull(duration, "duration"); - Validator.notNull(style, "style"); - + public static String format(@NotNull Duration duration, @NotNull DurationFormatStyle style) { if (duration.isZero() || duration.isNegative()) { return LESS_THAN_SECOND; } @@ -61,31 +26,11 @@ private Durations() { return style.format(duration); } - /** - * Sets the global default {@link DurationFormatStyle} used by - * {@link #format(Duration)}. - *

- * This modifies process-wide behavior and should be configured during - * plugin initialization. - * - * @param style the new default style (non-null) - * @throws IllegalArgumentException if the provided style is {@code null} - */ public static void setDefaultFormatStyle(@NotNull DurationFormatStyle style) { - Validator.notNull(style, "durationFormatStyle"); DEFAULT_FORMAT_STYLE = style; } - /** - * Normalizes (clamps) the given duration so it’s always non-negative - * and does not exceed {@link #MAX_NORMALIZED_DURATION}. - * - * @param input duration to normalize (must not be null) - * @return clamped, non-negative duration - */ - public static @NotNull Duration clamp(@NotNull Duration input) { - Validator.notNull(input, "duration"); - + public static Duration clamp(@NotNull Duration input) { if (input.isNegative()) { return Duration.ZERO; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java index f356272..0530af8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java @@ -1,62 +1,21 @@ package com.github.imdmk.playtime.shared.validate; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import java.util.function.Consumer; -/** - * Utility class for common validation checks. - *

- * Provides null-safety guards used throughout the codebase. - */ public final class Validator { private Validator() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - /** - * Ensures the given object is not {@code null}. - *

- * This method is typically used to validate constructor arguments and - * configuration values. If the supplied object is non-null, it is returned - * unchanged; otherwise a {@link NullPointerException} is thrown with the - * provided message. - * - * @param obj the value to validate; may be null - * @param context context of exception used when {@code obj} is null - * @param type of the validated object - * @return the non-null value of {@code obj} - * @throws NullPointerException if {@code obj} is null - */ - public static T notNull(@Nullable T obj, @NotNull String context) { + public static T notNull(T obj, String context) { if (obj == null) { throw new NullPointerException(context + " cannot be null"); } return obj; } - /** - * Executes the given {@link Consumer} only if the supplied object is not {@code null}. - *

- * This helper is especially useful during shutdown or cleanup phases where - * optional components may or may not be initialized. The consumer itself - * must be non-null; however, it will only be invoked when {@code obj} is non-null. - * - *

Example usage: - *

-     * Validator.ifNotNull(taskScheduler, TaskScheduler::shutdown);
-     * Validator.ifNotNull(messageService, MessageService::shutdown);
-     * 
- * - * @param obj the object to check before executing the consumer; may be null - * @param consumer operation to execute when {@code obj} is non-null (never null) - * @param type of the object passed to the consumer - * @throws NullPointerException if {@code consumer} is null - */ - public static void ifNotNull(@Nullable T obj, @NotNull Consumer consumer) { - Validator.notNull(consumer, "consumer is null"); + public static void ifNotNull(T obj, Consumer consumer) { if (obj != null) { consumer.accept(obj); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java index 549603d..edada38 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.message.MessageConfig; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.ParseResult; import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; @@ -39,10 +38,10 @@ final class UserArgument extends ArgumentResolver { @NotNull MessageConfig messageConfig, @NotNull UserService userService ) { - this.logger = Validator.notNull(logger, "logger"); - this.server = Validator.notNull(server, "server"); - this.messageConfig = Validator.notNull(messageConfig, "config"); - this.userService = Validator.notNull(userService, "userService"); + this.logger = logger; + this.server = server; + this.messageConfig = messageConfig; + this.userService = userService; } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java index f2fda4a..9ce7704 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java @@ -4,21 +4,9 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -/** - * Platform-to-domain adapter. Creates a new domain {@link User} from - * a Bukkit {@link Player} on first join. No update logic here. - */ public interface UserFactory { - /** - * Creates a fully initialized {@link User} from the given {@link Player}. - * Implementations should set initial playtime using platform stats. - */ - @NotNull User createFrom(@NotNull Player player); + User createFrom(@NotNull Player player); - /** - * Creates a fully initialized {@link User} from the given {@link OfflinePlayer}. - * Implementations should set initial playtime using platform stats. - */ - @NotNull User createFrom(@NotNull OfflinePlayer offlinePlayer); + User createFrom(@NotNull OfflinePlayer offlinePlayer); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserModule.java deleted file mode 100644 index 5919f4d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserModule.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.imdmk.playtime.user; - -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.ListenerPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.RepositoryPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.TaskPhase; -import com.github.imdmk.playtime.user.cache.CaffeineUserCache; -import com.github.imdmk.playtime.user.cache.UserCache; -import com.github.imdmk.playtime.user.listener.UserJoinListener; -import com.github.imdmk.playtime.user.listener.UserQuitListener; -import com.github.imdmk.playtime.user.repository.UserEntityMapper; -import com.github.imdmk.playtime.user.repository.UserRepository; -import com.github.imdmk.playtime.user.repository.UserRepositoryOrmLite; -import com.github.imdmk.playtime.user.top.MemoryTopUsersCache; -import com.github.imdmk.playtime.user.top.TopUsersCache; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class UserModule implements Module { - - private UserCache userCache; - private UserEntityMapper userEntityMapper; - private UserRepository userRepository; - private TopUsersCache topUsersCache; - private UserService userService; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(UserCache.class).assignInstance(() -> this.userCache); - resources.on(UserEntityMapper.class).assignInstance(() -> this.userEntityMapper); - resources.on(UserRepository.class).assignInstance(() -> this.userRepository); - resources.on(TopUsersCache.class).assignInstance(() -> this.topUsersCache); - resources.on(UserService.class).assignInstance(() -> this.userService); - } - - @Override - public void init(@NotNull Injector injector) { - this.userCache = new CaffeineUserCache(); - this.userEntityMapper = new UserEntityMapper(); - this.userRepository = injector.newInstance(UserRepositoryOrmLite.class); - this.topUsersCache = injector.newInstance(MemoryTopUsersCache.class); - this.userService = injector.newInstance(UserServiceImpl.class); - } - - @Override - public ListenerPhase listeners(@NotNull Injector injector) { - return registrar -> registrar.register( - injector.newInstance(UserJoinListener.class), - injector.newInstance(UserQuitListener.class) - ); - } - - @Override - public RepositoryPhase repositories(@NotNull Injector injector) { - return manager -> manager.register(this.userRepository); - } - - @Override - public CommandPhase commands(@NotNull Injector injector) { - return builder -> builder.argument(User.class, injector.newInstance(UserArgument.class)); - } - - @Override - public TaskPhase tasks(@NotNull Injector injector) { - return scheduler -> { - scheduler.runTimerAsync(injector.newInstance(UserSaveTask.class)); - }; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserSaveTask.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserSaveTask.java deleted file mode 100644 index b6a2ce2..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserSaveTask.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.imdmk.playtime.user; - -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.scheduler.PluginTask; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.time.Duration; - -final class UserSaveTask implements PluginTask { - - private static final Duration INITIAL_DELAY = Duration.ofMinutes(30); - private static final Duration INTERVAL = Duration.ofMinutes(30); - - private final Server server; - private final PluginLogger logger; - private final PlaytimeService playtimeService; - private final UserService userService; - - @Inject - UserSaveTask( - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull PlaytimeService playtimeService, - @NotNull UserService userService - ) { - this.server = Validator.notNull(server, "server"); - this.logger = Validator.notNull(logger, "logger"); - this.playtimeService = Validator.notNull(playtimeService, "playtime"); - this.userService = Validator.notNull(userService, "userService"); - } - - @Override - public void run() { - for (final Player player : server.getOnlinePlayers()) { - userService.findCachedByUuid(player.getUniqueId()) - .ifPresent(this::updateAndSaveUser); - } - } - - private void updateAndSaveUser(@NotNull User user) { - UserTime time = playtimeService.getTime(user.getUuid()); - user.setPlaytime(time); - - userService.save(user, UserSaveReason.SCHEDULED_SAVE) - .exceptionally(e -> { - logger.error(e, "Failed to perform scheduled save for user %s (%s)", user.getName(), user.getUuid()); - return null; - }); - } - - @Override - public Duration delay() { - return INITIAL_DELAY; - } - - @Override - public Duration period() { - return INTERVAL; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java index 93286a4..46a1e2e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java @@ -1,11 +1,9 @@ package com.github.imdmk.playtime.user; import com.github.imdmk.playtime.UserDeleteEvent; -import com.github.imdmk.playtime.UserPreSaveEvent; import com.github.imdmk.playtime.UserSaveEvent; -import com.github.imdmk.playtime.platform.events.BukkitEventCaller; +import com.github.imdmk.playtime.platform.event.BukkitEventCaller; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.cache.UserCache; import com.github.imdmk.playtime.user.repository.UserRepository; import com.github.imdmk.playtime.user.top.TopUsersCache; @@ -39,36 +37,32 @@ final class UserServiceImpl implements UserService { @NotNull UserRepository repository, @NotNull BukkitEventCaller eventCaller ) { - this.logger = Validator.notNull(logger, "logger"); - this.cache = Validator.notNull(cache, "cache"); - this.topUsersCache = Validator.notNull(topUsersCache, "topUsersCache"); - this.repository = Validator.notNull(repository, "repository"); - this.eventCaller = Validator.notNull(eventCaller, "eventCaller"); + this.logger = logger; + this.cache = cache; + this.topUsersCache = topUsersCache; + this.repository = repository; + this.eventCaller = eventCaller; } @Override - public @NotNull Optional findCachedByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); + public Optional findCachedByUuid(@NotNull UUID uuid) { return cache.getUserByUuid(uuid); } @Override - public @NotNull Optional findCachedByName(@NotNull String name) { - Validator.notNull(name, "name"); + public Optional findCachedByName(@NotNull String name) { return cache.getUserByName(name); } @Override @Unmodifiable - public @NotNull Collection getCachedUsers() { + public Collection getCachedUsers() { return cache.getCache(); // returns unmodifiable } @Override - public @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - - Optional cached = cache.getUserByUuid(uuid); + public CompletableFuture> findByUuid(@NotNull UUID uuid) { + final Optional cached = cache.getUserByUuid(uuid); if (cached.isPresent()) { return CompletableFuture.completedFuture(cached); } @@ -86,10 +80,8 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture> findByName(@NotNull String name) { - Validator.notNull(name, "name"); - - Optional cached = cache.getUserByName(name); + public CompletableFuture> findByName(@NotNull String name) { + final Optional cached = cache.getUserByName(name); if (cached.isPresent()) { return CompletableFuture.completedFuture(cached); } @@ -107,17 +99,12 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture> findTopByPlayTime(int limit) { + public CompletableFuture> findTopByPlayTime(int limit) { return topUsersCache.getTopByPlayTime(limit); } @Override - public @NotNull CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason) { - Validator.notNull(user, "user"); - Validator.notNull(reason, "reason"); - - eventCaller.callEvent(new UserPreSaveEvent(user, reason)); - + public CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason) { return repository.save(user) .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .thenApply(saved -> { @@ -132,8 +119,7 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); + public CompletableFuture deleteByUuid(@NotNull UUID uuid) { return repository.deleteByUuid(uuid) .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .thenApply(result -> { @@ -150,8 +136,7 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture deleteByName(@NotNull String name) { - Validator.notNull(name, "name"); + public CompletableFuture deleteByName(@NotNull String name) { return repository.deleteByName(name) .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .thenApply(deleteResult -> { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java index cc45992..349ec7c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java @@ -3,10 +3,14 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.user.User; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; +import org.panda_lang.utilities.inject.annotations.Inject; import java.time.Duration; import java.util.ArrayList; @@ -16,10 +20,7 @@ import java.util.UUID; import java.util.function.Consumer; -/** - * Caffeine-based implementation of {@link UserCache} with dual indexing by UUID and name. - * Entries expire after a period of inactivity and after a maximum lifetime. - */ +@Service(priority = Priority.LOWEST) public final class CaffeineUserCache implements UserCache { private static final Duration DEFAULT_EXPIRE_AFTER_ACCESS = Duration.ofHours(2); @@ -29,9 +30,6 @@ public final class CaffeineUserCache implements UserCache { private final Cache cacheByName; public CaffeineUserCache(@NotNull Duration expireAfterAccess, @NotNull Duration expireAfterWrite) { - Validator.notNull(expireAfterAccess, "expireAfterAccess"); - Validator.notNull(expireAfterWrite, "expireAfterWrite"); - this.cacheByName = Caffeine.newBuilder() .expireAfterWrite(expireAfterWrite) .expireAfterAccess(expireAfterAccess) @@ -48,14 +46,13 @@ public CaffeineUserCache(@NotNull Duration expireAfterAccess, @NotNull Duration .build(); } + @Inject public CaffeineUserCache() { this(DEFAULT_EXPIRE_AFTER_ACCESS, DEFAULT_EXPIRE_AFTER_WRITE); } @Override public void cacheUser(@NotNull User user) { - Validator.notNull(user, "user"); - final UUID uuid = user.getUuid(); final String name = user.getName(); @@ -73,16 +70,12 @@ public void cacheUser(@NotNull User user) { @Override public void invalidateUser(@NotNull User user) { - Validator.notNull(user, "user"); - cacheByUuid.invalidate(user.getUuid()); cacheByName.invalidate(user.getName()); } @Override public void invalidateByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - final User cached = cacheByUuid.getIfPresent(uuid); cacheByUuid.invalidate(uuid); if (cached != null) { @@ -92,8 +85,6 @@ public void invalidateByUuid(@NotNull UUID uuid) { @Override public void invalidateByName(@NotNull String name) { - Validator.notNull(name, "name"); - final UUID uuid = cacheByName.getIfPresent(name); if (uuid != null) { invalidateByUuid(uuid); @@ -103,24 +94,18 @@ public void invalidateByName(@NotNull String name) { } @Override - public @NotNull Optional getUserByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); + public Optional getUserByUuid(@NotNull UUID uuid) { return Optional.ofNullable(cacheByUuid.getIfPresent(uuid)); } @Override - public @NotNull Optional getUserByName(@NotNull String name) { - Validator.notNull(name, "name"); - + public Optional getUserByName(@NotNull String name) { final UUID uuid = cacheByName.getIfPresent(name); return uuid == null ? Optional.empty() : Optional.ofNullable(cacheByUuid.getIfPresent(uuid)); } @Override public void updateUserNameMapping(@NotNull User user, @NotNull String oldName) { - Validator.notNull(user, "user cannot be null"); - Validator.notNull(oldName, "oldName cannot be null"); - final String newName = user.getName(); if (!oldName.equals(newName)) { cacheByName.invalidate(oldName); @@ -132,8 +117,6 @@ public void updateUserNameMapping(@NotNull User user, @NotNull String oldName) { @Override public void forEachUser(@NotNull Consumer action) { - Validator.notNull(action, "action cannot be null"); - // Snapshot to avoid iterating over a live view while mutating the cache for (final User user : new ArrayList<>(cacheByUuid.asMap().values())) { action.accept(user); @@ -141,13 +124,13 @@ public void forEachUser(@NotNull Consumer action) { } @Override - @NotNull @Unmodifiable public Collection getCache() { return List.copyOf(cacheByUuid.asMap().values()); } @Override + @Subscribe(event = PlayTimeShutdownEvent.class) public void invalidateAll() { cacheByUuid.invalidateAll(); cacheByName.invalidateAll(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java index 109c1e0..98fcdcf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java @@ -2,91 +2,31 @@ import com.github.imdmk.playtime.user.User; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; import java.util.Collection; import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; -/** - * Cache for {@link User} aggregates. - *

- * Implementations are expected to maintain a fast lookup by both UUID and name, - * keep mappings consistent during updates, and guarantee thread-safety - * if used in a concurrent environment. - */ public interface UserCache { - /** - * Adds or replaces the given user in the cache. - * - * @param user the user instance to store - */ void cacheUser(@NotNull User user); - /** - * Removes the given user from the cache, if present. - * - * @param user the user instance to remove - */ void invalidateUser(@NotNull User user); - /** - * Removes a cached user by UUID. - * - * @param uuid the UUID to remove - */ void invalidateByUuid(@NotNull UUID uuid); - /** - * Removes a cached user by name (case-insensitive matching recommended). - * - * @param name the username to remove - */ void invalidateByName(@NotNull String name); - /** - * Retrieves a user by UUID. - * - * @param uuid the UUID to search - * @return an {@link Optional} containing the cached user, if present - */ - @NotNull Optional getUserByUuid(@NotNull UUID uuid); + Optional getUserByUuid(@NotNull UUID uuid); - /** - * Retrieves a user by name. - * - * @param name the username to search - * @return an {@link Optional} containing the cached user, if present - */ - @NotNull Optional getUserByName(@NotNull String name); + Optional getUserByName(@NotNull String name); - /** - * Updates internal name mappings for users whose username has changed. - * - * @param user the user instance with the new name - * @param oldName the previous username - */ void updateUserNameMapping(@NotNull User user, @NotNull String oldName); - /** - * Iterates over all cached users and executes the given action. - * - * @param action the callback executed for each cached user - */ void forEachUser(@NotNull Consumer action); - /** - * Returns a view of all cached users. - * - * @return an unmodifiable collection of all cached users - */ - @NotNull @Unmodifiable Collection getCache(); - /** - * Clears the entire cache. - */ void invalidateAll(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java index 1671fa0..2f85721 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserFactory; import com.github.imdmk.playtime.user.UserSaveReason; @@ -17,15 +16,11 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -import java.time.Duration; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.TimeUnit; public final class UserJoinListener implements Listener { - private static final Duration FIND_TIMEOUT = Duration.ofSeconds(2); - private static final Duration SAVE_TIMEOUT = Duration.ofSeconds(2); private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_JOIN; private final Server server; @@ -42,11 +37,11 @@ public UserJoinListener( @NotNull UserFactory userFactory, @NotNull TaskScheduler taskScheduler ) { - this.server = Validator.notNull(server, "server"); - this.logger = Validator.notNull(logger, "logger"); - this.userService = Validator.notNull(userService, "userService"); - this.userFactory = Validator.notNull(userFactory, "userFactory cannot be null"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); + this.server = server; + this.logger = logger; + this.userService = userService; + this.userFactory = userFactory; + this.taskScheduler = taskScheduler; } @EventHandler(priority = EventPriority.LOWEST) @@ -63,7 +58,6 @@ private void handlePlayerJoin(Player player) { final UUID uuid = player.getUniqueId(); userService.findByUuid(uuid) - .orTimeout(FIND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((optional, e) -> { if (e != null) { logger.error(e, "Failed to load user on join uuid=%s", uuid); @@ -88,7 +82,6 @@ private void handleNewUser(Player player) { private void handleExistingUser(Player player, User user) { final String name = player.getName(); - if (!updateNameIfChanged(user, name)) { return; } @@ -98,7 +91,6 @@ private void handleExistingUser(Player player, User user) { private void saveUser(User user, String context) { userService.save(user, SAVE_REASON) - .orTimeout(SAVE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((r, e) -> { if (e != null) { logger.error(e, "Failed to save user %s uuid=%s", context, user.getUuid()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java index b2ba6b4..053cbdb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.user.listener; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; import org.bukkit.entity.Player; @@ -12,13 +11,10 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -import java.time.Duration; import java.util.UUID; -import java.util.concurrent.TimeUnit; public final class UserQuitListener implements Listener { - private static final Duration SAVE_TIMEOUT = Duration.ofSeconds(2); private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_LEAVE; private final PluginLogger logger; @@ -26,8 +22,8 @@ public final class UserQuitListener implements Listener { @Inject public UserQuitListener(@NotNull PluginLogger logger, @NotNull UserService userService) { - this.logger = Validator.notNull(logger, "logger"); - this.userService = Validator.notNull(userService, "userService"); + this.logger = logger; + this.userService = userService; } @EventHandler(priority = EventPriority.HIGHEST) @@ -38,7 +34,6 @@ public void onPlayerQuit(PlayerQuitEvent event) { final String name = player.getName(); userService.findCachedByUuid(uuid).ifPresent(user -> userService.save(user, SAVE_REASON) - .orTimeout(SAVE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((u, e) -> { if (e != null) { logger.error(e, "Failed to save user on quit %s (%s)", name, uuid); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java index f9884ab..b19d94a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java @@ -37,7 +37,7 @@ public UserEntity(@NotNull UUID uuid, @NotNull String name, long playtimeMillis) this.playtimeMillis = playtimeMillis; } - public @NotNull UUID getUuid() { + public UUID getUuid() { return this.uuid; } @@ -45,7 +45,7 @@ public void setUuid(@NotNull UUID uuid) { this.uuid = uuid; } - public @NotNull String getName() { + public String getName() { return this.name; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java index 50384f2..c745311 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java @@ -1,19 +1,17 @@ package com.github.imdmk.playtime.user.repository; import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; import org.jetbrains.annotations.NotNull; -/** - * Maps between the persistent {@link UserEntity} and the in-memory {@link User}. - */ -public final class UserEntityMapper implements EntityMapper { +@Service +public final class UserEntityMapper + implements EntityMapper { @Override - public @NotNull UserEntity toEntity(@NotNull User user) { - Validator.notNull(user, "user"); + public UserEntity toEntity(@NotNull User user) { return new UserEntity( user.getUuid(), user.getName(), @@ -22,8 +20,7 @@ public final class UserEntityMapper implements EntityMapper { } @Override - public @NotNull User toDomain(@NotNull UserEntity entity) { - Validator.notNull(entity, "entity"); + public User toDomain(@NotNull UserEntity entity) { return new User( entity.getUuid(), entity.getName(), diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java index 10758ea..622d112 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java @@ -2,34 +2,16 @@ import com.github.imdmk.playtime.database.repository.ormlite.EntityMeta; -/** - * Database metadata for the {@code advanced_playtime_users} table. - * - *

This interface defines the table name and all column identifiers used by - * {@link UserEntity} and the corresponding repository implementation.

- * - *

Centralizing these names ensures consistency across entity mappings, - * DAO queries, migrations, and schema creation routines.

- */ interface UserEntityMeta extends EntityMeta { - /** Name of the table storing persistent user records. */ String TABLE = "advanced_playtime_users"; - /** - * Column name definitions for {@link UserEntity}. - * - *

All constants represent physical column names in the database schema.

- */ interface Col { - /** Unique player identifier (primary key, NOT NULL). */ String UUID = "uuid"; - /** Last known player name (NOT NULL, indexed). */ String NAME = "name"; - /** Total accumulated playtime in milliseconds (NOT NULL). */ String PLAYTIME_MILLIS = "playtimeMillis"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java index d4a248e..f106d85 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.database.repository.Repository; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserDeleteResult; import org.jetbrains.annotations.NotNull; @@ -10,71 +9,19 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -/** - * Asynchronous repository for managing and querying {@link User} records. - *

All methods return non-null {@link CompletableFuture}s and complete exceptionally on failure.

- *

Name matching policy should be documented by the implementation (recommended: case-insensitive, normalized).

- */ -public interface UserRepository extends Repository { +public interface UserRepository { - /** - * Finds a user by UUID. - * - * @param uuid non-null UUID - * @return non-null future with an Optional user (empty if not found) - */ - @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture> findByUuid(@NotNull UUID uuid); - /** - * Finds a user by exact name (implementation should document case handling). - * - * @param name non-null username - * @return non-null future with an Optional user (empty if not found) - */ - @NotNull CompletableFuture> findByName(@NotNull String name); + CompletableFuture> findByName(@NotNull String name); - /** - * Retrieves all users from the data source. - *

- * The returned list order is implementation-defined unless otherwise documented. - * Implementations are expected to return an immutable, non-null list, and may apply - * internal caching or batching strategies for performance. - *

- * - * @return non-null future with all persisted users (possibly empty) - */ - @NotNull CompletableFuture> findAll(); + CompletableFuture> findAll(); - /** - * Returns top users by spent time, sorted descending. - * Ties are resolved deterministically (e.g., by UUID ascending). - * - * @param limit number of users to return; must be > 0 - * @return non-null future with an immutable list (possibly empty) - */ - @NotNull CompletableFuture> findTopByPlayTime(long limit); + CompletableFuture> findTopByPlayTime(long limit); - /** - * Creates or updates the user (upsert). - * - * @param user non-null user - * @return non-null future with the persisted user - */ - @NotNull CompletableFuture save(@NotNull User user); + CompletableFuture deleteByUuid(@NotNull UUID uuid); - /** - * Deletes a user by UUID. - * - * @param uuid non-null UUID - * @return non-null future that completes with {@code true} if a row was deleted, otherwise {@code false} - */ - @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid); + CompletableFuture deleteByName(@NotNull String name); - /** - * Deletes a user by name. - * - * @param name non-null username - * @return non-null future that completes with {@code true} if a row was deleted, otherwise {@code false} - */ - @NotNull CompletableFuture deleteByName(@NotNull String name); + CompletableFuture save(@NotNull User user); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java index 9c2036f..68f7019 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java @@ -1,9 +1,9 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.database.repository.RepositoryContext; -import com.github.imdmk.playtime.database.repository.ormlite.BaseDaoRepository; +import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; +import com.github.imdmk.playtime.injector.annotations.Repository; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserDeleteResult; import com.github.imdmk.playtime.user.UserDeleteStatus; @@ -16,22 +16,21 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +@Repository public final class UserRepositoryOrmLite - extends BaseDaoRepository + extends OrmLiteRepository implements UserRepository { - private final PluginLogger logger; private final UserEntityMapper mapper; @Inject public UserRepositoryOrmLite( @NotNull PluginLogger logger, - @NotNull RepositoryContext context, + @NotNull TaskScheduler taskScheduler, @NotNull UserEntityMapper mapper ) { - super(logger, context); - this.logger = Validator.notNull(logger, "logger"); - this.mapper = Validator.notNull(mapper, "mapper"); + super(logger, taskScheduler); + this.mapper = mapper; } @Override @@ -45,9 +44,8 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - return executeAsync(() -> { + public CompletableFuture> findByUuid(@NotNull UUID uuid) { + return execute(() -> { try { return Optional.ofNullable(dao.queryForId(uuid)) .map(mapper::toDomain); @@ -59,11 +57,10 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findByName(@NotNull String name) { - Validator.notNull(name, "name"); - return executeAsync(() -> { + public CompletableFuture> findByName(@NotNull String name) { + return execute(() -> { try { - UserEntity entity = dao.queryBuilder() + final UserEntity entity = dao.queryBuilder() .where().eq(UserEntityMeta.Col.NAME, name) .queryForFirst(); return Optional.ofNullable(entity).map(mapper::toDomain); @@ -75,8 +72,8 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findAll() { - return executeAsync(() -> { + public CompletableFuture> findAll() { + return execute(() -> { try { return mapper.toDomainList(dao.queryForAll()); } catch (SQLException e) { @@ -87,12 +84,12 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findTopByPlayTime(long limit) { + public CompletableFuture> findTopByPlayTime(long limit) { if (limit <= 0) { return CompletableFuture.completedFuture(List.of()); } - return executeAsync(() -> { + return execute(() -> { try { return mapper.toDomainList( dao.queryBuilder() @@ -109,9 +106,8 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture save(@NotNull User user) { - Validator.notNull(user, "user"); - return executeAsync(() -> { + public CompletableFuture save(@NotNull User user) { + return execute(() -> { try { dao.createOrUpdate(mapper.toEntity(user)); return user; @@ -123,18 +119,16 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - return executeAsync(() -> { + public CompletableFuture deleteByUuid(@NotNull UUID uuid) { + return execute(() -> { try { - UserEntity userEntity = dao.queryForId(uuid); + final UserEntity userEntity = dao.queryForId(uuid); if (userEntity == null) { return new UserDeleteResult(null, UserDeleteStatus.NOT_FOUND); } - User user = mapper.toDomain(userEntity); - - int rows = dao.deleteById(uuid); + final User user = mapper.toDomain(userEntity); + final int rows = dao.deleteById(uuid); return rows > 0 ? new UserDeleteResult(user, UserDeleteStatus.DELETED) : new UserDeleteResult(user, UserDeleteStatus.FAILED); @@ -146,20 +140,18 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture deleteByName(@NotNull String name) { - Validator.notNull(name, "name"); - return executeAsync(() -> { + public CompletableFuture deleteByName(@NotNull String name) { + return execute(() -> { try { - UserEntity userEntity = dao.queryBuilder() + final UserEntity userEntity = dao.queryBuilder() .where().eq(UserEntityMeta.Col.NAME, name) .queryForFirst(); if (userEntity == null) { return new UserDeleteResult(null, UserDeleteStatus.NOT_FOUND); } - User user = mapper.toDomain(userEntity); - - int rows = dao.delete(userEntity); + final User user = mapper.toDomain(userEntity); + final int rows = dao.delete(userEntity); return rows > 0 ? new UserDeleteResult(user, UserDeleteStatus.DELETED) : new UserDeleteResult(user, UserDeleteStatus.FAILED); @@ -169,4 +161,6 @@ protected List> entitySubClasses() { } }); } + + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java index 5f1ce6e..8bdaaa4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.user.top; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; @@ -9,40 +8,17 @@ import java.time.Instant; import java.util.List; -/** - * Immutable representation of a cached leaderboard snapshot. - *

- * Holds the ordered user list, the limit used during loading, and the timestamp - * when the data was retrieved. Provides logic for determining whether the snapshot - * is still valid for a given request. - */ record CachedLeaderboard( - @NotNull @Unmodifiable List users, + @NotNull List users, int limit, @NotNull Instant loadedAt ) { - /** - * Constructs a new leaderboard snapshot. - * A defensive copy of the user list is created to ensure immutability. - */ CachedLeaderboard { - Validator.notNull(users, "users"); - Validator.notNull(loadedAt, "loadedAt"); users = List.copyOf(users); } - /** - * Determines whether this leaderboard is valid for the requested limit and expiration policy. - * - * @param requestedLimit limit requested by the caller - * @param expireAfter duration after which the snapshot becomes stale - * @param now current time reference - * @return {@code true} if the leaderboard is fresh and large enough, otherwise {@code false} - */ boolean isUsable(int requestedLimit, @NotNull Duration expireAfter, @NotNull Instant now) { - Validator.notNull(now, "now"); - if (this.limit < requestedLimit) { return false; } @@ -51,16 +27,10 @@ boolean isUsable(int requestedLimit, @NotNull Duration expireAfter, @NotNull Ins return true; } - Instant expiresAt = this.loadedAt.plus(expireAfter); + final Instant expiresAt = this.loadedAt.plus(expireAfter); return expiresAt.isAfter(now); } - /** - * Returns the ordered user list. - * This list is unmodifiable and safe to expose. - * - * @return immutable list of users - */ @Override @Unmodifiable public @NotNull List users() { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java index 1f1d5df..33d5102 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.user.top; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.repository.UserRepository; import org.jetbrains.annotations.NotNull; @@ -26,9 +25,9 @@ public MemoryTopUsersCache( @NotNull TopUsersCacheConfig config, @NotNull UserRepository userRepository ) { - this.logger = Validator.notNull(logger, "logger"); - this.config = Validator.notNull(config, "config"); - this.userRepository = Validator.notNull(userRepository, "userRepository"); + this.logger = logger; + this.config = config; + this.userRepository = userRepository; } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java index e840a2c..6270415 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java @@ -5,35 +5,11 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -/** - * Cache abstraction for retrieving the top users by spent time. - *

- * Implementations are responsible for storing and serving leaderboard data, - * including cache invalidation and optional limit-based slicing. - */ public interface TopUsersCache { - /** - * Returns the cached or freshly loaded leaderboard using the default limit - * defined in the cache configuration. - * - * @return future containing an ordered list of top users - */ CompletableFuture> getTopByPlayTime(); - /** - * Returns the cached or freshly loaded leaderboard limited to the given size. - * Implementations may slice an existing cached leaderboard or trigger a reload - * if the cache is stale or insufficient for the requested limit. - * - * @param limit maximum number of users to return - * @return future containing an ordered list of top users - */ CompletableFuture> getTopByPlayTime(int limit); - /** - * Invalidates all cached leaderboard data. - * Next invocation of {@link #getTopByPlayTime()} will trigger a reload. - */ void invalidateAll(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java index 2762395..c034e01 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java @@ -1,12 +1,14 @@ package com.github.imdmk.playtime.user.top; import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; import java.time.Duration; +@ConfigFile public final class TopUsersCacheConfig extends ConfigSection { @Comment({ @@ -53,12 +55,12 @@ public final class TopUsersCacheConfig extends ConfigSection { public Duration topUsersQueryTimeout = Duration.ofSeconds(3); @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> {}; } @Override - public @NotNull String getFileName() { - return "leaderboardConfig.yml"; + public @NotNull String fileName() { + return "leaderboard.yml"; } } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java similarity index 68% rename from playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java index ea3b291..d12ac1d 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java @@ -1,19 +1,19 @@ package com.github.imdmk.playtime.database.driver; import com.github.imdmk.playtime.database.DatabaseMode; -import com.github.imdmk.playtime.database.driver.configurer.DriverConfigurerFactory; +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurerFactory; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNullPointerException; -class DriverConfigurerFactoryTest { +class DataSourceConfigurerFactoryTest { @Test void shouldReturnConfigurerForEachSupportedMode() { for (DatabaseMode mode : DatabaseMode.values()) { - assertThatCode(() -> DriverConfigurerFactory.getFor(mode)) + assertThatCode(() -> DataSourceConfigurerFactory.getFor(mode)) .doesNotThrowAnyException(); } } @@ -21,7 +21,7 @@ void shouldReturnConfigurerForEachSupportedMode() { @Test void shouldRejectNullMode() { assertThatNullPointerException() - .isThrownBy(() -> DriverConfigurerFactory.getFor(null)); + .isThrownBy(() -> DataSourceConfigurerFactory.getFor(null)); } @Test @@ -29,7 +29,7 @@ void shouldThrowForUnsupportedMode() { // Just to be sure — if enum expands in future // create invalid fake enum value assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> DriverConfigurerFactory.getFor(DatabaseMode.valueOf("NON_EXISTENT"))); // if ever added + .isThrownBy(() -> DataSourceConfigurerFactory.getFor(DatabaseMode.valueOf("NON_EXISTENT"))); // if ever added } } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java index b36e25c..9e387bb 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java @@ -17,7 +17,7 @@ private static class FakeMapper implements EntityMapper { } @Override - public @NotNull Integer toDomain(String entity) { + public @NotNull Integer toDomain(@NotNull String entity) { return Integer.parseInt(entity.substring(1)); } } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java similarity index 98% rename from playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java index a58ddf0..134d3c1 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java @@ -21,9 +21,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -class BaseDaoRepositoryTest { +class OrmLiteRepositoryTest { - public static class TestDaoRepository extends BaseDaoRepository { + public static class TestDaoRepository extends OrmLiteRepository { public TestDaoRepository(PluginLogger logger, RepositoryContext context) { super(logger, context); From 9f10e933ea43439102ffafcb96d4da8e67d61269 Mon Sep 17 00:00:00 2001 From: imDMK Date: Thu, 1 Jan 2026 16:20:20 +0100 Subject: [PATCH 03/17] Continue work to writing. --- .../com/github/imdmk/playtime/user/User.java | 72 ------------------- .../imdmk/playtime/UserDeleteEvent.java | 35 +-------- .../github/imdmk/playtime/UserSaveEvent.java | 40 ----------- .../github/imdmk/playtime/PlayTimePlugin.java | 11 ++- .../playtime/config/ConfigConfigurer.java | 3 +- .../imdmk/playtime/config/ConfigFactory.java | 2 +- .../playtime/config/ConfigLifecycle.java | 6 +- .../imdmk/playtime/config/ConfigService.java | 16 ++--- .../playtime/database/DatabaseBootstrap.java | 3 + .../database/repository/Repository.java | 14 ---- .../repository/RepositoryBootstrap.java | 11 +++ .../repository/ormlite/OrmLiteRepository.java | 41 +++++++---- .../playtime/injector/ComponentManager.java | 3 +- .../{Task.java => lite/LiteArgument.java} | 12 ++-- .../annotations/lite/LiteContextual.java | 14 ++++ .../annotations/lite/LiteHandler.java | 14 ++++ .../processor/ComponentProcessors.java | 71 ++++++++++++++++++ .../injector/subscriber/LocalPublisher.java | 5 -- .../playtime/platform/gui/GuiRegistry.java | 9 +-- .../playtime/platform/gui/view/GuiOpener.java | 2 + .../playtime/shared/validate/Validator.java | 24 ------- .../time/DurationFormatStyle.java | 2 +- .../{shared => }/time/DurationSplitter.java | 2 +- .../{shared => }/time/DurationUnit.java | 12 +--- .../playtime/{shared => }/time/Durations.java | 2 +- .../imdmk/playtime/user/UserArgument.java | 6 -- .../imdmk/playtime/user/UserServiceImpl.java | 3 + .../UserJoinController.java} | 8 ++- .../UserQuitController.java} | 8 ++- .../playtime/user/repository/UserEntity.java | 10 --- .../user/repository/UserEntityMapper.java | 3 +- .../user/repository/UserRepository.java | 3 - .../repository/UserRepositoryOrmLite.java | 4 +- .../repository/RepositoryManagerTest.java | 14 ++-- .../time/DurationFormatStyleTest.java | 0 .../time/DurationSplitterTest.java | 0 .../{shared => }/time/DurationUnitTest.java | 2 - .../{shared => }/time/DurationsTest.java | 0 38 files changed, 210 insertions(+), 277 deletions(-) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java rename playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/{Task.java => lite/LiteArgument.java} (54%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java rename playtime-core/src/main/java/com/github/imdmk/playtime/{shared => }/time/DurationFormatStyle.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{shared => }/time/DurationSplitter.java (93%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{shared => }/time/DurationUnit.java (81%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{shared => }/time/Durations.java (96%) rename playtime-core/src/main/java/com/github/imdmk/playtime/user/{listener/UserJoinListener.java => controller/UserJoinController.java} (94%) rename playtime-core/src/main/java/com/github/imdmk/playtime/user/{listener/UserQuitListener.java => controller/UserQuitController.java} (82%) rename playtime-core/src/test/java/com/github/imdmk/playtime/{shared => }/time/DurationFormatStyleTest.java (100%) rename playtime-core/src/test/java/com/github/imdmk/playtime/{shared => }/time/DurationSplitterTest.java (100%) rename playtime-core/src/test/java/com/github/imdmk/playtime/{shared => }/time/DurationUnitTest.java (94%) rename playtime-core/src/test/java/com/github/imdmk/playtime/{shared => }/time/DurationsTest.java (100%) diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java index fedad9a..bf83867 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java @@ -2,116 +2,50 @@ import org.jetbrains.annotations.NotNull; -import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -/** - * Represents an immutable-identity player aggregate containing all tracked - * playtime-related metadata. - * - *

This class is the main domain model for player statistics and provides: - *

    - *
  • stable identity via {@link UUID},
  • - *
  • thread-safe counters using {@link AtomicLong} and {@link AtomicInteger},
  • - *
  • mutable fields for name, join tracking, and playtime accumulation.
  • - *
- * - * All numerical fields are stored in atomic structures to allow safe concurrent - * updates from asynchronous tasks (e.g., an async database writes). The name field - * is {@code volatile}, ensuring safe publication across threads. - *

- * Two {@code User} instances are considered equal if and only if their UUIDs match. - */ public final class User { - /** Permanently immutable player UUID. */ private final UUID uuid; - - /** Last known player name. Volatile for safe cross-thread publication. */ private volatile String name; - /** Total accumulated playtime in milliseconds. */ private final AtomicLong playtimeMillis; - /** - * Creates a fully initialized {@code User} instance. - * - * @param uuid unique player identifier (never null) - * @param name last known player name (never null or blank) - * @param playtime initial playtime value (never null) - */ public User(@NotNull UUID uuid, @NotNull String name, @NotNull UserTime playtime) { this.uuid = uuid; this.name = name; this.playtimeMillis = new AtomicLong(playtime.millis()); } - /** - * Convenience constructor for a new player with zero playtime. - * - * @param uuid unique player identifier - * @param name last known player name - */ public User(@NotNull UUID uuid, @NotNull String name) { this(uuid, name, UserTime.ZERO); } - /** - * Returns the unique identifier of this user. - * - * @return player's UUID (never null) - */ @NotNull public UUID getUuid() { return this.uuid; } - /** - * Returns the last known player name. - * - * @return name as a non-null String - */ @NotNull public String getName() { return this.name; } - /** - * Updates the stored player name. - * - * @param name the new name (non-null, non-blank) - * @throws NullPointerException if name is null - * @throws IllegalArgumentException if name is blank - */ public void setName(@NotNull String name) { this.name = name; } - /** - * Returns the total accumulated playtime as an immutable {@link UserTime} object. - * - * @return playtime value (never null) - */ @NotNull public UserTime getPlaytime() { return UserTime.ofMillis(playtimeMillis.get()); } - /** - * Replaces the stored playtime with a new value. - * - * @param playtime the new playtime (must not be null) - * @throws NullPointerException if playtime is null - */ public void setPlaytime(@NotNull UserTime playtime) { playtimeMillis.set(playtime.millis()); } - /** - * Users are equal if and only if their UUIDs match. - */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -119,17 +53,11 @@ public boolean equals(Object o) { return uuid.equals(other.uuid); } - /** - * Hash code is based solely on UUID. - */ @Override public int hashCode() { return uuid.hashCode(); } - /** - * Returns a concise diagnostic string representation. - */ @Override public String toString() { return "User{" + diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java index 1cc880c..af31de4 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java @@ -7,14 +7,6 @@ import java.util.Objects; -/** - * Fired after a user deletion attempt completes. - *

- * Carries a {@link UserDeleteResult} with the deleted user snapshot (if any) - * and the {@link com.github.imdmk.playtime.user.UserDeleteStatus} outcome. - *

- *

Threading: Dispatched synchronously on the main server thread.

- */ public final class UserDeleteEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); @@ -22,45 +14,20 @@ public final class UserDeleteEvent extends Event { private final UserDeleteResult result; - /** - * Creates a new {@code UserDeleteEvent}. - * - * @param result non-null deletion result - */ public UserDeleteEvent(@NotNull UserDeleteResult result) { super(ASYNC); - this.result = Objects.requireNonNull(result, "result"); + this.result = result; } - /** - * Returns the result of the deletion operation, including the status and an - * optional snapshot of the deleted user (if available). - * - * @return non-null {@link UserDeleteResult} representing the outcome of the deletion - */ public @NotNull UserDeleteResult getResult() { return this.result; } - /** - * Returns the handler list used internally by Bukkit to register and manage - * listeners for this event. - * - * @return non-null static {@link HandlerList} for this event type - */ @Override public @NotNull HandlerList getHandlers() { return HANDLERS; } - /** - * Returns the static handler list for this event type. - *

- * This method is required by the Bukkit event framework and allows Bukkit - * to correctly map event handlers to this event class. - * - * @return non-null static {@link HandlerList} - */ public static @NotNull HandlerList getHandlerList() { return HANDLERS; } diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java index 78402b6..0c46208 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java @@ -6,17 +6,6 @@ import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; -import java.util.Objects; - -/** - * Called whenever a {@link User} instance is saved by the plugin. - * - *

This event is fired after user data has been persisted. - * It can be canceled to prevent later operations that depend on the save, - * if applicable within the plugin's logic.

- * - *

Thread safety: This event is always fired synchronously on the main server thread.

- */ public final class UserSaveEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); @@ -25,54 +14,25 @@ public final class UserSaveEvent extends Event { private final User user; private final UserSaveReason reason; - /** - * Constructs a new {@code UserSaveEvent}. - * - * @param user the user that was saved (non-null) - * @param reason the reason of user save - */ public UserSaveEvent(@NotNull User user, @NotNull UserSaveReason reason) { super(ASYNC); this.user = user; this.reason = reason; } - /** - * Returns the {@link User} associated with this event. - * - * @return non-null user involved in this event - */ public @NotNull User getUser() { return this.user; } - /** - * Returns the reason why this save operation was triggered. - * - * @return non-null {@link UserSaveReason} describing the save cause - */ public @NotNull UserSaveReason getReason() { return this.reason; } - /** - * Returns the handler list used internally by Bukkit to manage event listeners. - * - * @return non-null static {@link HandlerList} for this event type - */ @Override public @NotNull HandlerList getHandlers() { return HANDLERS; } - /** - * Returns the static handler list for this event type. - *

- * This method is required by the Bukkit event system and is used - * to register and manage listeners for this event. - * - * @return non-null static {@link HandlerList} - */ public static @NotNull HandlerList getHandlerList() { return HANDLERS; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index 1b822bb..2ff80fc 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -2,6 +2,10 @@ import com.github.imdmk.playtime.injector.ComponentManager; import com.github.imdmk.playtime.injector.priority.AnnotationPriorityProvider; +import com.github.imdmk.playtime.injector.processor.ComponentProcessor; +import com.github.imdmk.playtime.injector.processor.ComponentProcessors; +import com.github.imdmk.playtime.injector.subscriber.LocalPublisher; +import com.github.imdmk.playtime.injector.subscriber.Publisher; import com.github.imdmk.playtime.platform.logger.BukkitPluginLogger; import com.github.imdmk.playtime.platform.logger.PluginLogger; import org.bukkit.Server; @@ -27,9 +31,12 @@ final class PlayTimePlugin { resources.on(PluginLogger.class).assignInstance(new BukkitPluginLogger(plugin)); }); + injector.getResources().on(Publisher.class).assignInstance(new LocalPublisher(injector)); + ComponentManager componentManager = new ComponentManager(injector, this.getClass().getPackageName()) - .setPriorityProvider(new AnnotationPriorityProvider()) - .addProcessor(); + .setPriorityProvider(new AnnotationPriorityProvider()); + + for (ComponentProcessor processor : injector.invokeMethod(ComponentProcessors.defaults())) componentManager.scanAll(); componentManager.processAll(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java index 5e28154..1ef369d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.config; -import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import eu.okaeri.configs.serdes.commons.SerdesCommons; import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; @@ -11,7 +10,7 @@ final class ConfigConfigurer { void configure( - @NotNull OkaeriConfig config, + @NotNull ConfigSection config, @NotNull File file, OkaeriSerdesPack... serdesPacks ) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java index aa40829..efaae08 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java @@ -7,7 +7,7 @@ final class ConfigFactory { - @NotNull T instantiate(@NotNull Class type) { + @NotNull T instantiate(@NotNull Class type) { try { return ConfigManager.create(type); } catch (OkaeriException e) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java index edf6e6c..7698030 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java @@ -13,12 +13,12 @@ final class ConfigLifecycle { this.logger = logger; } - void initialize(@NotNull OkaeriConfig config) { + void initialize(@NotNull ConfigSection config) { config.saveDefaults(); load(config); } - void load(@NotNull OkaeriConfig config) { + void load(@NotNull ConfigSection config) { try { config.load(true); } catch (OkaeriException e) { @@ -27,7 +27,7 @@ void load(@NotNull OkaeriConfig config) { } } - void save(@NotNull OkaeriConfig config) { + void save(@NotNull ConfigSection config) { try { config.save(); } catch (OkaeriException e) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index c89de31..c67c2ee 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -17,8 +17,8 @@ @Service(priority = Priority.LOWEST) public final class ConfigService { - private final Set configs = ConcurrentHashMap.newKeySet(); - private final Map, OkaeriConfig> byType = new ConcurrentHashMap<>(); + private final Set configs = ConcurrentHashMap.newKeySet(); + private final Map, ConfigSection> byType = new ConcurrentHashMap<>(); private final File dataFolder; @@ -35,9 +35,9 @@ public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { this.lifecycle = new ConfigLifecycle(logger); } - public @NotNull T create(@NotNull Class type, @NotNull String fileName) { + public @NotNull T create(@NotNull Class type) { final T config = factory.instantiate(type); - final File file = new File(dataFolder, fileName); + final File file = new File(dataFolder, config.fileName()); configurer.configure(config, file); lifecycle.initialize(config); @@ -47,11 +47,11 @@ public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { } @SuppressWarnings("unchecked") - public T get(@NotNull Class type) { + public T get(@NotNull Class type) { return (T) byType.get(type); } - public @NotNull T require(@NotNull Class type) { + public @NotNull T require(@NotNull Class type) { final T config = get(type); if (config == null) { throw new IllegalStateException("Config not created: " + type.getName()); @@ -70,7 +70,7 @@ public void saveAll() { @NotNull @Unmodifiable - public Set getConfigs() { + public Set getConfigs() { return Collections.unmodifiableSet(configs); } @@ -79,7 +79,7 @@ public void shutdown() { byType.clear(); } - private void register(Class type, OkaeriConfig config) { + private void register(Class type, ConfigSection config) { configs.add(config); byType.put(type, config); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java index 152954f..5b64907 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java @@ -5,6 +5,8 @@ import com.github.imdmk.playtime.database.library.DriverLibraryLoader; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.j256.ormlite.support.ConnectionSource; import org.bukkit.plugin.Plugin; @@ -58,6 +60,7 @@ public ConnectionSource getConnection() { return dataConnector.getConnectionSource(); } + @Subscribe(event = PlayTimeShutdownEvent.class) public void shutdown() { dataConnector.close(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java deleted file mode 100644 index fd4431d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/Repository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.imdmk.playtime.database.repository; - -import com.j256.ormlite.support.ConnectionSource; -import org.jetbrains.annotations.NotNull; - -import java.sql.SQLException; - -public interface Repository extends AutoCloseable { - - void start(@NotNull ConnectionSource source) throws SQLException; - - @Override - void close(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java new file mode 100644 index 0000000..1fae687 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java @@ -0,0 +1,11 @@ +package com.github.imdmk.playtime.database.repository; + +import java.sql.SQLException; + +public interface RepositoryBootstrap extends AutoCloseable { + + void start() throws SQLException; + + @Override + void close(); +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java index 443649f..a939133 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.database.repository.ormlite; -import com.github.imdmk.playtime.database.repository.Repository; +import com.github.imdmk.playtime.database.DatabaseBootstrap; +import com.github.imdmk.playtime.database.repository.RepositoryBootstrap; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.j256.ormlite.dao.Dao; @@ -10,6 +11,7 @@ import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; import java.sql.SQLException; import java.time.Duration; @@ -19,19 +21,26 @@ import java.util.function.Supplier; public abstract class OrmLiteRepository - implements Repository { + implements RepositoryBootstrap { private static final Duration EXECUTE_TIMEOUT = Duration.ofSeconds(3); protected final PluginLogger logger; protected final TaskScheduler scheduler; + private final DatabaseBootstrap databaseBootstrap; protected volatile Dao dao; - protected OrmLiteRepository(@NotNull PluginLogger logger, @NotNull TaskScheduler scheduler) { + @Inject + protected OrmLiteRepository( + @NotNull PluginLogger logger, + @NotNull TaskScheduler scheduler, + @NotNull DatabaseBootstrap databaseBootstrap + ) { this.logger = logger; this.scheduler = scheduler; - Logger.setGlobalLogLevel(Level.ERROR); // Change ORMLITE logging to errors + this.databaseBootstrap = databaseBootstrap; + configureOrmLiteLogger(); } protected abstract Class entityClass(); @@ -39,23 +48,28 @@ protected OrmLiteRepository(@NotNull PluginLogger logger, @NotNull TaskScheduler protected abstract List> entitySubClasses(); @Override - public void start(@NotNull ConnectionSource source) throws SQLException { + public void start() throws SQLException { + final ConnectionSource connection = databaseBootstrap.getConnection(); + if (connection == null) { + throw new IllegalStateException("DatabaseBootstrap not started before repository initialization"); + } + for (final Class subClass : this.entitySubClasses()) { - TableUtils.createTableIfNotExists(source, subClass); + TableUtils.createTableIfNotExists(connection, subClass); } - TableUtils.createTableIfNotExists(source, this.entityClass()); - this.dao = DaoManager.createDao(source, this.entityClass()); + TableUtils.createTableIfNotExists(connection, this.entityClass()); + dao = DaoManager.createDao(connection, this.entityClass()); } @Override public void close() { - final Dao current = this.dao; + final Dao current = dao; if (current == null) { return; } - this.dao = null; + dao = null; final ConnectionSource connection = current.getConnectionSource(); if (connection != null) { DaoManager.unregisterDao(connection, current); @@ -64,14 +78,17 @@ public void close() { protected CompletableFuture execute(@NotNull Supplier supplier) { final CompletableFuture future = new CompletableFuture<>(); - this.scheduler.runAsync(() -> { + scheduler.runAsync(() -> { try { future.complete(supplier.get()); } catch (Exception e) { future.completeExceptionally(e); } }); - return future.orTimeout(EXECUTE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); } + + private void configureOrmLiteLogger() { + Logger.setGlobalLogLevel(Level.ERROR); // only errors + } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java index 9f27cec..34d3ecf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -15,7 +15,7 @@ public final class ComponentManager { - private static final PriorityProvider DEFAULT_PRIORITY = (priority -> Priority.NORMAL); + private static final PriorityProvider DEFAULT_PRIORITY = priority -> Priority.NORMAL; private final Injector injector; @@ -26,7 +26,6 @@ public final class ComponentManager { public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) { this.injector = injector; - this.container = new ComponentQueue(DEFAULT_PRIORITY); this.scanner = new ComponentScanner(basePackage); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java similarity index 54% rename from playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java index c8c3945..9c997af 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Task.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java @@ -1,6 +1,4 @@ -package com.github.imdmk.playtime.injector.annotations; - -import com.github.imdmk.playtime.injector.priority.Priority; +package com.github.imdmk.playtime.injector.annotations.lite; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -9,8 +7,10 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -public @interface Task { +public @interface LiteArgument { + + Class type(); - Priority priority() default Priority.HIGHEST; + String name(); -} \ No newline at end of file +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java new file mode 100644 index 0000000..90d1840 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.injector.annotations.lite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LiteContextual { + + Class value(); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java new file mode 100644 index 0000000..ccb7809 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.injector.annotations.lite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LiteHandler { + + Class value(); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java new file mode 100644 index 0000000..b762b06 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -0,0 +1,71 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.config.ConfigService; +import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; +import com.github.imdmk.playtime.injector.annotations.Controller; +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; +import com.github.imdmk.playtime.injector.annotations.Repository; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Publisher; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public final class ComponentProcessors { + + @Inject + public static List> defaults( + @NotNull Plugin plugin, + @NotNull ConfigService configService, + @NotNull Publisher publisher + ) { + List> processors = new ArrayList<>(); + + processors.add(new FunctionalComponentProcessor<>( + ConfigFile.class, + ConfigSection.class, + (config, annotation, context) -> configService.create(config.getClass()) + )); + + processors.add(new FunctionalComponentProcessor<>( + Service.class, + Object.class, + (instance, annotation, context) -> {} + )); + + processors.add(new FunctionalComponentProcessor<>( + Repository.class, + OrmLiteRepository.class, + (repository, annotation, context) -> { + try { + repository.start(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + )); + + processors.add(new FunctionalComponentProcessor<>( + Controller.class, + Listener.class, + (listener, annotation, context) -> plugin.getServer().getPluginManager().registerEvents(listener, plugin) + )); + + processors.add(new FunctionalComponentProcessor<>( + NoneAnnotation.class, + Object.class, + (instance, none, context) -> publisher.subscribe(instance) + )); + + return processors; + } +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java index cc2288b..333bff8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -1,11 +1,8 @@ package com.github.imdmk.playtime.injector.subscriber; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.annotations.Inject; import java.lang.reflect.Method; import java.util.ArrayList; @@ -13,14 +10,12 @@ import java.util.List; import java.util.Map; -@Service(priority = Priority.LOWEST) public final class LocalPublisher implements Publisher { private final Map, List> subscribers = new HashMap<>(); private final Injector injector; - @Inject public LocalPublisher(@NotNull Injector injector) { this.injector = injector; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java index e920d4a..a6a7df3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java @@ -1,5 +1,7 @@ package com.github.imdmk.playtime.platform.gui; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -9,6 +11,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +@Service(priority = Priority.LOW) public final class GuiRegistry { private final Map byId = new ConcurrentHashMap<>(); @@ -51,8 +54,7 @@ public IdentifiableGui unregister(@NotNull String id) { @Nullable public IdentifiableGui getById(@NotNull String id) { - final String key = normalize(id); - return byId.get(key); + return byId.get(normalize(id)); } @Nullable @@ -63,8 +65,7 @@ public T getByClass(@NotNull Class type) { } public boolean isRegistered(@NotNull String id) { - final String key = normalize(id); - return byId.containsKey(key); + return byId.containsKey(normalize(id)); } @Unmodifiable diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java index 319f05c..040e6f3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.platform.gui.view; +import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.platform.gui.GuiRegistry; import com.github.imdmk.playtime.platform.gui.IdentifiableGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; @@ -8,6 +9,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; +@Service public final class GuiOpener { private final GuiRegistry registry; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java deleted file mode 100644 index 0530af8..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.imdmk.playtime.shared.validate; - -import java.util.function.Consumer; - -public final class Validator { - - private Validator() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); - } - - public static T notNull(T obj, String context) { - if (obj == null) { - throw new NullPointerException(context + " cannot be null"); - } - return obj; - } - - public static void ifNotNull(T obj, Consumer consumer) { - if (obj != null) { - consumer.accept(obj); - } - } - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java index f29cf2e..e90c150 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.shared.time; +package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java similarity index 93% rename from playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java index 7d37232..b25ffde 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.shared.time; +package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java similarity index 81% rename from playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java index 5f6356e..ef6638a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java @@ -1,14 +1,9 @@ -package com.github.imdmk.playtime.shared.time; +package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; import java.time.Duration; -/** - * Supported duration units and their metadata. - *

- * This enum centralizes singular/plural names, abbreviations, and extraction logic. - */ public enum DurationUnit { DAY("day", "days", "d") { @@ -36,8 +31,7 @@ public int extract(@NotNull Duration duration) { } }; - /** Ordered for consistent output. */ - public static final DurationUnit[] ORDERED = { + protected static final DurationUnit[] ORDERED = { DAY, HOUR, MINUTE, SECOND }; @@ -59,7 +53,7 @@ public String getAbbreviation() { return abbreviation; } - public String toDisplayName(int value) { + protected String toDisplayName(int value) { final String word = (value == 1 ? singular : plural); return DISPLAY_NAME_FORMAT.formatted(value, word); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java similarity index 96% rename from playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java index 8ca9ed4..d349fcb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.shared.time; +package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java index edada38..f01bb83 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java @@ -16,12 +16,6 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; -/** - * Argument resolver for {@link User} objects. - *

- * Performs a cache-only lookup on the primary server thread to avoid blocking, - * and a full asynchronous lookup (cache → database) off the main thread. - */ final class UserArgument extends ArgumentResolver { private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(2); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java index 46a1e2e..945ef71 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java @@ -2,6 +2,8 @@ import com.github.imdmk.playtime.UserDeleteEvent; import com.github.imdmk.playtime.UserSaveEvent; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.event.BukkitEventCaller; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.user.cache.UserCache; @@ -19,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +@Service(priority = Priority.HIGH) final class UserServiceImpl implements UserService { private static final Duration TIMEOUT = Duration.ofSeconds(2L); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java similarity index 94% rename from playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java index 2f85721..bab36c9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java @@ -1,5 +1,6 @@ -package com.github.imdmk.playtime.user.listener; +package com.github.imdmk.playtime.user.controller; +import com.github.imdmk.playtime.injector.annotations.Controller; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.github.imdmk.playtime.user.User; @@ -19,7 +20,8 @@ import java.util.Optional; import java.util.UUID; -public final class UserJoinListener implements Listener { +@Controller +public final class UserJoinController implements Listener { private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_JOIN; @@ -30,7 +32,7 @@ public final class UserJoinListener implements Listener { private final TaskScheduler taskScheduler; @Inject - public UserJoinListener( + public UserJoinController( @NotNull Server server, @NotNull PluginLogger logger, @NotNull UserService userService, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java similarity index 82% rename from playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java index 053cbdb..5f5b410 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java @@ -1,5 +1,6 @@ -package com.github.imdmk.playtime.user.listener; +package com.github.imdmk.playtime.user.controller; +import com.github.imdmk.playtime.injector.annotations.Controller; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -13,7 +14,8 @@ import java.util.UUID; -public final class UserQuitListener implements Listener { +@Controller +public final class UserQuitController implements Listener { private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_LEAVE; @@ -21,7 +23,7 @@ public final class UserQuitListener implements Listener { private final UserService userService; @Inject - public UserQuitListener(@NotNull PluginLogger logger, @NotNull UserService userService) { + public UserQuitController(@NotNull PluginLogger logger, @NotNull UserService userService) { this.logger = logger; this.userService = userService; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java index b19d94a..99d8f94 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java @@ -7,28 +7,18 @@ import java.util.Objects; import java.util.UUID; -/** - * Persistent representation of a user stored in the database. - *

- * This entity is managed by ORMLite and maps directly to the "spent_time_users" table. - * It mirrors the in-memory {@code User} object used in runtime logic. - */ @DatabaseTable(tableName = UserEntityMeta.TABLE) public final class UserEntity { - /** Primary key — unique player UUID. */ @DatabaseField(id = true, canBeNull = false, columnName = UserEntityMeta.Col.UUID) private UUID uuid; - /** Last known player name. */ @DatabaseField(canBeNull = false, index = true, columnName = UserEntityMeta.Col.NAME) private String name; - /** Total spent time in milliseconds. */ @DatabaseField(canBeNull = false, columnName = UserEntityMeta.Col.PLAYTIME_MILLIS) private long playtimeMillis; - /** No-arg constructor required by ORMLite. */ public UserEntity() {} public UserEntity(@NotNull UUID uuid, @NotNull String name, long playtimeMillis) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java index c745311..fafaa28 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java @@ -2,11 +2,12 @@ import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; import org.jetbrains.annotations.NotNull; -@Service +@Service(priority = Priority.LOW) public final class UserEntityMapper implements EntityMapper { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java index f106d85..fca03c0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java @@ -12,15 +12,12 @@ public interface UserRepository { CompletableFuture> findByUuid(@NotNull UUID uuid); - CompletableFuture> findByName(@NotNull String name); - CompletableFuture> findAll(); CompletableFuture> findTopByPlayTime(long limit); CompletableFuture deleteByUuid(@NotNull UUID uuid); - CompletableFuture deleteByName(@NotNull String name); CompletableFuture save(@NotNull User user); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java index 68f7019..7baa30d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.user.repository; +import com.github.imdmk.playtime.database.DatabaseBootstrap; import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; import com.github.imdmk.playtime.injector.annotations.Repository; import com.github.imdmk.playtime.platform.logger.PluginLogger; @@ -27,9 +28,10 @@ public final class UserRepositoryOrmLite public UserRepositoryOrmLite( @NotNull PluginLogger logger, @NotNull TaskScheduler taskScheduler, + @NotNull DatabaseBootstrap databaseBootstrap, @NotNull UserEntityMapper mapper ) { - super(logger, taskScheduler); + super(logger, taskScheduler, databaseBootstrap); this.mapper = mapper; } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java index 1a17802..c821959 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java @@ -18,7 +18,7 @@ class RepositoryManagerTest { @Test void registerShouldStoreRepository() { PluginLogger logger = mock(PluginLogger.class); - Repository repo = mock(Repository.class); + RepositoryBootstrap repo = mock(RepositoryBootstrap.class); RepositoryManager manager = new RepositoryManager(logger); @@ -36,8 +36,8 @@ void registerShouldStoreRepository() { @Test void startAllShouldInvokeStartOnEachRepository() throws Exception { PluginLogger logger = mock(PluginLogger.class); - Repository repo1 = mock(Repository.class); - Repository repo2 = mock(Repository.class); + RepositoryBootstrap repo1 = mock(RepositoryBootstrap.class); + RepositoryBootstrap repo2 = mock(RepositoryBootstrap.class); ConnectionSource source = mock(ConnectionSource.class); RepositoryManager manager = new RepositoryManager(logger); @@ -52,7 +52,7 @@ void startAllShouldInvokeStartOnEachRepository() throws Exception { @Test void startAllShouldLogAndRethrowSQLException() throws Exception { PluginLogger logger = mock(PluginLogger.class); - Repository faulty = mock(Repository.class); + RepositoryBootstrap faulty = mock(RepositoryBootstrap.class); ConnectionSource source = mock(ConnectionSource.class); doThrow(new SQLException("boom")).when(faulty).start(source); @@ -71,8 +71,8 @@ void startAllShouldLogAndRethrowSQLException() throws Exception { @Test void closeShouldInvokeCloseOnEachRepository() { PluginLogger logger = mock(PluginLogger.class); - Repository repo1 = mock(Repository.class); - Repository repo2 = mock(Repository.class); + RepositoryBootstrap repo1 = mock(RepositoryBootstrap.class); + RepositoryBootstrap repo2 = mock(RepositoryBootstrap.class); RepositoryManager manager = new RepositoryManager(logger); manager.register(repo1, repo2); @@ -86,7 +86,7 @@ void closeShouldInvokeCloseOnEachRepository() { @Test void closeShouldLogWarningWhenRepositoryThrows() { PluginLogger logger = mock(PluginLogger.class); - Repository repo = mock(Repository.class); + RepositoryBootstrap repo = mock(RepositoryBootstrap.class); doThrow(new RuntimeException("err")).when(repo).close(); diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationFormatStyleTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java similarity index 100% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationFormatStyleTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationSplitterTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java similarity index 100% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationSplitterTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationUnitTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java similarity index 94% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationUnitTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java index 853bee5..4fb45ce 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationUnitTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java @@ -4,8 +4,6 @@ import java.time.Duration; -import static org.assertj.core.api.Assertions.assertThat; - class DurationUnitTest { @Test diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationsTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java similarity index 100% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationsTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java From 3adbe2bd36db22dfef22736af7bbf55b76cd04c9 Mon Sep 17 00:00:00 2001 From: imDMK Date: Mon, 5 Jan 2026 14:23:48 +0100 Subject: [PATCH 04/17] Fix a Dependency Injection and Components system. --- .../com/github/imdmk/playtime/user/User.java | 1 - .../imdmk/playtime/UserDeleteEvent.java | 2 - .../github/imdmk/playtime/PlayTimePlugin.java | 10 +-- .../imdmk/playtime/config/ConfigFactory.java | 1 - .../playtime/config/ConfigLifecycle.java | 1 - .../imdmk/playtime/config/ConfigService.java | 1 - .../repository/RepositoryBootstrap.java | 4 +- .../RepositoryInitializationException.java | 10 +++ .../repository/ormlite/OrmLiteRepository.java | 19 +++-- .../feature/playtime/command/TimeCommand.java | 2 +- .../playtime/command/TimeSetCommand.java | 4 +- .../playtime/command/TimeTopCommand.java | 2 +- .../feature/playtime/gui/PlayTimeTopGui.java | 2 +- .../placeholder/PlayTimePlaceholder.java | 2 +- .../playtime/injector/ComponentManager.java | 14 +++- .../playtime/injector/ComponentQueue.java | 21 ++--- .../processor/ComponentProcessors.java | 83 ++++++++++++++----- .../injector/subscriber/LocalPublisher.java | 3 + .../playtime/message/MessageService.java | 5 ++ 19 files changed, 124 insertions(+), 63 deletions(-) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryInitializationException.java diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java index bf83867..5987b95 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java @@ -3,7 +3,6 @@ import org.jetbrains.annotations.NotNull; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; public final class User { diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java index af31de4..58c1d28 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java @@ -5,8 +5,6 @@ import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; -import java.util.Objects; - public final class UserDeleteEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index 2ff80fc..e32362c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -2,10 +2,7 @@ import com.github.imdmk.playtime.injector.ComponentManager; import com.github.imdmk.playtime.injector.priority.AnnotationPriorityProvider; -import com.github.imdmk.playtime.injector.processor.ComponentProcessor; import com.github.imdmk.playtime.injector.processor.ComponentProcessors; -import com.github.imdmk.playtime.injector.subscriber.LocalPublisher; -import com.github.imdmk.playtime.injector.subscriber.Publisher; import com.github.imdmk.playtime.platform.logger.BukkitPluginLogger; import com.github.imdmk.playtime.platform.logger.PluginLogger; import org.bukkit.Server; @@ -31,12 +28,9 @@ final class PlayTimePlugin { resources.on(PluginLogger.class).assignInstance(new BukkitPluginLogger(plugin)); }); - injector.getResources().on(Publisher.class).assignInstance(new LocalPublisher(injector)); - ComponentManager componentManager = new ComponentManager(injector, this.getClass().getPackageName()) - .setPriorityProvider(new AnnotationPriorityProvider()); - - for (ComponentProcessor processor : injector.invokeMethod(ComponentProcessors.defaults())) + .setPriorityProvider(new AnnotationPriorityProvider()) + .addProcessors(ComponentProcessors.defaults(plugin)); componentManager.scanAll(); componentManager.processAll(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java index efaae08..553b8fb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.config; import eu.okaeri.configs.ConfigManager; -import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java index 7698030..a9f20b1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.config; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index c67c2ee..166a6a8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -3,7 +3,6 @@ import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import eu.okaeri.configs.OkaeriConfig; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.panda_lang.utilities.inject.annotations.Inject; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java index 1fae687..81924ed 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java @@ -1,10 +1,8 @@ package com.github.imdmk.playtime.database.repository; -import java.sql.SQLException; - public interface RepositoryBootstrap extends AutoCloseable { - void start() throws SQLException; + void start() throws RepositoryInitializationException; @Override void close(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryInitializationException.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryInitializationException.java new file mode 100644 index 0000000..3cca89e --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryInitializationException.java @@ -0,0 +1,10 @@ +package com.github.imdmk.playtime.database.repository; + +public final class RepositoryInitializationException extends RuntimeException { + + public RepositoryInitializationException(Class repositoryClass, Throwable cause) { + super("Failed to initialize repository: " + repositoryClass.getName(), cause); + } + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java index a939133..2e881cf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java @@ -2,6 +2,7 @@ import com.github.imdmk.playtime.database.DatabaseBootstrap; import com.github.imdmk.playtime.database.repository.RepositoryBootstrap; +import com.github.imdmk.playtime.database.repository.RepositoryInitializationException; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.j256.ormlite.dao.Dao; @@ -48,18 +49,26 @@ protected OrmLiteRepository( protected abstract List> entitySubClasses(); @Override - public void start() throws SQLException { + public void start() throws RepositoryInitializationException { final ConnectionSource connection = databaseBootstrap.getConnection(); if (connection == null) { throw new IllegalStateException("DatabaseBootstrap not started before repository initialization"); } - for (final Class subClass : this.entitySubClasses()) { - TableUtils.createTableIfNotExists(connection, subClass); + for (final Class subClass : entitySubClasses()) { + try { + TableUtils.createTableIfNotExists(connection, subClass); + } catch (SQLException e) { + throw new RepositoryInitializationException(subClass, e); + } } - TableUtils.createTableIfNotExists(connection, this.entityClass()); - dao = DaoManager.createDao(connection, this.entityClass()); + try { + TableUtils.createTableIfNotExists(connection, entityClass()); + dao = DaoManager.createDao(connection, entityClass()); + } catch (SQLException e) { + throw new RepositoryInitializationException(entityClass(), e); + } } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java index 8638cfa..8b49e50 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java @@ -2,7 +2,7 @@ import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.shared.time.Durations; +import com.github.imdmk.playtime.time.Durations; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; import dev.rollczi.litecommands.annotations.argument.Arg; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java index 0abb4e8..5831386 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java @@ -3,7 +3,7 @@ import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.time.Durations; +import com.github.imdmk.playtime.time.Durations; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -60,7 +60,7 @@ void setPlaytime(@Context CommandSender sender, @Arg @Async User target, @Arg Du ) .exceptionally(e -> { logger.error(e, "Failed to save user on playtime set command (target=%s)", target.getName()); - messageService.send(sender, n -> n.actionExecutionError); + //messageService.send(sender, n -> n.actionExecutionError); return null; }); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java index fd81b7c..0ac644a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java @@ -43,7 +43,7 @@ void playtimeTop(@Context Player viewer) { .thenAccept(topUsers -> guiOpener.open(PlayTimeTopGui.class, viewer, topUsers)) .exceptionally(e -> { logger.error(e, "Failed to open PlaytimeTopGui for viewer=%s", viewer.getName()); - this.messageService.send(viewer, n -> n.actionExecutionError); + messageService.send(viewer, n -> n.actionExecutionError); return null; }); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java index ba9cea3..5b2e9d1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java @@ -17,7 +17,7 @@ import com.github.imdmk.playtime.platform.gui.view.AbstractGui; import com.github.imdmk.playtime.platform.gui.view.ParameterizedGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.time.Durations; +import com.github.imdmk.playtime.time.Durations; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java index 42560a0..bc6ce3b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java @@ -3,7 +3,7 @@ import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.injector.annotations.Placeholder; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; -import com.github.imdmk.playtime.shared.time.Durations; +import com.github.imdmk.playtime.time.Durations; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java index 34d3ecf..6a0341f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -10,6 +10,7 @@ import org.panda_lang.utilities.inject.Injector; import java.lang.annotation.Annotation; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -44,6 +45,11 @@ public ComponentManager addProcessor(@NotNull Class> processors) { + processors.forEach(this::addProcessor); + return this; + } + public ComponentManager onProcess( @NotNull Class annotation, @NotNull ComponentFunctional consumer @@ -72,9 +78,11 @@ public void scanAll() { } public void processAll() { - for (final ComponentProcessor processor : processors.values()) { - container.drain(processor.annotation()) - .forEach(component -> process(processor, component)); + for (final Priority priority : Priority.values()) { + for (final ComponentProcessor processor : processors.values()) { + container.drain(priority, processor.annotation()) + .forEach(component -> process(processor, component)); + } } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java index 7706750..ca09c7a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java @@ -47,19 +47,20 @@ void add(@NotNull Component component) { } } - List> drain(@NotNull Class annotation) { + List> drain( + @NotNull Priority priority, + @NotNull Class annotation + ) { final List> result = new ArrayList<>(); synchronized (this.lock) { - for (final Priority priority : Priority.values()) { // Iteration order is defined by Priority enum declaration order - final Deque> queue = this.componentsByPriority.get(priority).get(annotation); - if (queue == null) { - continue; - } - - while (!queue.isEmpty()) { - result.add(queue.pollFirst()); - } + final Deque> queue = this.componentsByPriority.get(priority).get(annotation); + if (queue == null) { + return result; + } + + while (!queue.isEmpty()) { + result.add(queue.pollFirst()); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java index b762b06..5259f2c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -14,42 +14,36 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public final class ComponentProcessors { - @Inject - public static List> defaults( - @NotNull Plugin plugin, - @NotNull ConfigService configService, - @NotNull Publisher publisher - ) { - List> processors = new ArrayList<>(); + /** + * Initializes all default component processors. + *

+ * This method is called once during plugin startup. + * Processors may perform eager initialization and side effects. + */ + public static List> defaults(@NotNull Plugin plugin) { + final List> processors = new ArrayList<>(); processors.add(new FunctionalComponentProcessor<>( - ConfigFile.class, - ConfigSection.class, - (config, annotation, context) -> configService.create(config.getClass()) + Service.class, + Object.class, + (service, annotation, context) -> context.injector().getResources().on(service.getClass()).assignInstance(service) )); processors.add(new FunctionalComponentProcessor<>( - Service.class, - Object.class, - (instance, annotation, context) -> {} + ConfigFile.class, + ConfigSection.class, + (config, annotation, context) -> context.injector().newInstance(ConfigFileProcessor.class).handle(config, annotation, context) )); processors.add(new FunctionalComponentProcessor<>( Repository.class, OrmLiteRepository.class, - (repository, annotation, context) -> { - try { - repository.start(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } + (repository, annotation, context) -> repository.start() )); processors.add(new FunctionalComponentProcessor<>( @@ -61,11 +55,56 @@ public static List> defaults( processors.add(new FunctionalComponentProcessor<>( NoneAnnotation.class, Object.class, - (instance, none, context) -> publisher.subscribe(instance) + (instance, none, context) -> context.injector().newInstance(NoneAnnotationProcessor.class).handle(instance, none, context) )); return processors; } + + private static class ConfigFileProcessor extends AbstractComponentProcessor { + + private final ConfigService configService; + + @Inject + private ConfigFileProcessor(@NotNull ConfigService configService) { + this.configService = configService; + } + + @Override + protected void handle(@NotNull Object instance, @NotNull ConfigFile annotation, @NotNull ComponentProcessorContext context) { + if (!(instance instanceof ConfigSection configSection)) { + throw new IllegalArgumentException("Invalid config section: " + instance.getClass().getName()); + } + + configService.create(configSection.getClass()); + context.injector().getResources().on(configSection.getClass()).assignInstance(configSection); + } + + @Override + public @NotNull Class annotation() { + return ConfigFile.class; + } + } + + private static class NoneAnnotationProcessor extends AbstractComponentProcessor { + + private final Publisher publisher; + + @Inject + private NoneAnnotationProcessor(@NotNull Publisher publisher) { + this.publisher = publisher; + } + + @Override + protected void handle(@NotNull Object instance, @NotNull NoneAnnotation annotation, @NotNull ComponentProcessorContext context) { + publisher.subscribe(instance); + } + + @Override + public @NotNull Class annotation() { + return NoneAnnotation.class; + } + } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java index 333bff8..ef9db5c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -1,5 +1,7 @@ package com.github.imdmk.playtime.injector.subscriber; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Injector; @@ -10,6 +12,7 @@ import java.util.List; import java.util.Map; +@Service(priority = Priority.LOWEST) public final class LocalPublisher implements Publisher { private final Map, List> subscribers = new HashMap<>(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java index 53d0abf..5001dbd 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java @@ -2,6 +2,7 @@ import com.eternalcode.multification.adventure.AudienceConverter; import com.eternalcode.multification.bukkit.BukkitMultification; +import com.eternalcode.multification.notice.provider.NoticeProvider; import com.eternalcode.multification.translation.TranslationProvider; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.priority.Priority; @@ -50,6 +51,10 @@ public MessageService(@NotNull Plugin plugin, @NotNull MessageConfig messageConf }; } + public void send(CommandSender sender, NoticeProvider notice) { + create().viewer(sender).notice(notice).send(); + } + @Subscribe(event = PlayTimeShutdownEvent.class) public void shutdown() { audienceProvider.close(); From 729dbd3575e0d91c70e4d5595a1dd6aeae722c89 Mon Sep 17 00:00:00 2001 From: imDMK Date: Mon, 5 Jan 2026 18:14:19 +0100 Subject: [PATCH 05/17] Enable a gradle configuration cache. --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a0b9d8b..c1aa675 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -10,6 +10,6 @@ org.gradle.configureondemand=true org.gradle.caching=true org.gradle.vfs.watch=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true systemProp.file.encoding=UTF-8 From 27bc325d3c5e2b764d356b7bf4146a8b9dbb7482 Mon Sep 17 00:00:00 2001 From: imDMK Date: Tue, 6 Jan 2026 20:29:03 +0100 Subject: [PATCH 06/17] Continue work. --- gradle/wrapper/gradle-wrapper.properties | 2 +- playtime-core/build.gradle.kts | 2 +- .../github/imdmk/playtime/PlayTimePlugin.java | 38 +++++++-- .../imdmk/playtime/config/ConfigService.java | 5 +- .../imdmk/playtime/config/YamlFactory.java | 1 - .../playtime/database/DatabaseBootstrap.java | 4 +- .../feature/playtime/PlayTimeUserFactory.java | 40 ++------- .../imdmk/playtime/injector/Component.java | 35 +++++++- .../playtime/injector/ComponentManager.java | 83 ++++++++++--------- .../playtime/injector/ComponentScanner.java | 37 +++++---- .../injector/annotations/Database.java | 16 ++++ .../injector/annotations/NoneAnnotation.java | 14 ---- .../postprocessor/ComponentPostProcessor.java | 13 +++ .../priority/AnnotationPriorityProvider.java | 5 -- .../processor/AbstractComponentProcessor.java | 14 ---- .../processor/ComponentProcessor.java | 9 +- .../processor/ComponentProcessors.java | 63 +++++++------- .../FunctionalComponentProcessor.java | 18 ++-- .../injector/subscriber/LocalPublisher.java | 5 +- .../playtime/message/MessageService.java | 2 +- .../platform/gui/config/GuiConfig.java | 2 +- .../platform/metrics/BMetricsService.java | 27 ++++++ .../imdmk/playtime/user/UserServiceImpl.java | 2 +- .../user/cache/CaffeineUserCache.java | 20 ++--- .../repository/UserRepositoryOrmLite.java | 3 +- .../user/top/MemoryTopUsersCache.java | 3 + playtime-plugin/build.gradle.kts | 54 ++++++------ 27 files changed, 283 insertions(+), 234 deletions(-) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/postprocessor/ComponentPostProcessor.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1aa675..a0b9d8b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -10,6 +10,6 @@ org.gradle.configureondemand=true org.gradle.caching=true org.gradle.vfs.watch=true -org.gradle.configuration-cache=true +org.gradle.configuration-cache=false systemProp.file.encoding=UTF-8 diff --git a/playtime-core/build.gradle.kts b/playtime-core/build.gradle.kts index 2fdc0bb..4227ce0 100644 --- a/playtime-core/build.gradle.kts +++ b/playtime-core/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT") // Reflections - implementation("org.reflections:reflections:0.10.2") + implementation("io.github.classgraph:classgraph:4.8.184") // Multification implementation("com.eternalcode:multification-bukkit:1.2.3") diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index e32362c..e2ca11d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -3,40 +3,62 @@ import com.github.imdmk.playtime.injector.ComponentManager; import com.github.imdmk.playtime.injector.priority.AnnotationPriorityProvider; import com.github.imdmk.playtime.injector.processor.ComponentProcessors; +import com.github.imdmk.playtime.injector.subscriber.LocalPublisher; +import com.github.imdmk.playtime.injector.subscriber.Publisher; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeInitializeEvent; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.logger.BukkitPluginLogger; import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.google.common.base.Stopwatch; import org.bukkit.Server; import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.DependencyInjection; import org.panda_lang.utilities.inject.Injector; import java.io.File; +import java.util.concurrent.TimeUnit; final class PlayTimePlugin { - //private static final String PREFIX = "AdvancedPlayTime"; - //private static final int PLUGIN_METRICS_ID = 19362; - - private final Injector injector; + private final Publisher publisher; PlayTimePlugin(@NotNull Plugin plugin) { - injector = DependencyInjection.createInjector(resources -> { + final Stopwatch stopwatch = Stopwatch.createStarted(); + final PluginLogger logger = new BukkitPluginLogger(plugin); + + final Injector injector = DependencyInjection.createInjector(resources -> { resources.on(Plugin.class).assignInstance(plugin); resources.on(Server.class).assignInstance(plugin.getServer()); resources.on(File.class).assignInstance(plugin.getDataFolder()); - resources.on(PluginLogger.class).assignInstance(new BukkitPluginLogger(plugin)); + resources.on(PluginLogger.class).assignInstance(logger); + resources.on(BukkitScheduler.class).assignInstance(plugin.getServer().getScheduler()); }); - ComponentManager componentManager = new ComponentManager(injector, this.getClass().getPackageName()) + injector.getResources().on(Injector.class).assignInstance(() -> injector); + + this.publisher = new LocalPublisher(injector); + + final ComponentManager componentManager = new ComponentManager(injector, plugin.getClass().getPackageName()) .setPriorityProvider(new AnnotationPriorityProvider()) - .addProcessors(ComponentProcessors.defaults(plugin)); + .addProcessors(ComponentProcessors.defaults(plugin)) + .addPostProcessor(((instance, context) -> this.publisher.subscribe(instance))); componentManager.scanAll(); componentManager.processAll(); + this.publisher.publish(new PlayTimeInitializeEvent()); + + //final PlayTimeApi api = injector.newInstance(PlayTimeApiAdapter.class); + //PlayTimeApiProvider.register(api); + + final long elapsedMillis = stopwatch.elapsed(TimeUnit.MILLISECONDS); + logger.info("Successfully loaded plugin in " + elapsedMillis + "ms!"); } void disable() { + this.publisher.publish(new PlayTimeShutdownEvent()); + PlayTimeApiProvider.unregister(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index 166a6a8..434b0fc 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -2,6 +2,8 @@ import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.logger.PluginLogger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; @@ -38,7 +40,7 @@ public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { final T config = factory.instantiate(type); final File file = new File(dataFolder, config.fileName()); - configurer.configure(config, file); + configurer.configure(config, file, config.serdesPack()); lifecycle.initialize(config); register(type, config); @@ -73,6 +75,7 @@ public Set getConfigs() { return Collections.unmodifiableSet(configs); } + @Subscribe(event = PlayTimeShutdownEvent.class) public void shutdown() { configs.clear(); byType.clear(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java index d2e31e2..43446f9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java @@ -19,7 +19,6 @@ static Yaml create() { loader.setMaxAliasesForCollections(50); final Constructor constructor = new Constructor(loader); - final DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); options.setIndent(2); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java index 5b64907..822ea82 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java @@ -3,6 +3,7 @@ import com.github.imdmk.playtime.database.configurer.DataSourceConfigurer; import com.github.imdmk.playtime.database.configurer.DataSourceConfigurerFactory; import com.github.imdmk.playtime.database.library.DriverLibraryLoader; +import com.github.imdmk.playtime.injector.annotations.Database; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.Subscribe; @@ -18,7 +19,7 @@ import java.io.File; import java.sql.SQLException; -@Service(priority = Priority.NORMAL) +@Database(priority = Priority.NORMAL) public final class DatabaseBootstrap { private final File dataFolder; @@ -44,7 +45,6 @@ public DatabaseBootstrap( this.dataConnector = new DataSourceConnector(logger, factory, configurer); } - @PostConstruct public void start() { libraryLoader.loadFor(config.databaseMode); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java index 30e00e0..816bc34 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java @@ -1,6 +1,8 @@ package com.github.imdmk.playtime.feature.playtime; import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserFactory; import com.github.imdmk.playtime.user.UserTime; @@ -12,20 +14,7 @@ import java.util.Optional; import java.util.UUID; -/** - * Concrete implementation of {@link UserFactory} that constructs {@link User} instances - * using data retrieved from the {@link PlayTimeService}. - * - *

This factory supports both online and offline players, resolving their unique identifiers, - * last known names, and total recorded playtime from the underlying service.

- * - *

Dependency: {@link PlayTimeService} is injected at runtime and must be available - * before this factory is used.

- * - * @see User - * @see PlayTimeService - * @see UserFactory - */ +@Service(priority = Priority.LOW) public final class PlayTimeUserFactory implements UserFactory { private static final String UNKNOWN_PLAYER_NAME_FORMAT = "Unknown:%s"; @@ -37,40 +26,21 @@ public PlayTimeUserFactory(@NotNull PlayTimeService playtimeService) { this.playtimeService = playtimeService; } - /** - * Creates a {@link User} instance from an online {@link Player}. - * - *

The user's UUID and current name are taken directly from the live {@link Player} object, - * and their total playtime is resolved via the {@link PlayTimeService}.

- * - * @param player non-null online player instance - * @return new {@link User} representing the given player and their current playtime - * @throws NullPointerException if {@code player} is null - */ @Override public @NotNull User createFrom(@NotNull Player player) { final UUID uuid = player.getUniqueId(); final String name = player.getName(); final UserTime time = playtimeService.getTime(uuid); + return new User(uuid, name, time); } - /** - * Creates a {@link User} instance from an {@link OfflinePlayer}. - * - *

If the player's name cannot be resolved (e.g. first join or data missing), - * a default placeholder name {@code "Unknown"} is used instead. - * The total playtime is fetched from {@link PlayTimeService} based on the player's UUID.

- * - * @param player non-null offline player instance - * @return new {@link User} representing the offline player and their playtime data - * @throws NullPointerException if {@code player} is null - */ @Override public @NotNull User createFrom(@NotNull OfflinePlayer player) { final UUID uuid = player.getUniqueId(); final String name = Optional.ofNullable(player.getName()).orElse(UNKNOWN_PLAYER_NAME_FORMAT.formatted(uuid)); final UserTime time = playtimeService.getTime(uuid); + return new User(uuid, name, time); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java index 946c780..f79d197 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java @@ -1,13 +1,40 @@ package com.github.imdmk.playtime.injector; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Injector; import java.lang.annotation.Annotation; -public record Component
( - @NotNull Class type, - @NotNull A annotation -) {} +public final class Component { + + private final Class type; + private final A annotation; + private Object instance; + + public Component(@NotNull Class type, @NotNull A annotation) { + this.type = type; + this.annotation = annotation; + } + + public Class type() { + return type; + } + + public A annotation() { + return annotation; + } + + public Object instance() { + return instance; + } + + public void create(@NotNull Injector injector) { + if (this.instance == null) { + this.instance = injector.newInstance(type); + } + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java index 6a0341f..7d4381e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -1,38 +1,39 @@ package com.github.imdmk.playtime.injector; -import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; +import com.github.imdmk.playtime.injector.postprocessor.ComponentPostProcessor; import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.priority.PriorityProvider; import com.github.imdmk.playtime.injector.processor.ComponentProcessor; import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; -import com.github.imdmk.playtime.injector.processor.FunctionalComponentProcessor; +import com.github.imdmk.playtime.injector.processor.ComponentProcessors; +import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Injector; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; public final class ComponentManager { - private static final PriorityProvider DEFAULT_PRIORITY = priority -> Priority.NORMAL; - private final Injector injector; - - private final ComponentQueue container; + private final ComponentQueue queue; private final ComponentScanner scanner; private final Map, ComponentProcessor> processors = new ConcurrentHashMap<>(); + private final List postProcessors = new ArrayList<>(); public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) { this.injector = injector; - this.container = new ComponentQueue(DEFAULT_PRIORITY); + this.queue = new ComponentQueue(priority -> Priority.NORMAL); this.scanner = new ComponentScanner(basePackage); } - public ComponentManager setPriorityProvider(@NotNull PriorityProvider provider) { - container.setPriorityProvider(provider); + public ComponentManager setPriorityProvider(@NotNull PriorityProvider priorityProvider) { + queue.setPriorityProvider(priorityProvider); return this; } @@ -41,57 +42,59 @@ public ComponentManager addProcessor(@NotNull ComponentProcessor processor) { return this; } - public ComponentManager addProcessor(@NotNull Class> processorClass) { - return addProcessor(injector.newInstance(processorClass)); - } - public ComponentManager addProcessors(@NotNull List> processors) { processors.forEach(this::addProcessor); return this; } - public ComponentManager onProcess( - @NotNull Class annotation, - @NotNull ComponentFunctional consumer - ) { - return onProcess(annotation, Object.class, consumer); - } - - public ComponentManager onProcess( - @NotNull Class annotation, - @NotNull Class targetType, - @NotNull ComponentFunctional consumer - ) { - return addProcessor(new FunctionalComponentProcessor<>(annotation, targetType, consumer)); + public ComponentManager addPostProcessor(@NotNull ComponentPostProcessor postProcessor) { + postProcessors.add(postProcessor); + return this; } - public ComponentManager onProcess( - @NotNull ComponentFunctional consumer - ) { - return onProcess(NoneAnnotation.class, Object.class, consumer); + public ComponentManager addPostProcessors(@NotNull List postProcessors) { + postProcessors.forEach(this::addPostProcessor); + return this; } public void scanAll() { - for (final Class annotation : processors.keySet()) { - scanner.scan(annotation).forEach(container::add); - } + processors.keySet().forEach(annotation -> scanner.scan(annotation).forEach(queue::add)); } public void processAll() { + final ComponentProcessorContext context = new ComponentProcessorContext(injector); + for (final Priority priority : Priority.values()) { - for (final ComponentProcessor processor : processors.values()) { - container.drain(priority, processor.annotation()) - .forEach(component -> process(processor, component)); + for (final Map.Entry, ComponentProcessor> entry : processors.entrySet()) { + + final Class annotation = entry.getKey(); + final ComponentProcessor processor = entry.getValue(); + + for (final Component component : queue.drain(priority, annotation)) { + System.out.println("creating instance componenet of class: " + component.type().getName()); + component.create(injector); + + processComponent(processor, component, context); + + for (final ComponentPostProcessor post : postProcessors) { + post.postProcess(component.instance(), context); + } + } } } } @SuppressWarnings("unchecked") - private void process( + private void processComponent( ComponentProcessor processor, - Component component + Component component, + ComponentProcessorContext context ) { - processor.process((Component) component, new ComponentProcessorContext(injector)); + processor.process( + component.instance(), + (A) component.annotation(), + context + ); } - } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java index a342719..9666cdd 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java @@ -1,8 +1,9 @@ package com.github.imdmk.playtime.injector; -import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; import org.jetbrains.annotations.NotNull; -import org.reflections.Reflections; import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; @@ -11,30 +12,36 @@ final class ComponentScanner { - private final Reflections reflections; + private static final String SHADED_LIBS = "com.github.imdmk.playtime.lib"; + + private final String basePackage; ComponentScanner(@NotNull String basePackage) { - this.reflections = new Reflections(basePackage); + this.basePackage = basePackage; } Set> scan(@NotNull Class annotation) { - if (annotation == NoneAnnotation.class) { - return reflections.getSubTypesOf(Object.class).stream() - .filter(type -> !type.isInterface()) - .filter(type -> !Modifier.isAbstract(type.getModifiers())) + try (final ScanResult scan = new ClassGraph() + .enableAllInfo() + .acceptPackages(basePackage) + .rejectPackages(SHADED_LIBS) + .scan()) { + + return scan.getClassesWithAnnotation(annotation.getName()) + .stream() + .map(ClassInfo::loadClass) + .filter(this::isValidComponent) .map(type -> new Component<>( type, - NoneAnnotation.INSTANCE + type.getAnnotation(annotation) )) .collect(Collectors.toSet()); } + } - return reflections.getTypesAnnotatedWith(annotation).stream() - .map(type -> new Component<>( - type, - type.getAnnotation(annotation) - )) - .collect(Collectors.toSet()); + private boolean isValidComponent(@NotNull Class type) { + return !type.isInterface() && !Modifier.isAbstract(type.getModifiers()); } } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java new file mode 100644 index 0000000..ee2a827 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Database { + + Priority priority() default Priority.NORMAL; + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java deleted file mode 100644 index 42dad5a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.imdmk.playtime.injector.annotations; - -import java.lang.annotation.Annotation; - -public class NoneAnnotation implements Annotation { - - public static final NoneAnnotation INSTANCE = new NoneAnnotation(); - - @Override - public Class annotationType() { - return NoneAnnotation.class; - } -} - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/postprocessor/ComponentPostProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/postprocessor/ComponentPostProcessor.java new file mode 100644 index 0000000..68d97ef --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/postprocessor/ComponentPostProcessor.java @@ -0,0 +1,13 @@ +package com.github.imdmk.playtime.injector.postprocessor; + +import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; +import org.jetbrains.annotations.NotNull; + +@FunctionalInterface +public interface ComponentPostProcessor { + + void postProcess(@NotNull Object instance, @NotNull ComponentProcessorContext context); + +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java index e327c94..3f3d9db 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.injector.priority; import com.github.imdmk.playtime.injector.Component; -import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -12,10 +11,6 @@ public final class AnnotationPriorityProvider implements PriorityProvider { public Priority apply(Component component) { final Class componentClass = component.type(); - if (component.annotation().annotationType() == NoneAnnotation.class) { - return Priority.HIGHEST; - } - for (final Annotation annotation : componentClass.getAnnotations()) { try { final Method method = annotation.annotationType().getMethod("priority"); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java index df64683..2efd185 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java @@ -1,24 +1,10 @@ package com.github.imdmk.playtime.injector.processor; -import com.github.imdmk.playtime.injector.Component; -import org.jetbrains.annotations.NotNull; - import java.lang.annotation.Annotation; public abstract class AbstractComponentProcessor implements ComponentProcessor { - @Override - public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { - final Object instance = context.injector().newInstance(component.type()); - this.handle(instance, component.annotation(), context); - } - - protected abstract void handle( - @NotNull Object instance, - @NotNull A annotation, - @NotNull ComponentProcessorContext context - ); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java index 8c74dac..b4640c4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.injector.processor; -import com.github.imdmk.playtime.injector.Component; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; @@ -9,7 +8,11 @@ public interface ComponentProcessor { @NotNull Class annotation(); - void process(@NotNull Component component, @NotNull ComponentProcessorContext context); - + void process( + @NotNull Object instance, + @NotNull A annotation, + @NotNull ComponentProcessorContext context + ); } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java index 5259f2c..9d6236c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -2,18 +2,20 @@ import com.github.imdmk.playtime.config.ConfigSection; import com.github.imdmk.playtime.config.ConfigService; +import com.github.imdmk.playtime.database.DatabaseBootstrap; import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; import com.github.imdmk.playtime.injector.annotations.ConfigFile; import com.github.imdmk.playtime.injector.annotations.Controller; -import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; +import com.github.imdmk.playtime.injector.annotations.Database; import com.github.imdmk.playtime.injector.annotations.Repository; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.subscriber.Publisher; import org.bukkit.event.Listener; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Resources; import org.panda_lang.utilities.inject.annotations.Inject; +import java.lang.module.ResolutionException; import java.util.ArrayList; import java.util.List; @@ -31,19 +33,39 @@ public static List> defaults(@NotNull Plugin plugin) { processors.add(new FunctionalComponentProcessor<>( Service.class, Object.class, - (service, annotation, context) -> context.injector().getResources().on(service.getClass()).assignInstance(service) + (service, annotation, context) -> { + final Resources resources = context.injector().getResources(); + + resources.on(service.getClass()).assignInstance(() -> service); + + for (final Class interfaces : service.getClass().getInterfaces()) { + resources.on(interfaces).assignInstance(service); + } + } )); processors.add(new FunctionalComponentProcessor<>( ConfigFile.class, ConfigSection.class, - (config, annotation, context) -> context.injector().newInstance(ConfigFileProcessor.class).handle(config, annotation, context) + (config, annotation, context) -> context.injector().newInstance(ConfigFileProcessor.class).process(config, annotation, context) + )); + + processors.add(new FunctionalComponentProcessor<>( + Database.class, + DatabaseBootstrap.class, + (database, annotation, context) -> { + database.start(); + context.injector().getResources().on(database.getClass()).assignInstance(database); + } )); processors.add(new FunctionalComponentProcessor<>( Repository.class, OrmLiteRepository.class, - (repository, annotation, context) -> repository.start() + (repository, annotation, context) -> { + repository.start(); + context.injector().getResources().on(repository.getClass()).assignInstance(repository); + } )); processors.add(new FunctionalComponentProcessor<>( @@ -52,12 +74,6 @@ public static List> defaults(@NotNull Plugin plugin) { (listener, annotation, context) -> plugin.getServer().getPluginManager().registerEvents(listener, plugin) )); - processors.add(new FunctionalComponentProcessor<>( - NoneAnnotation.class, - Object.class, - (instance, none, context) -> context.injector().newInstance(NoneAnnotationProcessor.class).handle(instance, none, context) - )); - return processors; } @@ -71,7 +87,7 @@ private ConfigFileProcessor(@NotNull ConfigService configService) { } @Override - protected void handle(@NotNull Object instance, @NotNull ConfigFile annotation, @NotNull ComponentProcessorContext context) { + public void process(@NotNull Object instance, @NotNull ConfigFile annotation, @NotNull ComponentProcessorContext context) { if (!(instance instanceof ConfigSection configSection)) { throw new IllegalArgumentException("Invalid config section: " + instance.getClass().getName()); } @@ -81,30 +97,11 @@ protected void handle(@NotNull Object instance, @NotNull ConfigFile annotation, } @Override - public @NotNull Class annotation() { + @NotNull + public Class annotation() { return ConfigFile.class; } } - - private static class NoneAnnotationProcessor extends AbstractComponentProcessor { - - private final Publisher publisher; - - @Inject - private NoneAnnotationProcessor(@NotNull Publisher publisher) { - this.publisher = publisher; - } - - @Override - protected void handle(@NotNull Object instance, @NotNull NoneAnnotation annotation, @NotNull ComponentProcessorContext context) { - publisher.subscribe(instance); - } - - @Override - public @NotNull Class annotation() { - return NoneAnnotation.class; - } - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java index 059312f..a05a4e9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java @@ -14,9 +14,9 @@ public final class FunctionalComponentProcessor private final ComponentFunctional consumer; public FunctionalComponentProcessor( - @NotNull Class annotationType, - @NotNull Class targetType, - @NotNull ComponentFunctional consumer + Class annotationType, + Class targetType, + ComponentFunctional consumer ) { this.annotationType = annotationType; this.targetType = targetType; @@ -25,22 +25,18 @@ public FunctionalComponentProcessor( @Override public @NotNull Class annotation() { - return this.annotationType; + return annotationType; } @Override @SuppressWarnings("unchecked") - public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { - final Object instance = context.injector().newInstance(component.type()); + public void process(@NotNull Object instance, @NotNull A annotation, @NotNull ComponentProcessorContext context) { if (!targetType.isInstance(instance)) { return; } - consumer.accept( - (T) instance, - component.annotation(), - context - ); + consumer.accept((T) instance, annotation, context); } } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java index ef9db5c..24d5bc1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -1,7 +1,5 @@ package com.github.imdmk.playtime.injector.subscriber; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Injector; @@ -12,7 +10,6 @@ import java.util.List; import java.util.Map; -@Service(priority = Priority.LOWEST) public final class LocalPublisher implements Publisher { private final Map, List> subscribers = new HashMap<>(); @@ -34,6 +31,8 @@ public void subscribe(@NotNull Object instance) { final Class eventType = subscribe.event(); method.setAccessible(true); + System.out.println("published class: " + instance.getClass().getName() + "method: " + method.getName() + "event: " + eventType.getName()); + subscribers .computeIfAbsent(eventType, k -> new ArrayList<>()) .add(new SubscriberMethod(instance, method)); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java index 5001dbd..4835b79 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java @@ -19,7 +19,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -@Service(priority = Priority.LOW) +@Service(priority = Priority.NORMAL) public final class MessageService extends BukkitMultification { private final MessageConfig messageConfig; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java index f1069b1..fec1fbe 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java @@ -15,7 +15,7 @@ public final class GuiConfig extends ConfigSection { @Comment({"#", "# Playtime top GUI", "#"}) - public PlayTimeTopGuiConfig playtimeTopGui = new PlayTimeTopGuiConfig(); + public PlayTimeTopGuiConfig playTimeTopGui = new PlayTimeTopGuiConfig(); @Comment({"#", "# Navigation Bar", "#"}) public NavigationBarConfig navigationBar = new NavigationBarConfig(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java new file mode 100644 index 0000000..af34ff7 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java @@ -0,0 +1,27 @@ +package com.github.imdmk.playtime.platform.metrics; + +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; +import org.bstats.bukkit.Metrics; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@Service(priority = Priority.LOW) +public class BMetricsService { + + private static final int METRICS_ID = 19362; + //private final Metrics metrics; + + @Inject + public BMetricsService(@NotNull Plugin plugin) { + //this.metrics = new Metrics(plugin, METRICS_ID); + } + + @Subscribe(event = PlayTimeShutdownEvent.class) + void shutdown() { + //metrics.shutdown(); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java index 945ef71..b6b723e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java @@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -@Service(priority = Priority.HIGH) +@Service(priority = Priority.HIGHEST) final class UserServiceImpl implements UserService { private static final Duration TIMEOUT = Duration.ofSeconds(2L); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java index 349ec7c..2ee9126 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java @@ -23,21 +23,22 @@ @Service(priority = Priority.LOWEST) public final class CaffeineUserCache implements UserCache { - private static final Duration DEFAULT_EXPIRE_AFTER_ACCESS = Duration.ofHours(2); - private static final Duration DEFAULT_EXPIRE_AFTER_WRITE = Duration.ofHours(12); + private static final Duration EXPIRE_AFTER_ACCESS = Duration.ofHours(2); + private static final Duration EXPIRE_AFTER_WRITE = Duration.ofHours(12); private final Cache cacheByUuid; private final Cache cacheByName; - public CaffeineUserCache(@NotNull Duration expireAfterAccess, @NotNull Duration expireAfterWrite) { + @Inject + public CaffeineUserCache() { this.cacheByName = Caffeine.newBuilder() - .expireAfterWrite(expireAfterWrite) - .expireAfterAccess(expireAfterAccess) + .expireAfterWrite(EXPIRE_AFTER_WRITE) + .expireAfterAccess(EXPIRE_AFTER_ACCESS) .build(); this.cacheByUuid = Caffeine.newBuilder() - .expireAfterWrite(expireAfterWrite) - .expireAfterAccess(expireAfterAccess) + .expireAfterWrite(EXPIRE_AFTER_WRITE) + .expireAfterAccess(EXPIRE_AFTER_ACCESS) .removalListener((UUID key, User user, RemovalCause cause) -> { if (key != null && user != null) { this.cacheByName.invalidate(user.getName()); @@ -46,11 +47,6 @@ public CaffeineUserCache(@NotNull Duration expireAfterAccess, @NotNull Duration .build(); } - @Inject - public CaffeineUserCache() { - this(DEFAULT_EXPIRE_AFTER_ACCESS, DEFAULT_EXPIRE_AFTER_WRITE); - } - @Override public void cacheUser(@NotNull User user) { final UUID uuid = user.getUuid(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java index 7baa30d..96355be 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java @@ -3,6 +3,7 @@ import com.github.imdmk.playtime.database.DatabaseBootstrap; import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; import com.github.imdmk.playtime.injector.annotations.Repository; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.github.imdmk.playtime.user.User; @@ -17,7 +18,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -@Repository +@Repository(priority = Priority.NORMAL) public final class UserRepositoryOrmLite extends OrmLiteRepository implements UserRepository { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java index 33d5102..db3a60e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java @@ -1,5 +1,7 @@ package com.github.imdmk.playtime.user.top; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.repository.UserRepository; @@ -12,6 +14,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +@Service(priority = Priority.NORMAL) public final class MemoryTopUsersCache implements TopUsersCache { private final PluginLogger logger; diff --git a/playtime-plugin/build.gradle.kts b/playtime-plugin/build.gradle.kts index 5b3d067..c513a23 100644 --- a/playtime-plugin/build.gradle.kts +++ b/playtime-plugin/build.gradle.kts @@ -28,33 +28,33 @@ tasks.withType { "org/jetbrains/annotations/**" ) - val relocationPrefix = "com.github.imdmk.playtime.lib" - listOf( - "com.alessiodp.libby", - "com.eternalcode.multification", - "com.github.benmanes.caffeine", - "com.google.errorprone", - "com.google.gson", - "com.j256.ormlite", - "com.zaxxer.hikari", - "dev.rollczi.litecommands", - "dev.triumphteam.gui", - "eu.okaeri.configs", - "javassist", - "net.kyori", - "org.bstats", - "org.jspecify.annotations", - "org.panda_lang.utilities", - "org.yaml.snakeyaml", - "panda.std", - "panda.utilities" - ).forEach { pkg -> - relocate(pkg, "$relocationPrefix.$pkg") - } - - minimize { - exclude(dependency("com.github.ben-manes.caffeine:caffeine")) - } +// val relocationPrefix = "com.github.imdmk.playtime.lib" +// listOf( +// "com.alessiodp.libby", +// "com.eternalcode.multification", +// "com.github.benmanes.caffeine", +// "com.google.errorprone", +// "com.google.gson", +// "com.j256.ormlite", +// "com.zaxxer.hikari", +// "dev.rollczi.litecommands", +// "dev.triumphteam.gui", +// "eu.okaeri.configs", +// "javassist", +// "net.kyori", +// "org.bstats", +// "org.jspecify.annotations", +// "org.panda_lang.utilities", +// "org.yaml.snakeyaml", +// "panda.std", +// "panda.utilities" +// ).forEach { pkg -> +// relocate(pkg, "$relocationPrefix.$pkg") +// } +// +// minimize { +// exclude(dependency("com.github.ben-manes.caffeine:caffeine")) +// } } bukkit { From 174eda20a229a7b2575374ebf0e89948f7b8f4bc Mon Sep 17 00:00:00 2001 From: imDMK Date: Thu, 8 Jan 2026 18:15:07 +0100 Subject: [PATCH 07/17] Continue work. --- .../github/imdmk/playtime/PlayTimePlugin.java | 4 +- .../config/ConfigCreateException.java | 16 ++ .../imdmk/playtime/config/ConfigFactory.java | 2 +- .../playtime/config/ConfigLifecycle.java | 13 +- .../imdmk/playtime/config/ConfigService.java | 23 +- .../playtime/database/DatabaseBootstrap.java | 5 +- .../DataSourceConfigurerFactory.java | 2 +- .../playtime/BukkitPlayTimeService.java | 4 +- .../feature/playtime/PlayTimeUserFactory.java | 4 +- .../playtime/command/TimeSetCommand.java | 2 +- .../playtime/gui/PlayTimeTopGuiConfig.java | 2 - .../placeholder/PlayTimePlaceholder.java | 2 +- .../imdmk/playtime/injector/Component.java | 27 +- .../playtime/injector/ComponentFactory.java | 40 +++ .../injector/ComponentFunctional.java | 17 -- .../playtime/injector/ComponentManager.java | 81 +++--- .../playtime/injector/ComponentPriority.java | 5 + .../playtime/injector/ComponentQueue.java | 71 ----- .../playtime/injector/ComponentScanner.java | 21 +- .../playtime/injector/ComponentSorter.java | 17 ++ .../injector/annotations/ConfigFile.java | 7 +- .../injector/annotations/Controller.java | 4 +- .../injector/annotations/Database.java | 6 +- .../{Placeholder.java => Gui.java} | 6 +- .../injector/annotations/Repository.java | 6 +- .../injector/annotations/Service.java | 6 +- .../annotations/lite/LiteContextual.java | 14 - .../annotations/lite/LiteHandler.java | 1 - .../placeholderapi/Placeholder.java | 16 ++ .../priority/AnnotationPriorityProvider.java | 35 --- .../playtime/injector/priority/Priority.java | 5 - .../injector/priority/PriorityProvider.java | 10 - .../processor/AbstractComponentProcessor.java | 10 - .../ComponentPostProcessor.java | 3 +- .../ComponentProcessorFunctional.java | 35 +++ .../processor/ComponentProcessors.java | 254 +++++++++++++----- .../FunctionalComponentProcessor.java | 42 --- .../injector/processor/ProcessorBuilder.java | 40 +++ .../processor/ProcessorContainer.java | 10 + .../injector/processor/ProcessorHandler.java | 13 + .../injector/subscriber/LocalPublisher.java | 4 +- .../playtime/message/MessageService.java | 4 +- .../platform/event/BukkitEventCaller.java | 4 +- .../playtime/platform/gui/GuiRegistry.java | 4 +- .../litecommands/LiteCommandsConfigurer.java | 32 +++ .../InvalidUsageHandlerImpl.java | 4 +- .../MissingPermissionsHandlerImpl.java | 4 +- .../NoticeResultHandlerImpl.java | 4 +- .../platform/metrics/BMetricsService.java | 10 +- ...apterImpl.java => PlaceholderService.java} | 35 ++- .../adapter/NoopPlaceholderAdapter.java | 21 -- .../adapter/PlaceholderAdapter.java | 16 -- .../adapter/PlaceholderAdapterFactory.java | 31 --- .../scheduler/BukkitTaskScheduler.java | 4 +- .../github/imdmk/playtime/time/Durations.java | 1 + .../imdmk/playtime/user/UserArgument.java | 2 + .../imdmk/playtime/user/UserServiceImpl.java | 4 +- .../user/cache/CaffeineUserCache.java | 16 +- .../user/repository/UserEntityMapper.java | 4 +- .../user/repository/UserEntityMeta.java | 1 + .../repository/UserRepositoryOrmLite.java | 3 +- .../user/top/MemoryTopUsersCache.java | 4 +- 62 files changed, 592 insertions(+), 501 deletions(-) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigCreateException.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFactory.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentPriority.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentSorter.java rename playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/{Placeholder.java => Gui.java} (65%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/placeholderapi/Placeholder.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java rename playtime-core/src/main/java/com/github/imdmk/playtime/injector/{postprocessor => processor}/ComponentPostProcessor.java (59%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorFunctional.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorBuilder.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorContainer.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorHandler.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java rename playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/{ => handler}/InvalidUsageHandlerImpl.java (90%) rename playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/{ => handler}/MissingPermissionsHandlerImpl.java (85%) rename playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/{ => handler}/NoticeResultHandlerImpl.java (84%) rename playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/{adapter/PlaceholderAdapterImpl.java => PlaceholderService.java} (78%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/NoopPlaceholderAdapter.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index e2ca11d..0bf6c6c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.injector.ComponentManager; -import com.github.imdmk.playtime.injector.priority.AnnotationPriorityProvider; import com.github.imdmk.playtime.injector.processor.ComponentProcessors; import com.github.imdmk.playtime.injector.subscriber.LocalPublisher; import com.github.imdmk.playtime.injector.subscriber.Publisher; @@ -41,7 +40,6 @@ final class PlayTimePlugin { this.publisher = new LocalPublisher(injector); final ComponentManager componentManager = new ComponentManager(injector, plugin.getClass().getPackageName()) - .setPriorityProvider(new AnnotationPriorityProvider()) .addProcessors(ComponentProcessors.defaults(plugin)) .addPostProcessor(((instance, context) -> this.publisher.subscribe(instance))); @@ -59,6 +57,6 @@ final class PlayTimePlugin { void disable() { this.publisher.publish(new PlayTimeShutdownEvent()); - PlayTimeApiProvider.unregister(); + //PlayTimeApiProvider.unregister(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigCreateException.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigCreateException.java new file mode 100644 index 0000000..260727e --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigCreateException.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.config; + +public class ConfigCreateException extends RuntimeException { + + public ConfigCreateException(String message) { + super(message); + } + + public ConfigCreateException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigCreateException(Throwable cause) { + super(cause); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java index 553b8fb..e50aad7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java @@ -10,7 +10,7 @@ final class ConfigFactory { try { return ConfigManager.create(type); } catch (OkaeriException e) { - throw new IllegalStateException( + throw new ConfigCreateException( "Failed to instantiate config: " + type.getName(), e ); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java index a9f20b1..6e941ad 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java @@ -1,17 +1,10 @@ package com.github.imdmk.playtime.config; -import com.github.imdmk.playtime.platform.logger.PluginLogger; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; final class ConfigLifecycle { - private final PluginLogger logger; - - ConfigLifecycle(@NotNull PluginLogger logger) { - this.logger = logger; - } - void initialize(@NotNull ConfigSection config) { config.saveDefaults(); load(config); @@ -21,8 +14,7 @@ void load(@NotNull ConfigSection config) { try { config.load(true); } catch (OkaeriException e) { - logger.error(e, "Failed to load config %s", config.getClass().getSimpleName()); - throw new ConfigAccessException(e); + throw new ConfigAccessException("Failed to load config: " + config.getClass().getSimpleName(), e); } } @@ -30,8 +22,7 @@ void save(@NotNull ConfigSection config) { try { config.save(); } catch (OkaeriException e) { - logger.error(e, "Failed to save config %s", config.getClass().getSimpleName()); - throw new ConfigAccessException(e); + throw new ConfigAccessException("Failed to save config: " + config.getClass().getSimpleName(), e); } } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index 434b0fc..5c21f7e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -1,10 +1,9 @@ package com.github.imdmk.playtime.config; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; -import com.github.imdmk.playtime.platform.logger.PluginLogger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.panda_lang.utilities.inject.annotations.Inject; @@ -15,7 +14,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -@Service(priority = Priority.LOWEST) +@Service(priority = ComponentPriority.LOWEST, order = 0) public final class ConfigService { private final Set configs = ConcurrentHashMap.newKeySet(); @@ -28,16 +27,16 @@ public final class ConfigService { private final ConfigLifecycle lifecycle; @Inject - public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { + public ConfigService(@NotNull File dataFolder) { this.dataFolder = dataFolder; this.factory = new ConfigFactory(); this.configurer = new ConfigConfigurer(); - this.lifecycle = new ConfigLifecycle(logger); + this.lifecycle = new ConfigLifecycle(); } - public @NotNull T create(@NotNull Class type) { - final T config = factory.instantiate(type); + public C create(@NotNull Class type) { + final C config = factory.instantiate(type); final File file = new File(dataFolder, config.fileName()); configurer.configure(config, file, config.serdesPack()); @@ -48,14 +47,14 @@ public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { } @SuppressWarnings("unchecked") - public T get(@NotNull Class type) { - return (T) byType.get(type); + public C get(@NotNull Class type) { + return (C) byType.get(type); } - public @NotNull T require(@NotNull Class type) { - final T config = get(type); + public C require(@NotNull Class type) { + final C config = get(type); if (config == null) { - throw new IllegalStateException("Config not created: " + type.getName()); + throw new ConfigAccessException("Config not created: " + type.getName()); } return config; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java index 822ea82..e6758c0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java @@ -4,8 +4,6 @@ import com.github.imdmk.playtime.database.configurer.DataSourceConfigurerFactory; import com.github.imdmk.playtime.database.library.DriverLibraryLoader; import com.github.imdmk.playtime.injector.annotations.Database; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.logger.PluginLogger; @@ -14,12 +12,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.panda_lang.utilities.inject.annotations.Inject; -import org.panda_lang.utilities.inject.annotations.PostConstruct; import java.io.File; import java.sql.SQLException; -@Database(priority = Priority.NORMAL) +@Database public final class DatabaseBootstrap { private final File dataFolder; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java index 762d737..fee9bf9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java @@ -16,7 +16,7 @@ DatabaseMode.H2, new H2Configurer(), DatabaseMode.SQL, new SQLConfigurer() ); - public static @NotNull DataSourceConfigurer getFor(@NotNull DatabaseMode mode) { + public static DataSourceConfigurer getFor(@NotNull DatabaseMode mode) { final DataSourceConfigurer configurer = CONFIGURER_BY_MODE.get(mode); if (configurer == null) { throw new IllegalArgumentException("Unsupported database mode: " + mode); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java index 450179a..4cf2684 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java @@ -1,8 +1,8 @@ package com.github.imdmk.playtime.feature.playtime; import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.UserTime; import org.bukkit.OfflinePlayer; import org.bukkit.Server; @@ -12,7 +12,7 @@ import java.util.UUID; -@Service(priority = Priority.LOWEST) +@Service(priority = ComponentPriority.LOWEST) final class BukkitPlayTimeService implements PlayTimeService { private static final Statistic PLAYTIME_STATISTIC = Statistic.PLAY_ONE_MINUTE; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java index 816bc34..8eb3368 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java @@ -1,8 +1,8 @@ package com.github.imdmk.playtime.feature.playtime; import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserFactory; import com.github.imdmk.playtime.user.UserTime; @@ -14,7 +14,7 @@ import java.util.Optional; import java.util.UUID; -@Service(priority = Priority.LOW) +@Service(priority = ComponentPriority.LOW) public final class PlayTimeUserFactory implements UserFactory { private static final String UNKNOWN_PLAYER_NAME_FORMAT = "Unknown:%s"; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java index 5831386..d56efe6 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java @@ -60,7 +60,7 @@ void setPlaytime(@Context CommandSender sender, @Arg @Async User target, @Arg Du ) .exceptionally(e -> { logger.error(e, "Failed to save user on playtime set command (target=%s)", target.getName()); - //messageService.send(sender, n -> n.actionExecutionError); + messageService.send(sender, n -> n.actionExecutionError); return null; }); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java index 76f5605..fd0bab0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.feature.playtime.gui; -import com.github.imdmk.playtime.injector.annotations.ConfigFile; import com.github.imdmk.playtime.platform.adventure.AdventureComponents; import com.github.imdmk.playtime.platform.gui.GuiType; import com.github.imdmk.playtime.platform.gui.config.ConfigurableGui; @@ -15,7 +14,6 @@ import java.util.Collections; -@ConfigFile public final class PlayTimeTopGuiConfig extends OkaeriConfig implements ConfigurableGui { @Comment({ diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java index bc6ce3b..5416ee5 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.feature.playtime.placeholder; import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.annotations.Placeholder; +import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import com.github.imdmk.playtime.time.Durations; import org.bukkit.OfflinePlayer; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java index f79d197..fcef0b3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java @@ -9,11 +9,21 @@ public final class Component { private final Class type; private final A annotation; + private final ComponentPriority componentPriority; + private final int order; + private Object instance; - public Component(@NotNull Class type, @NotNull A annotation) { + public Component( + @NotNull Class type, + @NotNull A annotation, + @NotNull ComponentPriority componentPriority, + int order + ) { this.type = type; this.annotation = annotation; + this.componentPriority = componentPriority; + this.order = order; } public Class type() { @@ -24,13 +34,21 @@ public A annotation() { return annotation; } + public ComponentPriority priority() { + return componentPriority; + } + + public int order() { + return order; + } + public Object instance() { return instance; } - public void create(@NotNull Injector injector) { - if (this.instance == null) { - this.instance = injector.newInstance(type); + public void createInstance(@NotNull Injector injector) { + if (instance == null) { + instance = injector.newInstance(type); } } } @@ -38,3 +56,4 @@ public void create(@NotNull Injector injector) { + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFactory.java new file mode 100644 index 0000000..d634024 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFactory.java @@ -0,0 +1,40 @@ +package com.github.imdmk.playtime.injector; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +final class ComponentFactory { + + Component create( + @NotNull Class type, + @NotNull Class annotationType + ) { + final A annotation = type.getAnnotation(annotationType); + + final ComponentPriority componentPriority = extractPriority(annotation); + final int order = extractOrder(annotation); + + return new Component<>(type, annotation, componentPriority, order); + } + + private ComponentPriority extractPriority(Annotation annotation) { + try { + return (ComponentPriority) annotation.annotationType() + .getMethod("priority") + .invoke(annotation); + } catch (Exception e) { + return ComponentPriority.NORMAL; + } + } + + private int extractOrder(Annotation annotation) { + try { + return (int) annotation.annotationType() + .getMethod("order") + .invoke(annotation); + } catch (Exception e) { + return 0; + } + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java deleted file mode 100644 index 11ce942..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.imdmk.playtime.injector; - -import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; -import org.jetbrains.annotations.NotNull; - -import java.lang.annotation.Annotation; - -@FunctionalInterface -public interface ComponentFunctional { - - void accept( - @NotNull T instance, - @NotNull A annotation, - @NotNull ComponentProcessorContext context - ); -} - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java index 7d4381e..1a4427c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -1,49 +1,39 @@ package com.github.imdmk.playtime.injector; -import com.github.imdmk.playtime.injector.postprocessor.ComponentPostProcessor; -import com.github.imdmk.playtime.injector.priority.Priority; -import com.github.imdmk.playtime.injector.priority.PriorityProvider; +import com.github.imdmk.playtime.injector.processor.ComponentPostProcessor; import com.github.imdmk.playtime.injector.processor.ComponentProcessor; import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; -import com.github.imdmk.playtime.injector.processor.ComponentProcessors; -import org.bukkit.plugin.Plugin; +import com.github.imdmk.playtime.injector.processor.ProcessorContainer; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Injector; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; public final class ComponentManager { private final Injector injector; - private final ComponentQueue queue; private final ComponentScanner scanner; + private final ComponentSorter sorter; - private final Map, ComponentProcessor> processors = new ConcurrentHashMap<>(); + private final List> processors = new ArrayList<>(); private final List postProcessors = new ArrayList<>(); + private final List> components = new ArrayList<>(); public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) { this.injector = injector; - this.queue = new ComponentQueue(priority -> Priority.NORMAL); this.scanner = new ComponentScanner(basePackage); + this.sorter = new ComponentSorter(); } - public ComponentManager setPriorityProvider(@NotNull PriorityProvider priorityProvider) { - queue.setPriorityProvider(priorityProvider); + public ComponentManager addProcessor(@NotNull ProcessorContainer container) { + processors.add(container); return this; } - public ComponentManager addProcessor(@NotNull ComponentProcessor processor) { - processors.put(processor.annotation(), processor); - return this; - } - - public ComponentManager addProcessors(@NotNull List> processors) { - processors.forEach(this::addProcessor); + public ComponentManager addProcessors(@NotNull List> containers) { + containers.forEach(this::addProcessor); return this; } @@ -58,43 +48,46 @@ public ComponentManager addPostProcessors(@NotNull List } public void scanAll() { - processors.keySet().forEach(annotation -> scanner.scan(annotation).forEach(queue::add)); + for (final ProcessorContainer container : processors) { + components.addAll(scanner.scan(container.processor().annotation())); + } } public void processAll() { final ComponentProcessorContext context = new ComponentProcessorContext(injector); - for (final Priority priority : Priority.values()) { - for (final Map.Entry, ComponentProcessor> entry : processors.entrySet()) { + sorter.sort(components); - final Class annotation = entry.getKey(); - final ComponentProcessor processor = entry.getValue(); - - for (final Component component : queue.drain(priority, annotation)) { - System.out.println("creating instance componenet of class: " + component.type().getName()); - component.create(injector); - - processComponent(processor, component, context); - - for (final ComponentPostProcessor post : postProcessors) { - post.postProcess(component.instance(), context); - } - } - } + for (final Component component : components) { + processComponent(component, context); } } @SuppressWarnings("unchecked") private void processComponent( - ComponentProcessor processor, - Component component, + Component component, ComponentProcessorContext context ) { - processor.process( - component.instance(), - (A) component.annotation(), - context - ); + component.createInstance(injector); + + for (final ProcessorContainer container : processors) { + if (container.annotationType() != component.annotation().annotationType()) { + continue; + } + + final ComponentProcessor processor = (ComponentProcessor) container.processor(); + + processor.process( + component.instance(), + component.annotation(), + context + ); + } + + for (final ComponentPostProcessor post : postProcessors) { + post.postProcess(component.instance(), context); + } } } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentPriority.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentPriority.java new file mode 100644 index 0000000..fe6f967 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentPriority.java @@ -0,0 +1,5 @@ +package com.github.imdmk.playtime.injector; + +public enum ComponentPriority { + LOWEST, LOW, NORMAL, HIGH, HIGHEST +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java deleted file mode 100644 index ca09c7a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.imdmk.playtime.injector; - -import com.github.imdmk.playtime.injector.priority.Priority; -import com.github.imdmk.playtime.injector.priority.PriorityProvider; -import org.jetbrains.annotations.NotNull; - -import java.lang.annotation.Annotation; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -final class ComponentQueue { - - private final Object lock = new Object(); - - private final EnumMap, Deque>>> componentsByPriority = new EnumMap<>(Priority.class); - - private PriorityProvider priorityProvider; - - ComponentQueue(@NotNull PriorityProvider priorityProvider) { - setPriorityProvider(priorityProvider); - - for (final Priority priority : Priority.values()) { - this.componentsByPriority.put(priority, new HashMap<>()); - } - } - - void setPriorityProvider(@NotNull PriorityProvider priorityProvider) { - this.priorityProvider = priorityProvider; - } - - void add(@NotNull Component component) { - final Priority priority = this.priorityProvider.apply(component); - - synchronized (this.lock) { - this.componentsByPriority - .get(priority) - .computeIfAbsent( - component.annotation().annotationType(), - a -> new ArrayDeque<>() - ) - .addLast(component); - } - } - - List> drain( - @NotNull Priority priority, - @NotNull Class annotation - ) { - final List> result = new ArrayList<>(); - - synchronized (this.lock) { - final Deque> queue = this.componentsByPriority.get(priority).get(annotation); - if (queue == null) { - return result; - } - - while (!queue.isEmpty()) { - result.add(queue.pollFirst()); - } - } - - return result; - } -} - - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java index 9666cdd..186c6cc 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java @@ -7,41 +7,40 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.List; final class ComponentScanner { private static final String SHADED_LIBS = "com.github.imdmk.playtime.lib"; private final String basePackage; + private final ComponentFactory componentFactory; ComponentScanner(@NotNull String basePackage) { this.basePackage = basePackage; + this.componentFactory = new ComponentFactory(); } - Set> scan(@NotNull Class annotation) { + List> scan(@NotNull Class annotationType) { try (final ScanResult scan = new ClassGraph() .enableAllInfo() .acceptPackages(basePackage) .rejectPackages(SHADED_LIBS) .scan()) { - return scan.getClassesWithAnnotation(annotation.getName()) + return scan.getClassesWithAnnotation(annotationType.getName()) .stream() .map(ClassInfo::loadClass) - .filter(this::isValidComponent) - .map(type -> new Component<>( - type, - type.getAnnotation(annotation) - )) - .collect(Collectors.toSet()); + .filter(ComponentScanner::isValidComponent) + .map(type -> componentFactory.create(type, annotationType)) + .toList(); } } - private boolean isValidComponent(@NotNull Class type) { + private static boolean isValidComponent(Class type) { return !type.isInterface() && !Modifier.isAbstract(type.getModifiers()); } } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentSorter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentSorter.java new file mode 100644 index 0000000..886fd9b --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentSorter.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector; + +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.List; + +final class ComponentSorter { + + void sort(@NotNull List> components) { + components.sort(Comparator + .comparing((Component c) -> c.priority()) + .thenComparingInt(Component::order) + .thenComparing(c -> c.getClass().getName()) + ); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java index e481087..50d1d54 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.injector.annotations; -import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.ComponentPriority; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,6 +11,9 @@ @Target(ElementType.TYPE) public @interface ConfigFile { - Priority priority() default Priority.LOW; + ComponentPriority priority() default ComponentPriority.LOWEST; + + int order() default 0; + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java index 235b889..20d33a0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.injector.annotations; -import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.ComponentPriority; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,7 +11,7 @@ @Target(ElementType.TYPE) public @interface Controller { - Priority priority() default Priority.HIGHEST; + ComponentPriority priority() default ComponentPriority.HIGHEST; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java index ee2a827..9625f11 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Database.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.injector.annotations; -import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.ComponentPriority; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,6 +11,8 @@ @Target(ElementType.TYPE) public @interface Database { - Priority priority() default Priority.NORMAL; + ComponentPriority priority() default ComponentPriority.LOW; + + int order() default 0; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Gui.java similarity index 65% rename from playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Gui.java index ca19620..9065d26 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Gui.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.injector.annotations; -import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.ComponentPriority; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -9,8 +9,8 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -public @interface Placeholder { +public @interface Gui { - Priority priority() default Priority.HIGHEST; + ComponentPriority priority() default ComponentPriority.LOW; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java index 08e1c1d..924a48b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.injector.annotations; -import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.ComponentPriority; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,6 +11,8 @@ @Target(ElementType.TYPE) public @interface Repository { - Priority priority() default Priority.HIGH; + ComponentPriority priority() default ComponentPriority.LOW; + + int order() default 1; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java index 1cfa54e..dd0decb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime.injector.annotations; -import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.ComponentPriority; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,7 +11,9 @@ @Target(ElementType.TYPE) public @interface Service { - Priority priority() default Priority.NORMAL; + ComponentPriority priority() default ComponentPriority.NORMAL; + + int order() default 1; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java deleted file mode 100644 index 90d1840..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.imdmk.playtime.injector.annotations.lite; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface LiteContextual { - - Class value(); - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java index ccb7809..0208b82 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java @@ -10,5 +10,4 @@ public @interface LiteHandler { Class value(); - } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/placeholderapi/Placeholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/placeholderapi/Placeholder.java new file mode 100644 index 0000000..9e52410 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/placeholderapi/Placeholder.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations.placeholderapi; + +import com.github.imdmk.playtime.injector.ComponentPriority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Placeholder { + + ComponentPriority priority() default ComponentPriority.HIGHEST; + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java deleted file mode 100644 index 3f3d9db..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.imdmk.playtime.injector.priority; - -import com.github.imdmk.playtime.injector.Component; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; - -public final class AnnotationPriorityProvider implements PriorityProvider { - - @Override - public Priority apply(Component component) { - final Class componentClass = component.type(); - - for (final Annotation annotation : componentClass.getAnnotations()) { - try { - final Method method = annotation.annotationType().getMethod("priority"); - - final Object value = method.invoke(annotation); - if (value instanceof Priority priority) { - return priority; - } - } - catch (NoSuchMethodException ignored) { - // doesn't support priority - skip - } - catch (ReflectiveOperationException e) { - throw new IllegalStateException( - "Failed to resolve priority for " + componentClass.getName(), e - ); - } - } - - return Priority.NORMAL; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java deleted file mode 100644 index 2ae1acb..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.imdmk.playtime.injector.priority; - -public enum Priority { - LOWEST, LOW, NORMAL, HIGH, HIGHEST -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java deleted file mode 100644 index 8c415b8..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.imdmk.playtime.injector.priority; - -import com.github.imdmk.playtime.injector.Component; - -import java.util.function.Function; - -@FunctionalInterface -public interface PriorityProvider extends Function, Priority> { -} - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java deleted file mode 100644 index 2efd185..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.imdmk.playtime.injector.processor; - -import java.lang.annotation.Annotation; - -public abstract class AbstractComponentProcessor - implements ComponentProcessor { - -} - - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/postprocessor/ComponentPostProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentPostProcessor.java similarity index 59% rename from playtime-core/src/main/java/com/github/imdmk/playtime/injector/postprocessor/ComponentPostProcessor.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentPostProcessor.java index 68d97ef..ef00986 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/postprocessor/ComponentPostProcessor.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentPostProcessor.java @@ -1,6 +1,5 @@ -package com.github.imdmk.playtime.injector.postprocessor; +package com.github.imdmk.playtime.injector.processor; -import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; import org.jetbrains.annotations.NotNull; @FunctionalInterface diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorFunctional.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorFunctional.java new file mode 100644 index 0000000..2e3f289 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorFunctional.java @@ -0,0 +1,35 @@ +package com.github.imdmk.playtime.injector.processor; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public final class ComponentProcessorFunctional + implements ComponentProcessor { + + private final Class annotation; + private final ProcessorHandler handler; + + public ComponentProcessorFunctional( + @NotNull Class annotation, + @NotNull ProcessorHandler handler + ) { + this.annotation = annotation; + this.handler = handler; + } + + @NotNull + @Override + public Class annotation() { + return annotation; + } + + @Override + public void process( + @NotNull Object instance, + @NotNull A annotation, + @NotNull ComponentProcessorContext context + ) { + handler.handle(instance, annotation, context); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java index 9d6236c..e67d476 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -7,101 +7,221 @@ import com.github.imdmk.playtime.injector.annotations.ConfigFile; import com.github.imdmk.playtime.injector.annotations.Controller; import com.github.imdmk.playtime.injector.annotations.Database; +import com.github.imdmk.playtime.injector.annotations.Gui; import com.github.imdmk.playtime.injector.annotations.Repository; import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.annotations.lite.LiteArgument; +import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; +import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; +import dev.rollczi.litecommands.LiteCommandsBuilder; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.argument.ArgumentKey; +import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; +import dev.rollczi.litecommands.handler.result.ResultHandler; import org.bukkit.event.Listener; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Resources; import org.panda_lang.utilities.inject.annotations.Inject; -import java.lang.module.ResolutionException; -import java.util.ArrayList; import java.util.List; public final class ComponentProcessors { - /** - * Initializes all default component processors. - *

- * This method is called once during plugin startup. - * Processors may perform eager initialization and side effects. - */ - public static List> defaults(@NotNull Plugin plugin) { - final List> processors = new ArrayList<>(); - - processors.add(new FunctionalComponentProcessor<>( - Service.class, - Object.class, - (service, annotation, context) -> { - final Resources resources = context.injector().getResources(); - - resources.on(service.getClass()).assignInstance(() -> service); - - for (final Class interfaces : service.getClass().getInterfaces()) { - resources.on(interfaces).assignInstance(service); - } - } - )); - - processors.add(new FunctionalComponentProcessor<>( - ConfigFile.class, - ConfigSection.class, - (config, annotation, context) -> context.injector().newInstance(ConfigFileProcessor.class).process(config, annotation, context) - )); - - processors.add(new FunctionalComponentProcessor<>( - Database.class, - DatabaseBootstrap.class, - (database, annotation, context) -> { - database.start(); - context.injector().getResources().on(database.getClass()).assignInstance(database); - } - )); - - processors.add(new FunctionalComponentProcessor<>( - Repository.class, - OrmLiteRepository.class, - (repository, annotation, context) -> { - repository.start(); - context.injector().getResources().on(repository.getClass()).assignInstance(repository); + private ComponentProcessors() { + throw new UnsupportedOperationException("This is utility class and cannot be instantiated."); + } + + public static List> defaults(@NotNull Plugin plugin) { + return List.of( + ProcessorBuilder.forAnnotation(Service.class) + .handle((instance, annotation, ctx) -> { + final Resources resources = ctx.injector().getResources(); + + // bind + resources.on(instance.getClass()) + .assignInstance(instance); + + // bind interfaces + for (final Class interfaces : instance.getClass().getInterfaces()) { + resources.on(interfaces).assignInstance(instance); + } + }) + .build(), + + ProcessorBuilder.forAnnotation(ConfigFile.class) + .handle((instance, annotation, ctx) -> ctx.injector().newInstance(ConfigFileProcessor.class).process(instance, annotation, ctx)) + .build(), + + ProcessorBuilder.forAnnotation(Database.class) + .handle((instance, annotation, ctx) -> { + if (!(instance instanceof DatabaseBootstrap databaseBootstrap)) { + throw new IllegalStateException("@Database can only be used on with DatabaseBootstrap class"); + } + + databaseBootstrap.start(); + + ctx.injector().getResources() + .on(DatabaseBootstrap.class) + .assignInstance(databaseBootstrap); + }) + .build(), + + ProcessorBuilder.forAnnotation(Repository.class) + .handle((instance, annotation, ctx) -> { + if (!(instance instanceof OrmLiteRepository repository)) { + return; + } + + repository.start(); + + ctx.injector().getResources() + .on(repository.getClass()) + .assignInstance(repository); + }) + .build(), + + ProcessorBuilder.forAnnotation(Controller.class) + .handle((instance, annotation, ctx) -> { + if (!(instance instanceof Listener listener)) { + throw new IllegalStateException( + "@Controller must implement Listener: " + + instance.getClass().getName() + ); + } + + plugin.getServer() + .getPluginManager() + .registerEvents(listener, plugin); + }) + .build(), + + ProcessorBuilder.forAnnotation(Gui.class) + .handle() + .build(), + + ProcessorBuilder.forAnnotation(Placeholder.class) + .handle() + .build(), + + ProcessorBuilder.forAnnotation(Command.class) + .handle((instance, annotation, ctx) -> ctx.injector().newInstance(LiteCommandProcessor.class).process(instance, annotation, ctx)) + .build(), + + ProcessorBuilder.forAnnotation(LiteHandler.class) + .handle((instance, annotation, ctx) -> ctx.injector().newInstance(LiteHandlerProcessor.class).process(instance, annotation, ctx)) + .build(), + + ProcessorBuilder.forAnnotation(LiteArgument.class) + .handle((instance, annotation, ctx) -> ctx.injector().newInstance(LiteArgumentProcessor.class).process(instance, annotation, ctx)) + .build() + + ); + } + + @Inject + private record ConfigFileProcessor(@NotNull ConfigService configService) + implements ComponentProcessor { + + @Override + public void process( + @NotNull Object instance, + @NotNull ConfigFile annotation, + @NotNull ComponentProcessorContext context + ) { + if (!(instance instanceof ConfigSection configSection)) { + throw new IllegalStateException( + "@ConfigFile can only be used on ConfigSection: " + + instance.getClass().getName() + ); } - )); - processors.add(new FunctionalComponentProcessor<>( - Controller.class, - Listener.class, - (listener, annotation, context) -> plugin.getServer().getPluginManager().registerEvents(listener, plugin) - )); + configService.create(configSection.getClass()); + context.injector().getResources() + .on(instance.getClass()) + .assignInstance(instance); + } - return processors; + @Override + @NotNull + public Class annotation() { + return ConfigFile.class; + } } - private static class ConfigFileProcessor extends AbstractComponentProcessor { + private record - private final ConfigService configService; + @Inject + private record LiteHandlerProcessor(@NotNull LiteCommandsBuilder liteBuilder) + implements ComponentProcessor { - @Inject - private ConfigFileProcessor(@NotNull ConfigService configService) { - this.configService = configService; + @Override + public @NotNull Class annotation() { + return LiteHandler.class; } @Override - public void process(@NotNull Object instance, @NotNull ConfigFile annotation, @NotNull ComponentProcessorContext context) { - if (!(instance instanceof ConfigSection configSection)) { - throw new IllegalArgumentException("Invalid config section: " + instance.getClass().getName()); + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void process( + @NotNull Object instance, + @NotNull LiteHandler annotation, + @NotNull ComponentProcessorContext context + ) { + if (!(instance instanceof ResultHandler handler)) { + throw new IllegalStateException( + "@LiteHandler can only be used on ResultHandler: " + + instance.getClass().getName() + ); } - configService.create(configSection.getClass()); - context.injector().getResources().on(configSection.getClass()).assignInstance(configSection); + liteBuilder.result(annotation.value(), handler); } + } + + @Inject + private record LiteCommandProcessor(@NotNull LiteCommandsBuilder liteBuilder) + implements ComponentProcessor { @Override - @NotNull - public Class annotation() { - return ConfigFile.class; + public @NotNull Class annotation() { + return Command.class; + } + + @Override + public void process( + @NotNull Object instance, + @NotNull Command annotation, + @NotNull ComponentProcessorContext context + ) { + liteBuilder.commands(instance); } } -} + @Inject + private record LiteArgumentProcessor(@NotNull LiteCommandsBuilder liteBuilder) + implements ComponentProcessor { + + @Override + public @NotNull Class annotation() { + return LiteArgument.class; + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void process( + @NotNull Object instance, + @NotNull LiteArgument annotation, + @NotNull ComponentProcessorContext context + ) { + if (!(instance instanceof ArgumentResolver argumentResolver)) { + throw new IllegalStateException( + "@LiteArgument can only be used on ArgumentResolver: " + + instance.getClass().getName() + ); + } + + liteBuilder.argument(annotation.type(), ArgumentKey.of(annotation.name()), argumentResolver); + } + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java deleted file mode 100644 index a05a4e9..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.imdmk.playtime.injector.processor; - -import com.github.imdmk.playtime.injector.Component; -import com.github.imdmk.playtime.injector.ComponentFunctional; -import org.jetbrains.annotations.NotNull; - -import java.lang.annotation.Annotation; - -public final class FunctionalComponentProcessor - implements ComponentProcessor { - - private final Class annotationType; - private final Class targetType; - private final ComponentFunctional consumer; - - public FunctionalComponentProcessor( - Class annotationType, - Class targetType, - ComponentFunctional consumer - ) { - this.annotationType = annotationType; - this.targetType = targetType; - this.consumer = consumer; - } - - @Override - public @NotNull Class annotation() { - return annotationType; - } - - @Override - @SuppressWarnings("unchecked") - public void process(@NotNull Object instance, @NotNull A annotation, @NotNull ComponentProcessorContext context) { - if (!targetType.isInstance(instance)) { - return; - } - - consumer.accept((T) instance, annotation, context); - } -} - - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorBuilder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorBuilder.java new file mode 100644 index 0000000..71fa564 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorBuilder.java @@ -0,0 +1,40 @@ +package com.github.imdmk.playtime.injector.processor; + +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public final class ProcessorBuilder { + + private final Class annotation; + + private ProcessorHandler handler; + + private ProcessorBuilder(@NotNull Class annotation) { + this.annotation = annotation; + } + + public static ProcessorBuilder forAnnotation(@NotNull Class annotation) { + return new ProcessorBuilder<>(annotation); + } + + @CheckReturnValue + public ProcessorBuilder handle(@NotNull ProcessorHandler handler) { + this.handler = handler; + return this; + } + + public ProcessorContainer build() { + if (handler == null) { + throw new IllegalStateException( + "Processor for @" + annotation.getSimpleName() + " has no handler defined" + ); + } + + return new ProcessorContainer<>( + annotation, + new ComponentProcessorFunctional<>(annotation, handler) + ); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorContainer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorContainer.java new file mode 100644 index 0000000..778f843 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorContainer.java @@ -0,0 +1,10 @@ +package com.github.imdmk.playtime.injector.processor; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public record ProcessorContainer( + @NotNull Class annotationType, + @NotNull ComponentProcessor processor +) {} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorHandler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorHandler.java new file mode 100644 index 0000000..fcc27f7 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ProcessorHandler.java @@ -0,0 +1,13 @@ +package com.github.imdmk.playtime.injector.processor; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +@FunctionalInterface +public interface ProcessorHandler { + + void handle(@NotNull Object instance, @NotNull A annotation, @NotNull ComponentProcessorContext context); + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java index 24d5bc1..4ad59fa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -12,10 +12,10 @@ public final class LocalPublisher implements Publisher { - private final Map, List> subscribers = new HashMap<>(); - private final Injector injector; + private final Map, List> subscribers = new HashMap<>(); + public LocalPublisher(@NotNull Injector injector) { this.injector = injector; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java index 4835b79..989c7b3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java @@ -4,8 +4,8 @@ import com.eternalcode.multification.bukkit.BukkitMultification; import com.eternalcode.multification.notice.provider.NoticeProvider; import com.eternalcode.multification.translation.TranslationProvider; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.adventure.AdventureComponents; @@ -19,7 +19,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -@Service(priority = Priority.NORMAL) +@Service(priority = ComponentPriority.NORMAL) public final class MessageService extends BukkitMultification { private final MessageConfig messageConfig; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java index f8a6203..775e007 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java @@ -1,14 +1,14 @@ package com.github.imdmk.playtime.platform.event; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import org.bukkit.Server; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -@Service(priority = Priority.LOW) +@Service(priority = ComponentPriority.LOW) public final class BukkitEventCaller implements EventCaller { private final Server server; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java index a6a7df3..2772c24 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.platform.gui; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -11,7 +11,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -@Service(priority = Priority.LOW) +@Service(priority = ComponentPriority.LOW) public final class GuiRegistry { private final Map byId = new ConcurrentHashMap<>(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java new file mode 100644 index 0000000..b227e13 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java @@ -0,0 +1,32 @@ +package com.github.imdmk.playtime.platform.litecommands; + +import com.github.imdmk.playtime.injector.ComponentPriority; +import com.github.imdmk.playtime.injector.annotations.Service; +import dev.rollczi.litecommands.LiteCommands; +import dev.rollczi.litecommands.LiteCommandsBuilder; +import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; +import org.bukkit.Server; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@Service(priority = ComponentPriority.LOWEST) +public class LiteCommandsConfigurer { + + private static final String FALLBACK_PREFIX = "AdvancedPlayTime"; + + private final LiteCommandsBuilder liteCommands; + + @Inject + public LiteCommandsConfigurer(@NotNull Plugin plugin, @NotNull Server server) { + this.liteCommands = LiteBukkitFactory.builder(FALLBACK_PREFIX, plugin, server); + } + + public LiteCommandsBuilder builder() { + return liteCommands; + } + + public LiteCommands build() { + return liteCommands.build(); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/InvalidUsageHandlerImpl.java similarity index 90% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/InvalidUsageHandlerImpl.java index 4c13fb1..ab840a1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/InvalidUsageHandlerImpl.java @@ -1,5 +1,6 @@ -package com.github.imdmk.playtime.platform.litecommands; +package com.github.imdmk.playtime.platform.litecommands.handler; +import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; import com.github.imdmk.playtime.message.MessageService; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invalidusage.InvalidUsage; @@ -9,6 +10,7 @@ import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +@LiteHandler(value = CommandSender.class) public final class InvalidUsageHandlerImpl implements InvalidUsageHandler { private final MessageService messageService; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/MissingPermissionsHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/MissingPermissionsHandlerImpl.java similarity index 85% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/MissingPermissionsHandlerImpl.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/MissingPermissionsHandlerImpl.java index a756f41..ae6e77b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/MissingPermissionsHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/MissingPermissionsHandlerImpl.java @@ -1,5 +1,6 @@ -package com.github.imdmk.playtime.platform.litecommands; +package com.github.imdmk.playtime.platform.litecommands.handler; +import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; import com.github.imdmk.playtime.message.MessageService; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invocation.Invocation; @@ -8,6 +9,7 @@ import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +@LiteHandler(value = CommandSender.class) public final class MissingPermissionsHandlerImpl implements MissingPermissionsHandler { private final MessageService messageService; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/NoticeResultHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/NoticeResultHandlerImpl.java similarity index 84% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/NoticeResultHandlerImpl.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/NoticeResultHandlerImpl.java index af51ff9..e75ba6b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/NoticeResultHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/NoticeResultHandlerImpl.java @@ -1,6 +1,7 @@ -package com.github.imdmk.playtime.platform.litecommands; +package com.github.imdmk.playtime.platform.litecommands.handler; import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; import com.github.imdmk.playtime.message.MessageService; import dev.rollczi.litecommands.handler.result.ResultHandler; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; @@ -8,6 +9,7 @@ import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +@LiteHandler(value = Notice.class) public final class NoticeResultHandlerImpl implements ResultHandler { private final MessageService messageService; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java index af34ff7..7977e7b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.platform.metrics; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import org.bstats.bukkit.Metrics; @@ -9,19 +9,19 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -@Service(priority = Priority.LOW) +@Service(priority = ComponentPriority.LOW) public class BMetricsService { private static final int METRICS_ID = 19362; - //private final Metrics metrics; + private final Metrics metrics; @Inject public BMetricsService(@NotNull Plugin plugin) { - //this.metrics = new Metrics(plugin, METRICS_ID); + this.metrics = new Metrics(plugin, METRICS_ID); } @Subscribe(event = PlayTimeShutdownEvent.class) void shutdown() { - //metrics.shutdown(); + metrics.shutdown(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java similarity index 78% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java index 75c8bfe..3426fe6 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java @@ -1,7 +1,9 @@ -package com.github.imdmk.playtime.platform.placeholder.adapter; +package com.github.imdmk.playtime.platform.placeholder; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -12,25 +14,26 @@ import java.util.HashMap; import java.util.Map; -final class PlaceholderAdapterImpl implements PlaceholderAdapter { +@Service +public class PlaceholderService { private final Plugin plugin; private final PluginLogger logger; + private final boolean supports; private final Map expansions = new HashMap<>(); - PlaceholderAdapterImpl(@NotNull Plugin plugin, @NotNull PluginLogger logger) { + public PlaceholderService(@NotNull Plugin plugin, @NotNull PluginLogger logger) { this.plugin = plugin; this.logger = logger; + this.supports = checkSupport(); } - @Override - public boolean isAvailable() { - return true; - } - - @Override public void register(@NotNull PluginPlaceholder placeholder) { + if (!supports) { + return; + } + if (expansions.containsKey(placeholder)) { logger.warn("Placeholder with name %s is already registered!", placeholder.identifier()); return; @@ -42,15 +45,18 @@ public void register(@NotNull PluginPlaceholder placeholder) { } } - @Override public void unregister(@NotNull PluginPlaceholder placeholder) { + if (!supports) { + return; + } + final PlaceholderExpansion expansion = expansions.remove(placeholder); if (expansion != null) { expansion.unregister(); } } - @Override + @Subscribe(event = PlayTimeShutdownEvent.class) public void unregisterAll() { for (final PlaceholderExpansion expansion : expansions.values()) { expansion.unregister(); @@ -59,6 +65,10 @@ public void unregisterAll() { expansions.clear(); } + private boolean checkSupport() { + return plugin.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null; + } + private static final class DelegatingExpansion extends PlaceholderExpansion { private final Plugin plugin; @@ -103,4 +113,3 @@ private DelegatingExpansion(@NotNull Plugin plugin, @NotNull PluginPlaceholder d } } } - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/NoopPlaceholderAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/NoopPlaceholderAdapter.java deleted file mode 100644 index 3b903a1..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/NoopPlaceholderAdapter.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.platform.placeholder.adapter; - -import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; -import org.jetbrains.annotations.NotNull; - -final class NoopPlaceholderAdapter implements PlaceholderAdapter { - - @Override - public boolean isAvailable() { - return false; - } - - @Override - public void register(@NotNull PluginPlaceholder placeholder) {} - - @Override - public void unregister(@NotNull PluginPlaceholder placeholder) {} - - @Override - public void unregisterAll() {} -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java deleted file mode 100644 index 42e8421..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.imdmk.playtime.platform.placeholder.adapter; - -import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; -import org.jetbrains.annotations.NotNull; - -public interface PlaceholderAdapter { - - boolean isAvailable(); - - void register(@NotNull PluginPlaceholder placeholder); - - void unregister(@NotNull PluginPlaceholder placeholder); - - void unregisterAll(); -} - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java deleted file mode 100644 index 35c230f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.imdmk.playtime.platform.placeholder.adapter; - -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import org.bukkit.Server; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; - -public final class PlaceholderAdapterFactory { - - private static final String PLACEHOLDER_API_NAME = "PlaceholderAPI"; - - public static PlaceholderAdapter createFor( - @NotNull Plugin plugin, - @NotNull Server server, - @NotNull PluginLogger logger - ) { - final boolean isEnabled = server.getPluginManager().isPluginEnabled(PLACEHOLDER_API_NAME); - if (isEnabled) { - logger.info("PlaceholderAPI detected — using PlaceholderAdapterImpl."); - return new PlaceholderAdapterImpl(plugin, logger); - } - - logger.info("PlaceholderAPI not found — using NoopPlaceholderAdapter."); - return new NoopPlaceholderAdapter(); - } - - private PlaceholderAdapterFactory() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); - } -} - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java index f0a2c1d..32e63e8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.platform.scheduler; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; @@ -10,7 +10,7 @@ import java.time.Duration; -@Service(priority = Priority.LOWEST) +@Service(priority = ComponentPriority.LOWEST) public final class BukkitTaskScheduler implements TaskScheduler { private static final long MILLIS_PER_TICK = 50L; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java index d349fcb..9a6e100 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.PostConstruct; import java.time.Duration; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java index f01bb83..78d8fd2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.user; +import com.github.imdmk.playtime.injector.annotations.lite.LiteArgument; import com.github.imdmk.playtime.message.MessageConfig; import com.github.imdmk.playtime.platform.logger.PluginLogger; import dev.rollczi.litecommands.argument.Argument; @@ -16,6 +17,7 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +@LiteArgument(type = User.class, name = "user") final class UserArgument extends ArgumentResolver { private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(2); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java index b6b723e..d284d5c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java @@ -2,8 +2,8 @@ import com.github.imdmk.playtime.UserDeleteEvent; import com.github.imdmk.playtime.UserSaveEvent; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.event.BukkitEventCaller; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.user.cache.UserCache; @@ -21,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -@Service(priority = Priority.HIGHEST) +@Service(priority = ComponentPriority.HIGH) final class UserServiceImpl implements UserService { private static final Duration TIMEOUT = Duration.ofSeconds(2L); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java index 2ee9126..730d10b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java @@ -3,8 +3,8 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.user.User; @@ -20,11 +20,11 @@ import java.util.UUID; import java.util.function.Consumer; -@Service(priority = Priority.LOWEST) +@Service(priority = ComponentPriority.LOWEST) public final class CaffeineUserCache implements UserCache { - private static final Duration EXPIRE_AFTER_ACCESS = Duration.ofHours(2); - private static final Duration EXPIRE_AFTER_WRITE = Duration.ofHours(12); + private static final Duration DEFAULT_EXPIRE_AFTER_ACCESS = Duration.ofHours(2); + private static final Duration DEFAULT_EXPIRE_AFTER_WRITE = Duration.ofHours(12); private final Cache cacheByUuid; private final Cache cacheByName; @@ -32,13 +32,13 @@ public final class CaffeineUserCache implements UserCache { @Inject public CaffeineUserCache() { this.cacheByName = Caffeine.newBuilder() - .expireAfterWrite(EXPIRE_AFTER_WRITE) - .expireAfterAccess(EXPIRE_AFTER_ACCESS) + .expireAfterWrite(DEFAULT_EXPIRE_AFTER_WRITE) + .expireAfterAccess(DEFAULT_EXPIRE_AFTER_ACCESS) .build(); this.cacheByUuid = Caffeine.newBuilder() - .expireAfterWrite(EXPIRE_AFTER_WRITE) - .expireAfterAccess(EXPIRE_AFTER_ACCESS) + .expireAfterWrite(DEFAULT_EXPIRE_AFTER_WRITE) + .expireAfterAccess(DEFAULT_EXPIRE_AFTER_ACCESS) .removalListener((UUID key, User user, RemovalCause cause) -> { if (key != null && user != null) { this.cacheByName.invalidate(user.getName()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java index fafaa28..2c1d9fb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java @@ -1,13 +1,13 @@ package com.github.imdmk.playtime.user.repository; import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; import org.jetbrains.annotations.NotNull; -@Service(priority = Priority.LOW) +@Service(priority = ComponentPriority.LOWEST) public final class UserEntityMapper implements EntityMapper { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java index 622d112..ea61d5b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java @@ -13,5 +13,6 @@ interface Col { String NAME = "name"; String PLAYTIME_MILLIS = "playtimeMillis"; + } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java index 96355be..7baa30d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java @@ -3,7 +3,6 @@ import com.github.imdmk.playtime.database.DatabaseBootstrap; import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; import com.github.imdmk.playtime.injector.annotations.Repository; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.github.imdmk.playtime.user.User; @@ -18,7 +17,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -@Repository(priority = Priority.NORMAL) +@Repository public final class UserRepositoryOrmLite extends OrmLiteRepository implements UserRepository { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java index db3a60e..6ca3862 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.user.top; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.repository.UserRepository; @@ -14,7 +14,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -@Service(priority = Priority.NORMAL) +@Service(priority = ComponentPriority.NORMAL) public final class MemoryTopUsersCache implements TopUsersCache { private final PluginLogger logger; From 08318deb4021742aba162deeeee33fd7390c0697 Mon Sep 17 00:00:00 2001 From: imDMK Date: Fri, 9 Jan 2026 17:05:00 +0100 Subject: [PATCH 08/17] Continue work. --- .../playtime/BukkitPlayTimeService.java | 2 +- .../placeholder/PlayTimePlaceholder.java | 18 +-- .../processor/ComponentProcessors.java | 138 +++++++++++------- .../injector/subscriber/LocalPublisher.java | 2 - .../playtime/platform/gui/view/GuiOpener.java | 14 +- .../litecommands/LiteCommandsConfigurer.java | 22 ++- .../platform/metrics/BMetricsService.java | 3 +- .../DisabledPlaceholderRegistry.java | 15 ++ .../placeholder/PapiPlaceholderRegistry.java | 104 +++++++++++++ .../placeholder/PlaceholderRegistry.java | 13 ++ .../PlaceholderRegistryFactory.java | 30 ++++ .../placeholder/PlaceholderService.java | 104 ++----------- .../placeholder/PluginPlaceholder.java | 11 +- .../scheduler/BukkitTaskScheduler.java | 16 +- .../playtime/time/DurationFormatStyle.java | 6 +- .../imdmk/playtime/time/DurationSplitter.java | 6 +- .../imdmk/playtime/time/DurationUnit.java | 10 +- .../github/imdmk/playtime/time/Durations.java | 22 ++- playtime-plugin/build.gradle.kts | 55 +++---- 19 files changed, 350 insertions(+), 241 deletions(-) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/DisabledPlaceholderRegistry.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PapiPlaceholderRegistry.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistry.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistryFactory.java diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java index 4cf2684..fcdf4ff 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java @@ -28,7 +28,7 @@ final class BukkitPlayTimeService implements PlayTimeService { public @NotNull UserTime getTime(@NotNull UUID uuid) { checkPrimaryThread(); - int timeTicks = getOffline(uuid).getStatistic(PLAYTIME_STATISTIC); + final int timeTicks = getOffline(uuid).getStatistic(PLAYTIME_STATISTIC); if (timeTicks <= 0) { return UserTime.ZERO; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java index 5416ee5..42acea8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java @@ -4,13 +4,11 @@ import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import com.github.imdmk.playtime.time.Durations; -import org.bukkit.OfflinePlayer; +import com.github.imdmk.playtime.user.UserTime; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -import java.util.UUID; - @Placeholder public final class PlayTimePlaceholder implements PluginPlaceholder { @@ -27,16 +25,8 @@ public PlayTimePlaceholder(@NotNull PlayTimeService playtimeService) { } @Override - public @NotNull String onRequest(@NotNull Player player, @NotNull String params) { - return formatUserTime(player.getUniqueId()); - } - - @Override - public @NotNull String onRequest(@NotNull OfflinePlayer player, @NotNull String params) { - return formatUserTime(player.getUniqueId()); - } - - private @NotNull String formatUserTime(@NotNull UUID uuid) { - return Durations.format(playtimeService.getTime(uuid).toDuration()); + public @NotNull String request(@NotNull Player player, @NotNull String params) { + final UserTime time = playtimeService.getTime(player.getUniqueId()); + return Durations.format(time.toDuration()); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java index e67d476..94e5c5c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -13,6 +13,10 @@ import com.github.imdmk.playtime.injector.annotations.lite.LiteArgument; import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; +import com.github.imdmk.playtime.platform.gui.GuiRegistry; +import com.github.imdmk.playtime.platform.gui.IdentifiableGui; +import com.github.imdmk.playtime.platform.placeholder.PlaceholderService; +import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import dev.rollczi.litecommands.LiteCommandsBuilder; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.argument.ArgumentKey; @@ -55,12 +59,9 @@ public static List> defaults(@NotNull Plugin plugin) { ProcessorBuilder.forAnnotation(Database.class) .handle((instance, annotation, ctx) -> { - if (!(instance instanceof DatabaseBootstrap databaseBootstrap)) { - throw new IllegalStateException("@Database can only be used on with DatabaseBootstrap class"); - } + final DatabaseBootstrap databaseBootstrap = requireInstance(instance, DatabaseBootstrap.class, Database.class); databaseBootstrap.start(); - ctx.injector().getResources() .on(DatabaseBootstrap.class) .assignInstance(databaseBootstrap); @@ -69,12 +70,9 @@ public static List> defaults(@NotNull Plugin plugin) { ProcessorBuilder.forAnnotation(Repository.class) .handle((instance, annotation, ctx) -> { - if (!(instance instanceof OrmLiteRepository repository)) { - return; - } + final OrmLiteRepository repository = requireInstance(instance, OrmLiteRepository.class, OrmLiteRepository.class); repository.start(); - ctx.injector().getResources() .on(repository.getClass()) .assignInstance(repository); @@ -83,13 +81,8 @@ public static List> defaults(@NotNull Plugin plugin) { ProcessorBuilder.forAnnotation(Controller.class) .handle((instance, annotation, ctx) -> { - if (!(instance instanceof Listener listener)) { - throw new IllegalStateException( - "@Controller must implement Listener: " - + instance.getClass().getName() - ); - } - + final Listener listener = requireInstance(instance, Listener.class, Controller.class); + System.out.println("registered listener: " + listener.getClass().getName()); plugin.getServer() .getPluginManager() .registerEvents(listener, plugin); @@ -97,11 +90,11 @@ public static List> defaults(@NotNull Plugin plugin) { .build(), ProcessorBuilder.forAnnotation(Gui.class) - .handle() + .handle((instance, annotation, ctx) -> ctx.injector().newInstance(GuiProcessor.class).process(instance, annotation, ctx)) .build(), ProcessorBuilder.forAnnotation(Placeholder.class) - .handle() + .handle((instance, annotation, ctx) -> ctx.injector().newInstance(PlaceholderProcessor.class).process(instance, annotation, ctx)) .build(), ProcessorBuilder.forAnnotation(Command.class) @@ -119,9 +112,44 @@ public static List> defaults(@NotNull Plugin plugin) { ); } + static T requireInstance( + Object instance, + Class expectedType, + Class annotation + ) { + if (!expectedType.isInstance(instance)) { + throw new IllegalStateException( + "@" + annotation.getSimpleName() + + " can only be used on " + + expectedType.getSimpleName() + + ": " + instance.getClass().getName() + ); + } + + return expectedType.cast(instance); + } + + @Inject + private record GuiProcessor(@NotNull GuiRegistry guiRegistry) + implements ComponentProcessor { + + @Override + public void process(@NotNull Object instance, @NotNull Gui annotation, @NotNull ComponentProcessorContext context) { + final IdentifiableGui identifiableGui = requireInstance(instance, IdentifiableGui.class, Gui.class); + System.out.println("registering gui: " + identifiableGui.getClass().getName()); + guiRegistry.register(identifiableGui); + } + + @NotNull + @Override + public Class annotation() { + return Gui.class; + } + } + @Inject private record ConfigFileProcessor(@NotNull ConfigService configService) - implements ComponentProcessor { + implements ComponentProcessor { @Override public void process( @@ -129,12 +157,7 @@ public void process( @NotNull ConfigFile annotation, @NotNull ComponentProcessorContext context ) { - if (!(instance instanceof ConfigSection configSection)) { - throw new IllegalStateException( - "@ConfigFile can only be used on ConfigSection: " - + instance.getClass().getName() - ); - } + final ConfigSection configSection = requireInstance(instance, ConfigSection.class, ConfigFile.class); configService.create(configSection.getClass()); context.injector().getResources() @@ -149,17 +172,27 @@ public Class annotation() { } } - private record - @Inject - private record LiteHandlerProcessor(@NotNull LiteCommandsBuilder liteBuilder) - implements ComponentProcessor { + private record PlaceholderProcessor(@NotNull PlaceholderService placeholderService) + implements ComponentProcessor { @Override - public @NotNull Class annotation() { - return LiteHandler.class; + public void process(@NotNull Object instance, @NotNull Placeholder annotation, @NotNull ComponentProcessorContext context) { + final PluginPlaceholder pluginPlaceholder = requireInstance(instance, PluginPlaceholder.class, Placeholder.class); + placeholderService.register(pluginPlaceholder); } + @NotNull + @Override + public Class annotation() { + return Placeholder.class; + } + } + + @Inject + private record LiteHandlerProcessor(@NotNull LiteCommandsBuilder liteBuilder) + implements ComponentProcessor { + @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public void process( @@ -167,14 +200,15 @@ public void process( @NotNull LiteHandler annotation, @NotNull ComponentProcessorContext context ) { - if (!(instance instanceof ResultHandler handler)) { - throw new IllegalStateException( - "@LiteHandler can only be used on ResultHandler: " - + instance.getClass().getName() - ); - } + final ResultHandler resultHandler = requireInstance(instance, ResultHandler.class, LiteHandler.class); + System.out.println("registering result handler: " + resultHandler.getClass().getName()); + liteBuilder.result(annotation.value(), resultHandler); + } - liteBuilder.result(annotation.value(), handler); + @NotNull + @Override + public Class annotation() { + return LiteHandler.class; } } @@ -182,11 +216,6 @@ public void process( private record LiteCommandProcessor(@NotNull LiteCommandsBuilder liteBuilder) implements ComponentProcessor { - @Override - public @NotNull Class annotation() { - return Command.class; - } - @Override public void process( @NotNull Object instance, @@ -195,17 +224,18 @@ public void process( ) { liteBuilder.commands(instance); } + + @NotNull + @Override + public Class annotation() { + return Command.class; + } } @Inject private record LiteArgumentProcessor(@NotNull LiteCommandsBuilder liteBuilder) implements ComponentProcessor { - @Override - public @NotNull Class annotation() { - return LiteArgument.class; - } - @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public void process( @@ -213,15 +243,15 @@ public void process( @NotNull LiteArgument annotation, @NotNull ComponentProcessorContext context ) { - if (!(instance instanceof ArgumentResolver argumentResolver)) { - throw new IllegalStateException( - "@LiteArgument can only be used on ArgumentResolver: " - + instance.getClass().getName() - ); - } - + final ArgumentResolver argumentResolver = requireInstance(instance, ArgumentResolver.class, LiteArgument.class); liteBuilder.argument(annotation.type(), ArgumentKey.of(annotation.name()), argumentResolver); } + + @NotNull + @Override + public Class annotation() { + return LiteArgument.class; + } } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java index 4ad59fa..27bb088 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -31,8 +31,6 @@ public void subscribe(@NotNull Object instance) { final Class eventType = subscribe.event(); method.setAccessible(true); - System.out.println("published class: " + instance.getClass().getName() + "method: " + method.getName() + "event: " + eventType.getName()); - subscribers .computeIfAbsent(eventType, k -> new ArrayList<>()) .add(new SubscriberMethod(instance, method)); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java index 040e6f3..d625b14 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java @@ -13,12 +13,12 @@ public final class GuiOpener { private final GuiRegistry registry; - private final TaskScheduler taskScheduler; + private final TaskScheduler scheduler; @Inject - public GuiOpener(@NotNull GuiRegistry registry, @NotNull TaskScheduler taskScheduler) { + public GuiOpener(@NotNull GuiRegistry registry, @NotNull TaskScheduler scheduler) { this.registry = registry; - this.taskScheduler = taskScheduler; + this.scheduler = scheduler; } public void open( @@ -32,7 +32,7 @@ public void open( final BaseGui baseGui = simpleGui.createGui(); simpleGui.prepareItems(baseGui, viewer); - taskScheduler.runSync(() -> baseGui.open(viewer)); + scheduler.runSync(() -> baseGui.open(viewer)); } @SuppressWarnings("unchecked") @@ -50,7 +50,7 @@ public void open( final BaseGui baseGui = typed.createGui(viewer, parameter); typed.prepareItems(baseGui, viewer, parameter); - taskScheduler.runSync(() -> baseGui.open(viewer)); + scheduler.runSync(() -> baseGui.open(viewer)); } public void open( @@ -65,7 +65,7 @@ public void open( final BaseGui baseGui = simpleGui.createGui(); simpleGui.prepareItems(baseGui, viewer); - taskScheduler.runSync(() -> baseGui.open(viewer)); + scheduler.runSync(() -> baseGui.open(viewer)); } @SuppressWarnings("unchecked") @@ -83,7 +83,7 @@ public void open( final BaseGui baseGui = typed.createGui(viewer, parameter); typed.prepareItems(baseGui, viewer, parameter); - taskScheduler.runSync(() -> baseGui.open(viewer)); + scheduler.runSync(() -> baseGui.open(viewer)); } private IdentifiableGui require(String id) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java index b227e13..4d2563f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java @@ -2,6 +2,8 @@ import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeInitializeEvent; import dev.rollczi.litecommands.LiteCommands; import dev.rollczi.litecommands.LiteCommandsBuilder; import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; @@ -11,22 +13,32 @@ import org.panda_lang.utilities.inject.annotations.Inject; @Service(priority = ComponentPriority.LOWEST) -public class LiteCommandsConfigurer { +public final class LiteCommandsConfigurer { private static final String FALLBACK_PREFIX = "AdvancedPlayTime"; - private final LiteCommandsBuilder liteCommands; + private final LiteCommandsBuilder builder; + private LiteCommands liteCommands; @Inject public LiteCommandsConfigurer(@NotNull Plugin plugin, @NotNull Server server) { - this.liteCommands = LiteBukkitFactory.builder(FALLBACK_PREFIX, plugin, server); + this.builder = LiteBukkitFactory.builder(FALLBACK_PREFIX, plugin, server); } public LiteCommandsBuilder builder() { + return builder; + } + + public LiteCommands liteCommands() { + if (liteCommands == null) { + throw new IllegalStateException("LiteCommands not initialized yet"); + } return liteCommands; } - public LiteCommands build() { - return liteCommands.build(); + @Subscribe(event = PlayTimeInitializeEvent.class) + private void onInitialize() { + this.liteCommands = builder.build(); } } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java index 7977e7b..4c00ebf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java @@ -9,10 +9,11 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -@Service(priority = ComponentPriority.LOW) +@Service public class BMetricsService { private static final int METRICS_ID = 19362; + private final Metrics metrics; @Inject diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/DisabledPlaceholderRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/DisabledPlaceholderRegistry.java new file mode 100644 index 0000000..182d54a --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/DisabledPlaceholderRegistry.java @@ -0,0 +1,15 @@ +package com.github.imdmk.playtime.platform.placeholder; + +import org.jetbrains.annotations.NotNull; + +final class DisabledPlaceholderRegistry implements PlaceholderRegistry { + + @Override + public void register(@NotNull PluginPlaceholder placeholder) {} + + @Override + public void unregister(@NotNull PluginPlaceholder placeholder) {} + + @Override + public void unregisterAll() {} +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PapiPlaceholderRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PapiPlaceholderRegistry.java new file mode 100644 index 0000000..8d8f138 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PapiPlaceholderRegistry.java @@ -0,0 +1,104 @@ +package com.github.imdmk.playtime.platform.placeholder; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +final class PapiPlaceholderRegistry implements PlaceholderRegistry { + + private final Plugin plugin; + private final Map expansions = new HashMap<>(); + + PapiPlaceholderRegistry(@NotNull Plugin plugin) { + this.plugin = plugin; + } + + @Override + public void register(@NotNull PluginPlaceholder placeholder) { + final String id = normalizeId(placeholder.identifier()); + if (expansions.containsKey(id)) { + throw new IllegalStateException("Placeholder with id " + id + " is already registered!"); + } + + final PlaceholderExpansion expansion = new PlaceholderExpansionAdapter(plugin, placeholder); + if (expansion.register()) { + expansions.put(id, expansion); + } + } + + @Override + public void unregister(@NotNull PluginPlaceholder placeholder) { + final String id = normalizeId(placeholder.identifier()); + + final PlaceholderExpansion expansion = expansions.remove(id); + if (expansion != null) { + expansion.unregister(); + } + } + + @Override + public void unregisterAll() { + for (final PlaceholderExpansion expansion : expansions.values()) { + expansion.unregister(); + } + + expansions.clear(); + } + + private String normalizeId(String rawId) { + final String id = rawId.trim().toLowerCase(Locale.ROOT); + if (id.isEmpty()) { + throw new IllegalArgumentException("Placeholder identifier cannot be empty"); + } + + return id; + } + + private static final class PlaceholderExpansionAdapter extends PlaceholderExpansion { + + private final Plugin plugin; + private final PluginPlaceholder delegate; + + private PlaceholderExpansionAdapter(@NotNull Plugin plugin, @NotNull PluginPlaceholder delegate) { + this.plugin = plugin; + this.delegate = delegate; + } + + @Override + @NotNull + public String getIdentifier() { + return delegate.identifier(); + } + + @Override + @NotNull + public String getAuthor() { + return String.join(", ", plugin.getDescription().getAuthors()); + } + + @Override + @NotNull + public String getVersion() { + return plugin.getDescription().getVersion(); + } + + @Override + public String onPlaceholderRequest(Player player, @NotNull String params) { + if (player == null) { + return null; + } + + return delegate.request(player, params); + } + + @Override + public boolean persist() { + return true; + } + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistry.java new file mode 100644 index 0000000..c53a314 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistry.java @@ -0,0 +1,13 @@ +package com.github.imdmk.playtime.platform.placeholder; + +import org.jetbrains.annotations.NotNull; + +public interface PlaceholderRegistry { + + void register(@NotNull PluginPlaceholder placeholder); + + void unregister(@NotNull PluginPlaceholder placeholder); + + void unregisterAll(); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistryFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistryFactory.java new file mode 100644 index 0000000..99f252a --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderRegistryFactory.java @@ -0,0 +1,30 @@ +package com.github.imdmk.playtime.platform.placeholder; + +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +final class PlaceholderRegistryFactory { + + private static final String PAPI_PLUGIN_NAME = "PlaceholderAPI"; + + PlaceholderRegistryFactory() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } + + static PlaceholderRegistry create( + @NotNull Plugin plugin, + @NotNull PluginLogger logger + ) { + final Plugin papi = plugin.getServer() + .getPluginManager() + .getPlugin(PAPI_PLUGIN_NAME); + + if (papi == null) { + return new DisabledPlaceholderRegistry(); + } + + logger.info("Founded PlaceholderAPI plugin! Enabling integration..."); + return new PapiPlaceholderRegistry(plugin); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java index 3426fe6..8f0c7a4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java @@ -1,115 +1,37 @@ package com.github.imdmk.playtime.platform.placeholder; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import me.clip.placeholderapi.expansion.PlaceholderExpansion; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.HashMap; -import java.util.Map; +import org.panda_lang.utilities.inject.annotations.Inject; @Service -public class PlaceholderService { - - private final Plugin plugin; - private final PluginLogger logger; - private final boolean supports; +public final class PlaceholderService { - private final Map expansions = new HashMap<>(); + private final PlaceholderRegistry registry; - public PlaceholderService(@NotNull Plugin plugin, @NotNull PluginLogger logger) { - this.plugin = plugin; - this.logger = logger; - this.supports = checkSupport(); + @Inject + public PlaceholderService( + @NotNull Plugin plugin, + @NotNull PluginLogger logger + ) { + this.registry = PlaceholderRegistryFactory.create(plugin, logger); } public void register(@NotNull PluginPlaceholder placeholder) { - if (!supports) { - return; - } - - if (expansions.containsKey(placeholder)) { - logger.warn("Placeholder with name %s is already registered!", placeholder.identifier()); - return; - } - - final PlaceholderExpansion expansion = new DelegatingExpansion(plugin, placeholder); - if (expansion.register()) { - expansions.put(placeholder, expansion); - } + registry.register(placeholder); } public void unregister(@NotNull PluginPlaceholder placeholder) { - if (!supports) { - return; - } - - final PlaceholderExpansion expansion = expansions.remove(placeholder); - if (expansion != null) { - expansion.unregister(); - } + registry.unregister(placeholder); } @Subscribe(event = PlayTimeShutdownEvent.class) public void unregisterAll() { - for (final PlaceholderExpansion expansion : expansions.values()) { - expansion.unregister(); - } - - expansions.clear(); - } - - private boolean checkSupport() { - return plugin.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null; - } - - private static final class DelegatingExpansion extends PlaceholderExpansion { - - private final Plugin plugin; - private final PluginPlaceholder delegate; - - private DelegatingExpansion(@NotNull Plugin plugin, @NotNull PluginPlaceholder delegate) { - this.plugin = plugin; - this.delegate = delegate; - } - - @Override - public @NotNull String getIdentifier() { - return delegate.identifier(); - } - - @Override - public @NotNull String getAuthor() { - return String.join(", ", plugin.getDescription().getAuthors()); - } - - @Override - public @NotNull String getVersion() { - return plugin.getDescription().getVersion(); - } - - @Override - public @Nullable String onPlaceholderRequest(Player player, @NotNull String params) { - if (player == null) { - return null; - } - - return delegate.onRequest(player, params); - } - - @Override - public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) { - if (player == null) { - return null; - } - - return delegate.onRequest(player, params); - } + registry.unregisterAll(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java index 3f4afda..c245c49 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java @@ -1,19 +1,12 @@ package com.github.imdmk.playtime.platform.placeholder; -import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public interface PluginPlaceholder { - @NotNull String identifier(); + String identifier(); - default @Nullable String onRequest(@NotNull Player player, @NotNull String params) { - return null; - } + String request(@NotNull Player player, @NotNull String params); - default @Nullable String onRequest(@NotNull OfflinePlayer player, @NotNull String params) { - return null; - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java index 32e63e8..a003024 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java @@ -2,6 +2,7 @@ import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.time.Durations; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; @@ -13,8 +14,6 @@ @Service(priority = ComponentPriority.LOWEST) public final class BukkitTaskScheduler implements TaskScheduler { - private static final long MILLIS_PER_TICK = 50L; - private final Plugin plugin; private final BukkitScheduler scheduler; @@ -39,7 +38,7 @@ public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler sche @NotNull Runnable runnable, @NotNull Duration delay ) { - return scheduler.runTaskLaterAsynchronously(plugin, runnable, toTicks(delay)); + return scheduler.runTaskLaterAsynchronously(plugin, runnable, Durations.convertToTicks(delay)); } @Override @@ -47,7 +46,7 @@ public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler sche @NotNull Runnable runnable, @NotNull Duration delay ) { - return scheduler.runTaskLater(plugin, runnable, toTicks(delay)); + return scheduler.runTaskLater(plugin, runnable, Durations.convertToTicks(delay)); } @Override @@ -56,7 +55,7 @@ public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler sche @NotNull Duration delay, @NotNull Duration period ) { - return scheduler.runTaskTimer(plugin, runnable, toTicks(delay), toTicks(period)); + return scheduler.runTaskTimer(plugin, runnable, Durations.convertToTicks(delay), Durations.convertToTicks(period)); } @Override @@ -65,7 +64,7 @@ public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler sche @NotNull Duration delay, @NotNull Duration period ) { - return scheduler.runTaskTimerAsynchronously(plugin, runnable, toTicks(delay), toTicks(period)); + return scheduler.runTaskTimerAsynchronously(plugin, runnable, Durations.convertToTicks(delay), Durations.convertToTicks(period)); } @Override @@ -77,9 +76,4 @@ public void cancelTask(int taskId) { public void cancelAllTasks() { scheduler.cancelTasks(plugin); } - - private static int toTicks(Duration duration) { - long ticks = duration.toMillis() / MILLIS_PER_TICK; - return ticks <= 0 ? 0 : (int) ticks; - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java index e90c150..19a442a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java @@ -44,7 +44,7 @@ public String format(@NotNull Duration duration) { public abstract String format(@NotNull Duration duration); - protected static String formatWith( + static String formatWith( @NotNull Duration duration, @NotNull BiFunction valueFormatter, @NotNull Separator separator @@ -56,7 +56,7 @@ protected static String formatWith( .collect(Collectors.joining(separator.value())); } - protected enum Separator { + enum Separator { SPACE(" "), AND(" and "), @@ -68,7 +68,7 @@ protected enum Separator { this.value = value; } - public String value() { + String value() { return value; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java index b25ffde..c361463 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java @@ -6,13 +6,13 @@ import java.util.EnumMap; import java.util.Map; -public final class DurationSplitter { +final class DurationSplitter { - private DurationSplitter() { + DurationSplitter() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - public static Map split(@NotNull Duration duration) { + static Map split(@NotNull Duration duration) { final EnumMap parts = new EnumMap<>(DurationUnit.class); for (final DurationUnit unit : DurationUnit.ORDERED) { parts.put(unit, unit.extract(duration)); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java index ef6638a..cb3bea7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java @@ -4,7 +4,7 @@ import java.time.Duration; -public enum DurationUnit { +enum DurationUnit { DAY("day", "days", "d") { @Override @@ -31,7 +31,7 @@ public int extract(@NotNull Duration duration) { } }; - protected static final DurationUnit[] ORDERED = { + static final DurationUnit[] ORDERED = { DAY, HOUR, MINUTE, SECOND }; @@ -47,13 +47,13 @@ public int extract(@NotNull Duration duration) { this.abbreviation = abbreviation; } - public abstract int extract(@NotNull Duration duration); + abstract int extract(@NotNull Duration duration); - public String getAbbreviation() { + String getAbbreviation() { return abbreviation; } - protected String toDisplayName(int value) { + String toDisplayName(int value) { final String word = (value == 1 ? singular : plural); return DISPLAY_NAME_FORMAT.formatted(value, word); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java index 9a6e100..4d42879 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.PostConstruct; import java.time.Duration; @@ -9,16 +8,14 @@ public final class Durations { private static final Duration MAX_NORMALIZED_DURATION = Duration.ofDays(3650); private static final String LESS_THAN_SECOND = "<1s"; - private static DurationFormatStyle DEFAULT_FORMAT_STYLE = DurationFormatStyle.NATURAL; + private static final long MILLIS_PER_TICK = 50L; + + private static DurationFormatStyle FORMAT_STYLE = DurationFormatStyle.NATURAL; private Durations() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - public static String format(@NotNull Duration duration) { - return format(duration, DEFAULT_FORMAT_STYLE); - } - public static String format(@NotNull Duration duration, @NotNull DurationFormatStyle style) { if (duration.isZero() || duration.isNegative()) { return LESS_THAN_SECOND; @@ -27,8 +24,8 @@ public static String format(@NotNull Duration duration, @NotNull DurationFormatS return style.format(duration); } - public static void setDefaultFormatStyle(@NotNull DurationFormatStyle style) { - DEFAULT_FORMAT_STYLE = style; + public static String format(@NotNull Duration duration) { + return format(duration, FORMAT_STYLE); } public static Duration clamp(@NotNull Duration input) { @@ -38,4 +35,13 @@ public static Duration clamp(@NotNull Duration input) { return input.compareTo(MAX_NORMALIZED_DURATION) > 0 ? MAX_NORMALIZED_DURATION : input; } + + public static int convertToTicks(@NotNull Duration duration) { + long ticks = duration.toMillis() / MILLIS_PER_TICK; + return ticks <= 0 ? 0 : (int) ticks; + } + + public static void setFormatStyle(@NotNull DurationFormatStyle style) { + FORMAT_STYLE = style; + } } diff --git a/playtime-plugin/build.gradle.kts b/playtime-plugin/build.gradle.kts index c513a23..32f667d 100644 --- a/playtime-plugin/build.gradle.kts +++ b/playtime-plugin/build.gradle.kts @@ -28,33 +28,34 @@ tasks.withType { "org/jetbrains/annotations/**" ) -// val relocationPrefix = "com.github.imdmk.playtime.lib" -// listOf( -// "com.alessiodp.libby", -// "com.eternalcode.multification", -// "com.github.benmanes.caffeine", -// "com.google.errorprone", -// "com.google.gson", -// "com.j256.ormlite", -// "com.zaxxer.hikari", -// "dev.rollczi.litecommands", -// "dev.triumphteam.gui", -// "eu.okaeri.configs", -// "javassist", -// "net.kyori", -// "org.bstats", -// "org.jspecify.annotations", -// "org.panda_lang.utilities", -// "org.yaml.snakeyaml", -// "panda.std", -// "panda.utilities" -// ).forEach { pkg -> -// relocate(pkg, "$relocationPrefix.$pkg") -// } -// -// minimize { -// exclude(dependency("com.github.ben-manes.caffeine:caffeine")) -// } + val relocationPrefix = "com.github.imdmk.playtime.lib" + listOf( + "com.alessiodp.libby", + "com.eternalcode.multification", + "com.github.benmanes.caffeine", + "com.google.errorprone", + "com.google.gson", + "com.j256.ormlite", + "com.zaxxer.hikari", + "dev.rollczi.litecommands", + "dev.triumphteam.gui", + "eu.okaeri.configs", + "javassist", + "net.kyori", + "org.bstats", + "org.jspecify.annotations", + "org.panda_lang.utilities", + "org.yaml.snakeyaml", + "panda.std", + "panda.utilities" + ).forEach { pkg -> + relocate(pkg, "$relocationPrefix.$pkg") + } + + minimize { + exclude { dependency -> dependency.moduleGroup == "com.github.imdmk.playtime" } + exclude(dependency("com.github.ben-manes.caffeine:caffeine")) + } } bukkit { From 44baa9373e69d7278fa01c8de49d1f09bdf51e47 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sat, 10 Jan 2026 14:30:37 +0100 Subject: [PATCH 09/17] Fix DI processors. --- .../feature/playtime/command/TimeCommand.java | 2 + .../playtime/command/TimeResetAllCommand.java | 2 + .../playtime/command/TimeResetCommand.java | 2 + .../playtime/command/TimeSetCommand.java | 2 + .../playtime/command/TimeTopCommand.java | 2 + .../command/TimeTopInvalidateCommand.java | 2 + .../feature/playtime/gui/PlayTimeTopGui.java | 5 +- .../playtime/gui/PlayTimeTopGuiConfig.java | 4 +- .../playtime/injector/ComponentManager.java | 2 +- .../playtime/injector/annotations/Gui.java | 2 +- .../annotations/lite/LiteArgument.java | 5 +- .../annotations/lite/LiteCommand.java | 16 +++++ .../annotations/lite/LiteHandler.java | 4 ++ .../processor/ComponentProcessors.java | 72 ++++++++++++++----- .../playtime/message/MessageService.java | 2 +- .../playtime/platform/gui/GuiRegistry.java | 40 +++++------ .../platform/gui/IdentifiableGui.java | 1 - .../imdmk/playtime/user/UserArgument.java | 2 +- playtime-plugin/build.gradle.kts | 8 +-- 19 files changed, 122 insertions(+), 53 deletions(-) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteCommand.java diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java index 8b49e50..8e5e55c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.feature.playtime.command; import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.time.Durations; import com.github.imdmk.playtime.user.User; @@ -15,6 +16,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; +@LiteCommand @Command(name = "playtime") public final class TimeCommand { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java index b41f98e..fc1708d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.feature.playtime.command; import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; @@ -22,6 +23,7 @@ import java.util.Arrays; import java.util.concurrent.CompletableFuture; +@LiteCommand @Command(name = "playtime reset-all") @Permission("command.playtime.reset.all") public final class TimeResetAllCommand { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java index c230f07..2ab4a51 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.feature.playtime.command; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.user.User; @@ -16,6 +17,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; +@LiteCommand @Command(name = "playtime reset") @Permission("command.playtime.reset") public final class TimeResetCommand { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java index d56efe6..c1658a9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.feature.playtime.command; import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.time.Durations; @@ -20,6 +21,7 @@ import java.time.Duration; +@LiteCommand @Command(name = "playtime set") @Permission("command.playtime.set") public final class TimeSetCommand { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java index 0ac644a..58627b3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.feature.playtime.command; import com.github.imdmk.playtime.feature.playtime.gui.PlayTimeTopGui; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.gui.view.GuiOpener; import com.github.imdmk.playtime.platform.logger.PluginLogger; @@ -13,6 +14,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; +@LiteCommand @Command(name = "playtime top") @Permission("command.playtime.top") public final class TimeTopCommand { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java index 97dde2e..c184b93 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.feature.playtime.command; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.user.top.TopUsersCache; import dev.rollczi.litecommands.annotations.command.Command; @@ -10,6 +11,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; +@LiteCommand @Command(name = "playtime top invalidate") @Permission("command.playtime.top.invalidate") public final class TimeTopInvalidateCommand { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java index 5b2e9d1..ea68c56 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.feature.playtime.gui; +import com.github.imdmk.playtime.injector.annotations.Gui; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.adventure.AdventureFormatter; import com.github.imdmk.playtime.platform.adventure.AdventurePlaceholders; @@ -35,12 +36,12 @@ import java.util.List; import java.util.function.Consumer; +@Gui public final class PlayTimeTopGui extends AbstractGui implements ParameterizedGui> { private static final String GUI_IDENTIFIER = "playtime-top"; - private static final UserSaveReason SAVE_REASON = UserSaveReason.GUI_RESET_CLICK; private static final GuiRenderer GUI_RENDERER = TriumphGuiRenderer.newRenderer(); private static final RenderOptions RENDER_OPTIONS = RenderOptions.defaultHide(); @@ -103,7 +104,7 @@ public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull gui.close(viewer); user.setPlaytime(UserTime.ZERO); - userService.save(user, SAVE_REASON) + userService.save(user, UserSaveReason.GUI_RESET_CLICK) .thenAccept(result -> messageService.send(viewer, n -> n.playtimeMessages.playerPlaytimeReset())) .exceptionally(e -> { messageService.send(viewer, n -> n.actionExecutionError); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java index fd0bab0..920203b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java @@ -14,7 +14,9 @@ import java.util.Collections; -public final class PlayTimeTopGuiConfig extends OkaeriConfig implements ConfigurableGui { +public final class PlayTimeTopGuiConfig + extends OkaeriConfig + implements ConfigurableGui { @Comment({ "#", diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java index 1a4427c..63bd7fb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -49,7 +49,7 @@ public ComponentManager addPostProcessors(@NotNull List public void scanAll() { for (final ProcessorContainer container : processors) { - components.addAll(scanner.scan(container.processor().annotation())); + components.addAll(scanner.scan(container.annotationType())); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Gui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Gui.java index 9065d26..1daae9a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Gui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Gui.java @@ -11,6 +11,6 @@ @Target(ElementType.TYPE) public @interface Gui { - ComponentPriority priority() default ComponentPriority.LOW; + ComponentPriority priority() default ComponentPriority.HIGHEST; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java index 9c997af..bd5ce2f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java @@ -1,5 +1,7 @@ package com.github.imdmk.playtime.injector.annotations.lite; +import com.github.imdmk.playtime.injector.ComponentPriority; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -11,6 +13,7 @@ Class type(); - String name(); + String key() default ""; + ComponentPriority priority() default ComponentPriority.HIGHEST; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteCommand.java new file mode 100644 index 0000000..9b46b17 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteCommand.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations.lite; + +import com.github.imdmk.playtime.injector.ComponentPriority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LiteCommand { + + ComponentPriority priority() default ComponentPriority.HIGHEST; +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java index 0208b82..b996322 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java @@ -1,5 +1,7 @@ package com.github.imdmk.playtime.injector.annotations.lite; +import com.github.imdmk.playtime.injector.ComponentPriority; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -10,4 +12,6 @@ public @interface LiteHandler { Class value(); + + ComponentPriority priority() default ComponentPriority.HIGHEST; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java index 94e5c5c..8031b99 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -11,27 +11,35 @@ import com.github.imdmk.playtime.injector.annotations.Repository; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.annotations.lite.LiteArgument; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; import com.github.imdmk.playtime.platform.gui.GuiRegistry; import com.github.imdmk.playtime.platform.gui.IdentifiableGui; +import com.github.imdmk.playtime.platform.litecommands.LiteCommandsConfigurer; import com.github.imdmk.playtime.platform.placeholder.PlaceholderService; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import dev.rollczi.litecommands.LiteCommandsBuilder; -import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.argument.ArgumentKey; import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; import dev.rollczi.litecommands.handler.result.ResultHandler; +import dev.rollczi.litecommands.invalidusage.InvalidUsageHandler; +import dev.rollczi.litecommands.permission.MissingPermissionsHandler; import org.bukkit.event.Listener; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Resources; import org.panda_lang.utilities.inject.annotations.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.reflect.Field; import java.util.List; public final class ComponentProcessors { + private static final Logger log = LoggerFactory.getLogger(ComponentProcessors.class); + private ComponentProcessors() { throw new UnsupportedOperationException("This is utility class and cannot be instantiated."); } @@ -82,7 +90,6 @@ public static List> defaults(@NotNull Plugin plugin) { ProcessorBuilder.forAnnotation(Controller.class) .handle((instance, annotation, ctx) -> { final Listener listener = requireInstance(instance, Listener.class, Controller.class); - System.out.println("registered listener: " + listener.getClass().getName()); plugin.getServer() .getPluginManager() .registerEvents(listener, plugin); @@ -97,7 +104,7 @@ public static List> defaults(@NotNull Plugin plugin) { .handle((instance, annotation, ctx) -> ctx.injector().newInstance(PlaceholderProcessor.class).process(instance, annotation, ctx)) .build(), - ProcessorBuilder.forAnnotation(Command.class) + ProcessorBuilder.forAnnotation(LiteCommand.class) .handle((instance, annotation, ctx) -> ctx.injector().newInstance(LiteCommandProcessor.class).process(instance, annotation, ctx)) .build(), @@ -136,7 +143,6 @@ private record GuiProcessor(@NotNull GuiRegistry guiRegistry) @Override public void process(@NotNull Object instance, @NotNull Gui annotation, @NotNull ComponentProcessorContext context) { final IdentifiableGui identifiableGui = requireInstance(instance, IdentifiableGui.class, Gui.class); - System.out.println("registering gui: " + identifiableGui.getClass().getName()); guiRegistry.register(identifiableGui); } @@ -157,12 +163,23 @@ public void process( @NotNull ConfigFile annotation, @NotNull ComponentProcessorContext context ) { - final ConfigSection configSection = requireInstance(instance, ConfigSection.class, ConfigFile.class); + final Resources resources = context.injector().getResources(); + final ConfigSection config = requireInstance(instance, ConfigSection.class, ConfigFile.class); - configService.create(configSection.getClass()); - context.injector().getResources() - .on(instance.getClass()) + configService.create(config.getClass()); + resources.on(instance.getClass()) .assignInstance(instance); + + for (final Field field : config.getClass().getFields()) { + try { + final Object value = field.get(config); + if (value != null) { + resources.on(field.getType()) + .assignInstance(value); + } + } + catch (IllegalAccessException ignored) {} + } } @Override @@ -190,7 +207,7 @@ public Class annotation() { } @Inject - private record LiteHandlerProcessor(@NotNull LiteCommandsBuilder liteBuilder) + private record LiteHandlerProcessor(@NotNull LiteCommandsConfigurer liteCommandsConfigurer) implements ComponentProcessor { @Override @@ -200,9 +217,21 @@ public void process( @NotNull LiteHandler annotation, @NotNull ComponentProcessorContext context ) { + + final LiteCommandsBuilder builder = liteCommandsConfigurer.builder(); final ResultHandler resultHandler = requireInstance(instance, ResultHandler.class, LiteHandler.class); - System.out.println("registering result handler: " + resultHandler.getClass().getName()); - liteBuilder.result(annotation.value(), resultHandler); + + if (resultHandler instanceof InvalidUsageHandler invalidUsageHandler) { + builder.invalidUsage(invalidUsageHandler); + return; + } + + if (resultHandler instanceof MissingPermissionsHandler missingPermissionsHandler) { + builder.missingPermission(missingPermissionsHandler); + return; + } + + builder.result(annotation.value(), resultHandler); } @NotNull @@ -213,27 +242,27 @@ public Class annotation() { } @Inject - private record LiteCommandProcessor(@NotNull LiteCommandsBuilder liteBuilder) - implements ComponentProcessor { + private record LiteCommandProcessor(@NotNull LiteCommandsConfigurer liteCommandsConfigurer) + implements ComponentProcessor { @Override public void process( @NotNull Object instance, - @NotNull Command annotation, + @NotNull LiteCommand annotation, @NotNull ComponentProcessorContext context ) { - liteBuilder.commands(instance); + liteCommandsConfigurer.builder().commands(instance); } @NotNull @Override - public Class annotation() { - return Command.class; + public Class annotation() { + return LiteCommand.class; } } @Inject - private record LiteArgumentProcessor(@NotNull LiteCommandsBuilder liteBuilder) + private record LiteArgumentProcessor(@NotNull LiteCommandsConfigurer liteCommandsConfigurer) implements ComponentProcessor { @Override @@ -244,7 +273,12 @@ public void process( @NotNull ComponentProcessorContext context ) { final ArgumentResolver argumentResolver = requireInstance(instance, ArgumentResolver.class, LiteArgument.class); - liteBuilder.argument(annotation.type(), ArgumentKey.of(annotation.name()), argumentResolver); + + final LiteCommandsBuilder builder = liteCommandsConfigurer.builder(); + final Class argumentClass = annotation.type(); + final String argumentKey = annotation.key(); + + builder.argument(argumentClass, ArgumentKey.of(argumentKey), argumentResolver); } @NotNull diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java index 989c7b3..23bee3c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java @@ -19,7 +19,7 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -@Service(priority = ComponentPriority.NORMAL) +@Service(priority = ComponentPriority.LOW) public final class MessageService extends BukkitMultification { private final MessageConfig messageConfig; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java index 2772c24..568964f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java @@ -2,13 +2,13 @@ import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.Unmodifiable; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @Service(priority = ComponentPriority.LOW) @@ -18,7 +18,7 @@ public final class GuiRegistry { private final Map, IdentifiableGui> byClass = new ConcurrentHashMap<>(); public void register(@NotNull IdentifiableGui gui) { - final String id = normalize(gui.getId()); + final String id = normalizeId(gui.getId()); final IdentifiableGui previous = byId.put(id, gui); // maintain class index (assume single instance per class) @@ -32,7 +32,7 @@ public void register(@NotNull IdentifiableGui gui) { } public boolean registerIfAbsent(@NotNull IdentifiableGui gui) { - final String id = normalize(gui.getId()); + final String id = normalizeId(gui.getId()); final IdentifiableGui existing = byId.putIfAbsent(id, gui); if (existing == null) { // we won the race; update class index @@ -42,9 +42,12 @@ public boolean registerIfAbsent(@NotNull IdentifiableGui gui) { return false; } - @Nullable + public boolean isRegistered(@NotNull String id) { + return byId.containsKey(normalizeId(id)); + } + public IdentifiableGui unregister(@NotNull String id) { - final String key = normalize(id); + final String key = normalizeId(id); final IdentifiableGui removed = byId.remove(key); if (removed != null) { byClass.compute(removed.getClass(), (k, current) -> current == removed ? null : current); @@ -52,29 +55,24 @@ public IdentifiableGui unregister(@NotNull String id) { return removed; } + @Subscribe(event = PlayTimeShutdownEvent.class) + public void unregisterAll() { + byId.clear(); + byClass.clear(); + } + @Nullable public IdentifiableGui getById(@NotNull String id) { - return byId.get(normalize(id)); + return byId.get(normalizeId(id)); } @Nullable @SuppressWarnings("unchecked") public T getByClass(@NotNull Class type) { - final IdentifiableGui gui = byClass.get(type); - return (T) gui; - } - - public boolean isRegistered(@NotNull String id) { - return byId.containsKey(normalize(id)); - } - - @Unmodifiable - public Set ids() { - return Set.copyOf(byId.keySet()); + return (T) byClass.get(type); } - private static String normalize(String id) { - final String trimmed = id.trim(); - return trimmed.toLowerCase(Locale.ROOT); + private static String normalizeId(String id) { + return id.trim().toLowerCase(Locale.ROOT); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java index cacc917..d27510a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.platform.gui; import org.jetbrains.annotations.NotNull; -@FunctionalInterface public interface IdentifiableGui { @NotNull String getId(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java index 78d8fd2..293424c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java @@ -17,7 +17,7 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; -@LiteArgument(type = User.class, name = "user") +@LiteArgument(type = User.class) final class UserArgument extends ArgumentResolver { private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(2); diff --git a/playtime-plugin/build.gradle.kts b/playtime-plugin/build.gradle.kts index 32f667d..70455de 100644 --- a/playtime-plugin/build.gradle.kts +++ b/playtime-plugin/build.gradle.kts @@ -52,10 +52,10 @@ tasks.withType { relocate(pkg, "$relocationPrefix.$pkg") } - minimize { - exclude { dependency -> dependency.moduleGroup == "com.github.imdmk.playtime" } - exclude(dependency("com.github.ben-manes.caffeine:caffeine")) - } +// minimize { +// exclude { dependency -> dependency.moduleGroup == "com.github.imdmk.playtime" } +// exclude(dependency("com.github.ben-manes.caffeine:caffeine")) +// } } bukkit { From 53b90eb6e9f4fd5cdb74e84b46651a66490e6ac4 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sat, 10 Jan 2026 17:29:12 +0100 Subject: [PATCH 10/17] Improve API. --- .../com/github/imdmk/playtime/PlayTime.java | 64 ++++++++++ .../github/imdmk/playtime/PlayTimeApi.java | 1 + .../imdmk/playtime/PlayTimeService.java | 5 +- .../com/github/imdmk/playtime/user/User.java | 45 ++++--- .../imdmk/playtime/user/UserDeleteResult.java | 11 -- .../imdmk/playtime/user/UserDeleteStatus.java | 7 -- .../imdmk/playtime/user/UserSaveReason.java | 12 -- .../imdmk/playtime/user/UserService.java | 6 +- .../github/imdmk/playtime/user/UserTime.java | 116 ------------------ .../{UserTimeTest.java => PlayTimeTest.java} | 73 ++++++----- .../com/github/imdmk/playtime/UserTest.java | 13 +- .../imdmk/playtime/UserDeleteEvent.java | 19 +-- .../github/imdmk/playtime/UserSaveEvent.java | 18 ++- .../github/imdmk/playtime/PlayTimePlugin.java | 6 +- .../playtime/BukkitPlayTimeService.java | 12 +- .../feature/playtime/PlayTimeUserFactory.java | 6 +- .../feature/playtime/command/TimeCommand.java | 6 +- .../playtime/command/TimeResetAllCommand.java | 4 +- .../playtime/command/TimeResetCommand.java | 4 +- .../playtime/command/TimeSetCommand.java | 4 +- .../feature/playtime/gui/PlayTimeTopGui.java | 4 +- .../listener/PlayTimeSaveController.java | 4 +- .../placeholder/PlayTimePlaceholder.java | 4 +- .../playtime/message/MessageService.java | 9 +- .../platform/gui/view/AbstractGui.java | 4 +- .../playtime/platform/gui/view/GridSlots.java | 8 +- .../playtime/platform/gui/view/GuiOpener.java | 87 +++---------- .../platform/gui/view/OpenableGui.java | 16 +++ .../platform/gui/view/ParameterizedGui.java | 23 +++- .../playtime/platform/gui/view/SimpleGui.java | 22 +++- .../platform/logger/BukkitPluginLogger.java | 1 - .../platform/logger/PluginLogger.java | 103 +--------------- .../platform/metrics/BMetricsService.java | 1 - .../placeholder/PlaceholderService.java | 1 - .../playtime/time/DurationFormatStyle.java | 10 +- .../imdmk/playtime/user/UserArgument.java | 19 +-- .../imdmk/playtime/user/UserFactory.java | 2 +- .../imdmk/playtime/user/UserServiceImpl.java | 8 +- .../user/cache/CaffeineUserCache.java | 26 ++-- .../imdmk/playtime/user/cache/UserCache.java | 6 +- .../user/controller/UserJoinController.java | 2 - .../playtime/user/repository/UserEntity.java | 8 +- .../user/repository/UserEntityMapper.java | 10 +- .../repository/UserRepositoryOrmLite.java | 5 +- .../playtime/user/top/CachedLeaderboard.java | 3 +- .../user/top/MemoryTopUsersCache.java | 5 +- .../playtime/user/top/TopUsersCache.java | 1 - .../user/repository/UserEntityMapperTest.java | 4 +- 48 files changed, 322 insertions(+), 506 deletions(-) create mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java rename playtime-api/src/test/java/com/github/imdmk/playtime/{UserTimeTest.java => PlayTimeTest.java} (69%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/OpenableGui.java diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java new file mode 100644 index 0000000..171fcd1 --- /dev/null +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java @@ -0,0 +1,64 @@ +package com.github.imdmk.playtime; + +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public record PlayTime(long millis) implements Comparable { + + private static final long MILLIS_PER_TICK = 50L; + + public static final PlayTime ZERO = new PlayTime(0L); + + public PlayTime { + if (millis < 0L) { + throw new IllegalArgumentException("UserTime millis cannot be negative"); + } + } + + public static PlayTime of(@NotNull Duration duration) { + return new PlayTime(duration.toMillis()); + } + + public static PlayTime ofMillis(long millis) { + return new PlayTime(millis); + } + + public static PlayTime ofTicks(long ticks) { + return new PlayTime(Math.multiplyExact(ticks, MILLIS_PER_TICK)); + } + + public long toMillis() { + return millis; + } + + public Duration toDuration() { + return Duration.ofMillis(millis); + } + + public long toSeconds() { + return TimeUnit.MILLISECONDS.toSeconds(millis); + } + + public int toTicks() { + return Math.toIntExact(millis / MILLIS_PER_TICK); + } + + public boolean isZero() { + return millis == 0; + } + + public PlayTime plus(@NotNull PlayTime other) { + return new PlayTime(Math.addExact(millis, other.millis)); + } + + public PlayTime minus(@NotNull PlayTime other) { + return new PlayTime(Math.subtractExact(millis, other.millis)); + } + + @Override + public int compareTo(PlayTime o) { + return Long.compare(millis, o.millis); + } +} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java index 298bec0..a0a59f7 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java @@ -7,4 +7,5 @@ public interface PlayTimeApi { UserService getUserService(); PlayTimeService getPlayTimeService(); + } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java index 1008372..6a2c11f 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java @@ -1,15 +1,14 @@ package com.github.imdmk.playtime; -import com.github.imdmk.playtime.user.UserTime; import org.jetbrains.annotations.NotNull; import java.util.UUID; public interface PlayTimeService { - UserTime getTime(@NotNull UUID uuid); + PlayTime getTime(@NotNull UUID uuid); - void setTime(@NotNull UUID uuid, @NotNull UserTime time); + void setTime(@NotNull UUID uuid, @NotNull PlayTime time); void resetTime(@NotNull UUID uuid); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java index 5987b95..8cff3a7 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java @@ -1,35 +1,45 @@ package com.github.imdmk.playtime.user; +import com.github.imdmk.playtime.PlayTime; import org.jetbrains.annotations.NotNull; +import java.util.Objects; import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; public final class User { private final UUID uuid; - private volatile String name; + private String name; - private final AtomicLong playtimeMillis; + private PlayTime playtime; - public User(@NotNull UUID uuid, @NotNull String name, @NotNull UserTime playtime) { + public User( + @NotNull UUID uuid, + @NotNull String name, + @NotNull PlayTime playtime + ) { this.uuid = uuid; this.name = name; - this.playtimeMillis = new AtomicLong(playtime.millis()); + this.playtime = playtime; } - public User(@NotNull UUID uuid, @NotNull String name) { - this(uuid, name, UserTime.ZERO); + public User( + @NotNull UUID uuid, + @NotNull String name + ) { + this.uuid = uuid; + this.name = name; + this.playtime = PlayTime.ZERO; } @NotNull public UUID getUuid() { - return this.uuid; + return uuid; } @NotNull public String getName() { - return this.name; + return name; } public void setName(@NotNull String name) { @@ -37,24 +47,23 @@ public void setName(@NotNull String name) { } @NotNull - public UserTime getPlaytime() { - return UserTime.ofMillis(playtimeMillis.get()); + public PlayTime getPlaytime() { + return playtime; } - public void setPlaytime(@NotNull UserTime playtime) { - playtimeMillis.set(playtime.millis()); + public void setPlaytime(@NotNull PlayTime playtime) { + this.playtime = playtime; } @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof User other)) return false; - return uuid.equals(other.uuid); + if (!(o instanceof User user)) return false; + return Objects.equals(uuid, user.uuid); } @Override public int hashCode() { - return uuid.hashCode(); + return Objects.hashCode(uuid); } @Override @@ -62,7 +71,7 @@ public String toString() { return "User{" + "uuid=" + uuid + ", name='" + name + '\'' + - ", playtimeMillis=" + playtimeMillis.get() + + ", time=" + playtime.millis() + '}'; } } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java deleted file mode 100644 index 7f8ee36..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.imdmk.playtime.user; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public record UserDeleteResult(@Nullable User user, @NotNull UserDeleteStatus status) { - - public boolean isSuccess() { - return this.user != null && this.status == UserDeleteStatus.DELETED; - } -} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java deleted file mode 100644 index efb33cc..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.imdmk.playtime.user; - -public enum UserDeleteStatus { - DELETED, - NOT_FOUND, - FAILED -} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java deleted file mode 100644 index 64486e2..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.imdmk.playtime.user; - -public enum UserSaveReason { - - PLAYER_JOIN, - PLAYER_LEAVE, - - SET_COMMAND, - RESET_COMMAND, - - GUI_RESET_CLICK -} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java index ebc38ec..a4fa5eb 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java @@ -17,10 +17,10 @@ public interface UserService { CompletableFuture> findByUuid(@NotNull UUID uuid); CompletableFuture> findByName(@NotNull String name); - CompletableFuture deleteByUuid(@NotNull UUID uuid); - CompletableFuture deleteByName(@NotNull String name); + CompletableFuture deleteByUuid(@NotNull UUID uuid); + CompletableFuture deleteByName(@NotNull String name); - CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason); + CompletableFuture save(@NotNull User user); CompletableFuture> findTopByPlayTime(int limit); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java deleted file mode 100644 index 117055a..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.github.imdmk.playtime.user; - -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; - -import java.io.Serial; -import java.io.Serializable; -import java.time.Duration; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -public record UserTime(long millis) implements Comparable, Serializable { - - @Serial private static final long serialVersionUID = 1L; - - public static final UserTime ZERO = new UserTime(0L); - - private static final long MILLIS_PER_TICK = 50L; - private static final long MILLIS_PER_SECOND = 1_000L; - - public UserTime { - if (millis < 0L) { - throw new IllegalArgumentException("UserTime millis cannot be negative"); - } - } - - @Contract("_ -> new") - public static @NotNull UserTime ofMillis(long millis) { - return new UserTime(millis); - } - - @Contract("_ -> new") - public static @NotNull UserTime ofDuration(@NotNull Duration duration) { - return ofMillis(duration.toMillis()); - } - - @Contract("_ -> new") - public static @NotNull UserTime ofSeconds(long seconds) { - return new UserTime(seconds * MILLIS_PER_SECOND); - } - - @Contract("_ -> new") - public static @NotNull UserTime ofTicks(long ticks) { - return new UserTime(ticks * MILLIS_PER_TICK); - } - - @Contract("_ -> new") - public static @NotNull UserTime from(@NotNull Duration duration) { - return new UserTime(duration.toMillis()); - } - - @Contract(pure = true) - public long toSeconds() { - return TimeUnit.MILLISECONDS.toSeconds(millis); - } - - @Contract(pure = true) - public int toTicks() { - return Math.toIntExact(millis / MILLIS_PER_TICK); - } - - @Contract(pure = true) - public @NotNull Duration toDuration() { - return Duration.ofMillis(millis); - } - - @Contract(pure = true) - public boolean isZero() { - return millis == 0; - } - - @Contract(pure = true) - public UserTime plus(@NotNull UserTime other) { - return new UserTime(Math.addExact(millis, other.millis)); - } - - @Contract(pure = true) - public UserTime minus(@NotNull UserTime other) { - return new UserTime(Math.subtractExact(millis, other.millis)); - } - - @Contract(pure = true) - public UserTime min(@NotNull UserTime other) { - return millis <= other.millis ? this : other; - } - - @Contract(pure = true) - public UserTime max(@NotNull UserTime other) { - return millis >= other.millis ? this : other; - } - - @Override - public int compareTo(@NotNull UserTime o) { - return Long.compare(millis, o.millis); - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - UserTime userTime = (UserTime) o; - return compareTo(userTime) == 0; - } - - @Override - public int hashCode() { - return Objects.hashCode(millis); - } - - @Override - @NotNull - public String toString() { - return "UserTime{" + - "millis=" + millis + - '}'; - } -} diff --git a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTimeTest.java b/playtime-api/src/test/java/com/github/imdmk/playtime/PlayTimeTest.java similarity index 69% rename from playtime-api/src/test/java/com/github/imdmk/playtime/UserTimeTest.java rename to playtime-api/src/test/java/com/github/imdmk/playtime/PlayTimeTest.java index a68f325..a46cb51 100644 --- a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTimeTest.java +++ b/playtime-api/src/test/java/com/github/imdmk/playtime/PlayTimeTest.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime; -import com.github.imdmk.playtime.user.UserTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -12,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; -class UserTimeTest { +class PlayTimeTest { @Nested @DisplayName("Construction & factories") @@ -20,40 +19,40 @@ class ConstructionTests { @Test void shouldCreateWithMillis() { - UserTime t = UserTime.ofMillis(1500); + PlayTime t = PlayTime.ofMillis(1500); assertThat(t.millis()).isEqualTo(1500); } @Test void shouldRejectNegativeMillis() { assertThatIllegalArgumentException() - .isThrownBy(() -> new UserTime(-1)) + .isThrownBy(() -> new PlayTime(-1)) .withMessageContaining("negative"); } @Test void shouldCreateFromSeconds() { - UserTime t = UserTime.ofSeconds(2); + PlayTime t = PlayTime.ofSeconds(2); assertThat(t.millis()).isEqualTo(2000); } @Test void shouldCreateFromTicks() { - UserTime t = UserTime.ofTicks(10); + PlayTime t = PlayTime.ofTicks(10); assertThat(t.millis()).isEqualTo(10 * 50); } @Test void shouldCreateFromDuration() { Duration d = Duration.ofMillis(1234); - UserTime t = UserTime.from(d); + PlayTime t = PlayTime.from(d); assertThat(t.millis()).isEqualTo(1234); } @Test void shouldRejectNullDuration() { assertThatNullPointerException() - .isThrownBy(() -> UserTime.from(null)) + .isThrownBy(() -> PlayTime.from(null)) .withMessageContaining("duration"); } } @@ -64,19 +63,19 @@ class ConversionTests { @Test void shouldConvertToSeconds() { - UserTime t = UserTime.ofMillis(2500); + PlayTime t = PlayTime.ofMillis(2500); assertThat(t.toSeconds()).isEqualTo(2); } @Test void shouldConvertToTicks() { - UserTime t = UserTime.ofMillis(250); + PlayTime t = PlayTime.ofMillis(250); assertThat(t.toTicks()).isEqualTo(5); // 250 / 50 } @Test void shouldConvertToDuration() { - UserTime t = UserTime.ofMillis(500); + PlayTime t = PlayTime.ofMillis(500); assertThat(t.toDuration()).isEqualTo(Duration.ofMillis(500)); } } @@ -87,15 +86,15 @@ class ArithmeticTests { @Test void shouldAddUserTimes() { - UserTime a = UserTime.ofMillis(1000); - UserTime b = UserTime.ofMillis(2000); - assertThat(a.plus(b)).isEqualTo(UserTime.ofMillis(3000)); + PlayTime a = PlayTime.ofMillis(1000); + PlayTime b = PlayTime.ofMillis(2000); + assertThat(a.plus(b)).isEqualTo(PlayTime.ofMillis(3000)); } @Test void shouldThrowOnAddOverflow() { - UserTime a = UserTime.ofMillis(Long.MAX_VALUE); - UserTime b = UserTime.ofMillis(1); + PlayTime a = PlayTime.ofMillis(Long.MAX_VALUE); + PlayTime b = PlayTime.ofMillis(1); assertThatExceptionOfType(ArithmeticException.class) .isThrownBy(() -> a.plus(b)); @@ -103,14 +102,14 @@ void shouldThrowOnAddOverflow() { @Test void shouldSubtractUserTimes() { - UserTime a = UserTime.ofMillis(3000); - UserTime b = UserTime.ofMillis(1000); - assertThat(a.minus(b)).isEqualTo(UserTime.ofMillis(2000)); + PlayTime a = PlayTime.ofMillis(3000); + PlayTime b = PlayTime.ofMillis(1000); + assertThat(a.minus(b)).isEqualTo(PlayTime.ofMillis(2000)); } @Test void shouldRejectNullInPlus() { - UserTime t = UserTime.ZERO; + PlayTime t = PlayTime.ZERO; assertThatNullPointerException() .isThrownBy(() -> t.plus(null)); @@ -118,7 +117,7 @@ void shouldRejectNullInPlus() { @Test void shouldRejectNullInMinus() { - UserTime t = UserTime.ZERO; + PlayTime t = PlayTime.ZERO; assertThatNullPointerException() .isThrownBy(() -> t.minus(null)); @@ -131,8 +130,8 @@ class MinMaxTests { @Test void shouldReturnMin() { - UserTime a = UserTime.ofMillis(500); - UserTime b = UserTime.ofMillis(1000); + PlayTime a = PlayTime.ofMillis(500); + PlayTime b = PlayTime.ofMillis(1000); assertThat(a.min(b)).isEqualTo(a); assertThat(b.min(a)).isEqualTo(a); @@ -140,8 +139,8 @@ void shouldReturnMin() { @Test void shouldReturnMax() { - UserTime a = UserTime.ofMillis(500); - UserTime b = UserTime.ofMillis(1000); + PlayTime a = PlayTime.ofMillis(500); + PlayTime b = PlayTime.ofMillis(1000); assertThat(a.max(b)).isEqualTo(b); assertThat(b.max(a)).isEqualTo(b); @@ -149,7 +148,7 @@ void shouldReturnMax() { @Test void shouldRejectNullInMin() { - UserTime a = UserTime.ZERO; + PlayTime a = PlayTime.ZERO; assertThatNullPointerException() .isThrownBy(() -> a.min(null)); @@ -157,7 +156,7 @@ void shouldRejectNullInMin() { @Test void shouldRejectNullInMax() { - UserTime a = UserTime.ZERO; + PlayTime a = PlayTime.ZERO; assertThatNullPointerException() .isThrownBy(() -> a.max(null)); @@ -170,9 +169,9 @@ class ComparisonTests { @Test void shouldCompareByMillis() { - UserTime a = UserTime.ofMillis(100); - UserTime b = UserTime.ofMillis(200); - UserTime c = UserTime.ofMillis(100); + PlayTime a = PlayTime.ofMillis(100); + PlayTime b = PlayTime.ofMillis(200); + PlayTime c = PlayTime.ofMillis(100); assertThat(a.compareTo(b)).isNegative(); assertThat(b.compareTo(a)).isPositive(); @@ -186,8 +185,8 @@ class EqualityTests { @Test void shouldBeEqualWhenMillisIsSame() { - UserTime a = UserTime.ofMillis(500); - UserTime b = UserTime.ofMillis(500); + PlayTime a = PlayTime.ofMillis(500); + PlayTime b = PlayTime.ofMillis(500); assertThat(a).isEqualTo(b); assertThat(a.hashCode()).isEqualTo(b.hashCode()); @@ -195,8 +194,8 @@ void shouldBeEqualWhenMillisIsSame() { @Test void shouldNotBeEqualWhenMillisDiffers() { - UserTime a = UserTime.ofMillis(500); - UserTime b = UserTime.ofMillis(300); + PlayTime a = PlayTime.ofMillis(500); + PlayTime b = PlayTime.ofMillis(300); assertThat(a).isNotEqualTo(b); } @@ -204,13 +203,13 @@ void shouldNotBeEqualWhenMillisDiffers() { @Test void shouldIdentifyZero() { - assertThat(UserTime.ZERO.isZero()).isTrue(); - assertThat(UserTime.ofMillis(1).isZero()).isFalse(); + assertThat(PlayTime.ZERO.isZero()).isTrue(); + assertThat(PlayTime.ofMillis(1).isZero()).isFalse(); } @Test void toStringShouldContainMillis() { - UserTime t = UserTime.ofMillis(1234); + PlayTime t = PlayTime.ofMillis(1234); assertThat(t.toString()).contains("millis=1234"); } } diff --git a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java b/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java index 758d654..15fd16c 100644 --- a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java +++ b/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -21,7 +20,7 @@ class ConstructorTests { @Test void shouldInitializeFieldsCorrectly() { UUID uuid = UUID.randomUUID(); - UserTime time = UserTime.ofMillis(5000); + PlayTime time = PlayTime.ofMillis(5000); User user = new User(uuid, "Player", time); @@ -34,7 +33,7 @@ void shouldInitializeFieldsCorrectly() { void shouldCreateUserWithZeroPlaytime() { User user = new User(UUID.randomUUID(), "Player"); - assertThat(user.getPlaytime()).isEqualTo(UserTime.ZERO); + assertThat(user.getPlaytime()).isEqualTo(PlayTime.ZERO); } @Test @@ -42,7 +41,7 @@ void shouldThrowWhenNameIsNull() { UUID uuid = UUID.randomUUID(); assertThatNullPointerException() - .isThrownBy(() -> new User(uuid, null, UserTime.ZERO)) + .isThrownBy(() -> new User(uuid, null, PlayTime.ZERO)) .withMessageContaining("name"); } @@ -94,7 +93,7 @@ class PlaytimeTests { @Test void shouldReturnCurrentPlaytimeAsUserTime() { - User user = new User(UUID.randomUUID(), "Player", UserTime.ofMillis(1000)); + User user = new User(UUID.randomUUID(), "Player", PlayTime.ofMillis(1000)); assertThat(user.getPlaytime().millis()).isEqualTo(1000); } @@ -103,7 +102,7 @@ void shouldReturnCurrentPlaytimeAsUserTime() { void shouldSetNewPlaytime() { User user = new User(UUID.randomUUID(), "Player"); - user.setPlaytime(UserTime.ofMillis(12345)); + user.setPlaytime(PlayTime.ofMillis(12345)); assertThat(user.getPlaytime().millis()).isEqualTo(12345); } @@ -144,7 +143,7 @@ void usersWithDifferentUuidShouldNotBeEqual() { @Test void toStringShouldContainKeyInformation() { - User user = new User(UUID.randomUUID(), "Player", UserTime.ofMillis(100)); + User user = new User(UUID.randomUUID(), "Player", PlayTime.ofMillis(100)); String str = user.toString(); diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java index 58c1d28..e78fe61 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java @@ -1,6 +1,6 @@ package com.github.imdmk.playtime; -import com.github.imdmk.playtime.user.UserDeleteResult; +import com.github.imdmk.playtime.user.User; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; @@ -10,23 +10,26 @@ public final class UserDeleteEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); private static final boolean ASYNC = true; - private final UserDeleteResult result; + private final User user; - public UserDeleteEvent(@NotNull UserDeleteResult result) { + public UserDeleteEvent(@NotNull User user) { super(ASYNC); - this.result = result; + this.user = user; } - public @NotNull UserDeleteResult getResult() { - return this.result; + @NotNull + public User getUser() { + return user; } + @NotNull @Override - public @NotNull HandlerList getHandlers() { + public HandlerList getHandlers() { return HANDLERS; } - public static @NotNull HandlerList getHandlerList() { + @NotNull + public static HandlerList getHandlerList() { return HANDLERS; } diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java index 0c46208..a041925 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; @@ -12,28 +11,25 @@ public final class UserSaveEvent extends Event { private static final boolean ASYNC = true; private final User user; - private final UserSaveReason reason; - public UserSaveEvent(@NotNull User user, @NotNull UserSaveReason reason) { + public UserSaveEvent(@NotNull User user) { super(ASYNC); this.user = user; - this.reason = reason; } - public @NotNull User getUser() { + @NotNull + public User getUser() { return this.user; } - public @NotNull UserSaveReason getReason() { - return this.reason; - } - + @NotNull @Override - public @NotNull HandlerList getHandlers() { + public HandlerList getHandlers() { return HANDLERS; } - public static @NotNull HandlerList getHandlerList() { + @NotNull + public static HandlerList getHandlerList() { return HANDLERS; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index 0bf6c6c..2542631 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -48,8 +48,8 @@ final class PlayTimePlugin { this.publisher.publish(new PlayTimeInitializeEvent()); - //final PlayTimeApi api = injector.newInstance(PlayTimeApiAdapter.class); - //PlayTimeApiProvider.register(api); + final PlayTimeApi api = injector.newInstance(PlayTimeApiAdapter.class); + PlayTimeApiProvider.register(api); final long elapsedMillis = stopwatch.elapsed(TimeUnit.MILLISECONDS); logger.info("Successfully loaded plugin in " + elapsedMillis + "ms!"); @@ -57,6 +57,6 @@ final class PlayTimePlugin { void disable() { this.publisher.publish(new PlayTimeShutdownEvent()); - //PlayTimeApiProvider.unregister(); + PlayTimeApiProvider.unregister(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java index fcdf4ff..61a7f53 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java @@ -3,7 +3,7 @@ import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import org.bukkit.OfflinePlayer; import org.bukkit.Server; import org.bukkit.Statistic; @@ -25,26 +25,26 @@ final class BukkitPlayTimeService implements PlayTimeService { } @Override - public @NotNull UserTime getTime(@NotNull UUID uuid) { + public @NotNull PlayTime getTime(@NotNull UUID uuid) { checkPrimaryThread(); final int timeTicks = getOffline(uuid).getStatistic(PLAYTIME_STATISTIC); if (timeTicks <= 0) { - return UserTime.ZERO; + return PlayTime.ZERO; } - return UserTime.ofTicks(timeTicks); + return PlayTime.ofTicks(timeTicks); } @Override - public void setTime(@NotNull UUID uuid, @NotNull UserTime time) { + public void setTime(@NotNull UUID uuid, @NotNull PlayTime time) { checkPrimaryThread(); getOffline(uuid).setStatistic(PLAYTIME_STATISTIC, time.toTicks()); } @Override public void resetTime(@NotNull UUID uuid) { - setTime(uuid, UserTime.ZERO); + setTime(uuid, PlayTime.ZERO); } private OfflinePlayer getOffline(UUID uuid) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java index 8eb3368..85f0e01 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java @@ -5,7 +5,7 @@ import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserFactory; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -30,7 +30,7 @@ public PlayTimeUserFactory(@NotNull PlayTimeService playtimeService) { public @NotNull User createFrom(@NotNull Player player) { final UUID uuid = player.getUniqueId(); final String name = player.getName(); - final UserTime time = playtimeService.getTime(uuid); + final PlayTime time = playtimeService.getTime(uuid); return new User(uuid, name, time); } @@ -39,7 +39,7 @@ public PlayTimeUserFactory(@NotNull PlayTimeService playtimeService) { public @NotNull User createFrom(@NotNull OfflinePlayer player) { final UUID uuid = player.getUniqueId(); final String name = Optional.ofNullable(player.getName()).orElse(UNKNOWN_PLAYER_NAME_FORMAT.formatted(uuid)); - final UserTime time = playtimeService.getTime(uuid); + final PlayTime time = playtimeService.getTime(uuid); return new User(uuid, name, time); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java index 8e5e55c..4e4f153 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java @@ -5,7 +5,7 @@ import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.time.Durations; import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.async.Async; import dev.rollczi.litecommands.annotations.command.Command; @@ -35,7 +35,7 @@ public TimeCommand( @Execute @Permission("command.playtime") void selfPlaytime(@Context Player viewer) { - final UserTime time = playtimeService.getTime(viewer.getUniqueId()); + final PlayTime time = playtimeService.getTime(viewer.getUniqueId()); messageService.create() .notice(n -> n.playtimeMessages.playerPlaytimeSelf()) @@ -47,7 +47,7 @@ void selfPlaytime(@Context Player viewer) { @Execute @Permission("command.playtime.target") void targetPlaytime(@Context Player viewer, @Arg @Async User target) { - final UserTime time = playtimeService.getTime(target.getUuid()); + final PlayTime time = playtimeService.getTime(target.getUuid()); messageService.create() .notice(n -> n.playtimeMessages.playerPlaytimeTarget()) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java index fc1708d..1d117b2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java @@ -8,7 +8,7 @@ import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import com.github.imdmk.playtime.user.repository.UserRepository; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; @@ -85,7 +85,7 @@ void resetAll(@Context CommandSender sender) { } private CompletableFuture resetUser(User user) { - user.setPlaytime(UserTime.ZERO); + user.setPlaytime(PlayTime.ZERO); return userService.save(user, SAVE_REASON); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java index 2ab4a51..b61e397 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java @@ -6,7 +6,7 @@ import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.async.Async; import dev.rollczi.litecommands.annotations.command.Command; @@ -41,7 +41,7 @@ public TimeResetCommand( @Execute void reset(@Context CommandSender sender, @Arg @Async User target) { - target.setPlaytime(UserTime.ZERO); + target.setPlaytime(PlayTime.ZERO); userService.save(target, SAVE_REASON) .thenAccept(user -> messageService.create() diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java index c1658a9..899051d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java @@ -8,7 +8,7 @@ import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.async.Async; import dev.rollczi.litecommands.annotations.command.Command; @@ -47,7 +47,7 @@ public TimeSetCommand( @Execute void setPlaytime(@Context CommandSender sender, @Arg @Async User target, @Arg Duration time) { final Duration normalizedTime = Durations.clamp(time); - final UserTime newTime = UserTime.ofDuration(normalizedTime); + final PlayTime newTime = PlayTime.ofDuration(normalizedTime); target.setPlaytime(newTime); playTimeService.setTime(target.getUuid(), newTime); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java index ea68c56..0bffa2d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java @@ -22,7 +22,7 @@ import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.builder.item.SkullBuilder; import dev.triumphteam.gui.guis.BaseGui; @@ -103,7 +103,7 @@ public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull gui.close(viewer); - user.setPlaytime(UserTime.ZERO); + user.setPlaytime(PlayTime.ZERO); userService.save(user, UserSaveReason.GUI_RESET_CLICK) .thenAccept(result -> messageService.send(viewer, n -> n.playtimeMessages.playerPlaytimeReset())) .exceptionally(e -> { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java index eecdad2..3202894 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java @@ -5,7 +5,7 @@ import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; @@ -34,7 +34,7 @@ public void onPlayerQuit(PlayerQuitEvent event) { final UUID uuid = event.getPlayer().getUniqueId(); final User user = userService.findCachedByUuid(uuid).orElseThrow(); - final UserTime time = playtimeService.getTime(uuid); + final PlayTime time = playtimeService.getTime(uuid); user.setPlaytime(time); userService.save(user, UserSaveReason.PLAYER_LEAVE); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java index 42acea8..76d414d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java @@ -4,7 +4,7 @@ import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import com.github.imdmk.playtime.time.Durations; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; @@ -26,7 +26,7 @@ public PlayTimePlaceholder(@NotNull PlayTimeService playtimeService) { @Override public @NotNull String request(@NotNull Player player, @NotNull String params) { - final UserTime time = playtimeService.getTime(player.getUniqueId()); + final PlayTime time = playtimeService.getTime(player.getUniqueId()); return Durations.format(time.toDuration()); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java index 23bee3c..21a860d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java @@ -31,18 +31,21 @@ public MessageService(@NotNull Plugin plugin, @NotNull MessageConfig messageConf this.audienceProvider = BukkitAudiences.create(plugin); } + @NotNull @Override - protected @NotNull TranslationProvider translationProvider() { + protected TranslationProvider translationProvider() { return provider -> messageConfig; } + @NotNull @Override - protected @NotNull ComponentSerializer serializer() { + protected ComponentSerializer serializer() { return AdventureComponents.miniMessage(); } + @NotNull @Override - protected @NotNull AudienceConverter audienceConverter() { + protected AudienceConverter audienceConverter() { return sender -> { if (sender instanceof Player player) { return audienceProvider.player(player.getUniqueId()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java index 2a6599b..eeaad41 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java @@ -47,7 +47,7 @@ protected void placePrevious(@NotNull BaseGui gui, @NotNull Player viewer) { navigationBar.setPrevious(gui, viewer); } - protected void placeExit(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull Consumer exit) { - navigationBar.setExit(gui, viewer, exit); + protected void placeExit(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull Consumer exitAction) { + navigationBar.setExit(gui, viewer, exitAction); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java index 5b604fb..6efe5a2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java @@ -18,10 +18,6 @@ final class GridSlots { private static final int ROW_6_PREVIOUS = 46; private static final int ROW_6_EXIT = 49; - private GridSlots() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); - } - static int next(int rows) { return switch (rows) { case 3 -> ROW_3_NEXT; @@ -51,4 +47,8 @@ static int exit(int rows) { default -> throw new IllegalArgumentException("Unsupported rows for EXIT: " + rows); }; } + + GridSlots() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java index d625b14..ee7759f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java @@ -16,95 +16,40 @@ public final class GuiOpener { private final TaskScheduler scheduler; @Inject - public GuiOpener(@NotNull GuiRegistry registry, @NotNull TaskScheduler scheduler) { + public GuiOpener( + GuiRegistry registry, + TaskScheduler scheduler + ) { this.registry = registry; this.scheduler = scheduler; } public void open( - @NotNull Class type, - @NotNull Player viewer - ) { - final IdentifiableGui gui = require(type); - if (!(gui instanceof SimpleGui simpleGui)) { - throw wrongType(type.getName(), gui, "SimpleGui"); - } - - final BaseGui baseGui = simpleGui.createGui(); - simpleGui.prepareItems(baseGui, viewer); - scheduler.runSync(() -> baseGui.open(viewer)); - } - - @SuppressWarnings("unchecked") - public void open( - @NotNull Class> type, - @NotNull Player viewer, - @NotNull T parameter - ) { - final IdentifiableGui gui = require(type); - if (!(gui instanceof ParameterizedGui paramGui)) { - throw wrongType(type.getName(), gui, "ParameterizedGui"); - } - - final ParameterizedGui typed = (ParameterizedGui) paramGui; - final BaseGui baseGui = typed.createGui(viewer, parameter); - - typed.prepareItems(baseGui, viewer, parameter); - scheduler.runSync(() -> baseGui.open(viewer)); - } - - public void open( - @NotNull String id, + @NotNull Class> type, @NotNull Player viewer ) { - final IdentifiableGui gui = require(id); - if (!(gui instanceof SimpleGui simpleGui)) { - throw wrongType(id, gui, "SimpleGui"); - } - - final BaseGui baseGui = simpleGui.createGui(); - - simpleGui.prepareItems(baseGui, viewer); - scheduler.runSync(() -> baseGui.open(viewer)); + final OpenableGui gui = require(type); + gui.open(viewer, scheduler, null); } - @SuppressWarnings("unchecked") public void open( - @NotNull String id, + @NotNull Class> type, @NotNull Player viewer, @NotNull T parameter ) { - final IdentifiableGui gui = require(id); - if (!(gui instanceof ParameterizedGui paramGui)) { - throw wrongType(id, gui, "ParameterizedGui"); - } - - final ParameterizedGui typed = (ParameterizedGui) paramGui; - final BaseGui baseGui = typed.createGui(viewer, parameter); - - typed.prepareItems(baseGui, viewer, parameter); - scheduler.runSync(() -> baseGui.open(viewer)); + final OpenableGui gui = require(type); + gui.open(viewer, scheduler, parameter); } - private IdentifiableGui require(String id) { - final IdentifiableGui gui = registry.getById(id); + private

OpenableGui

require(Class> type) { + final OpenableGui

gui = registry.getByClass(type); if (gui == null) { - throw new IllegalArgumentException("No GUI registered under id '" + id + "'"); + throw new IllegalArgumentException( + "No GUI registered for class '" + type.getName() + "'" + ); } - return gui; - } - private IdentifiableGui require(Class type) { - final IdentifiableGui gui = registry.getByClass(type); - if (gui == null) { - throw new IllegalArgumentException("No GUI registered for class '" + type.getName() + "'"); - } return gui; } - private static IllegalArgumentException wrongType(String key, IdentifiableGui gui, String expected) { - return new IllegalArgumentException( - "GUI '" + key + "' is not a " + expected + " (actual=" + gui.getClass().getSimpleName() + ")" - ); - } -} \ No newline at end of file +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/OpenableGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/OpenableGui.java new file mode 100644 index 0000000..79bdb88 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/OpenableGui.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.platform.gui.view; + +import com.github.imdmk.playtime.platform.gui.IdentifiableGui; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public interface OpenableGui

extends IdentifiableGui { + + void open( + @NotNull Player viewer, + @NotNull TaskScheduler scheduler, + P parameter + ); +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java index bda1ecd..3d22289 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java @@ -1,14 +1,29 @@ package com.github.imdmk.playtime.platform.gui.view; -import com.github.imdmk.playtime.platform.gui.IdentifiableGui; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import dev.triumphteam.gui.guis.BaseGui; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public interface ParameterizedGui extends IdentifiableGui { +public interface ParameterizedGui extends OpenableGui { - BaseGui createGui(@NotNull Player viewer, @NotNull T parameter); + @Override + default void open( + @NotNull Player viewer, + @NotNull TaskScheduler scheduler, + @NotNull T parameter + ) { + final BaseGui gui = createGui(viewer, parameter); + prepareItems(gui, viewer, parameter); + scheduler.runSync(() -> gui.open(viewer)); + } - void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull T parameter); + BaseGui createGui(@NotNull Player viewer, @NotNull T parameter); + void prepareItems( + @NotNull BaseGui gui, + @NotNull Player viewer, + @NotNull T parameter + ); } + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java index 0144eaf..37b2e30 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java @@ -1,13 +1,27 @@ package com.github.imdmk.playtime.platform.gui.view; -import com.github.imdmk.playtime.platform.gui.IdentifiableGui; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import dev.triumphteam.gui.guis.BaseGui; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -public interface SimpleGui extends IdentifiableGui { +public interface SimpleGui extends OpenableGui { - BaseGui createGui(); + @Override + default void open( + @NotNull Player viewer, + @NotNull TaskScheduler scheduler, + Void unused + ) { + final BaseGui gui = createGui(viewer); + prepareItems(gui, viewer); + scheduler.runSync(() -> gui.open(viewer)); + } - void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer); + BaseGui createGui(@NotNull Player viewer); + + void prepareItems( + @NotNull BaseGui gui, + @NotNull Player viewer + ); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java index f15ea04..ca28657 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java @@ -10,7 +10,6 @@ public final class BukkitPluginLogger implements PluginLogger { - /** Backing {@link java.util.logging.Logger} provided by Bukkit. */ private final Logger logger; @Inject diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/PluginLogger.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/PluginLogger.java index bbaafa1..e0cbf49 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/PluginLogger.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/PluginLogger.java @@ -3,115 +3,20 @@ import org.intellij.lang.annotations.PrintFormat; import org.jetbrains.annotations.NotNull; -/** - * Unified logging abstraction for the PlayTime plugin environment. - * - *

This interface defines a consistent logging API decoupled from the underlying - * logging backend (e.g., Bukkit’s {@link java.util.logging.Logger}, SLF4J, or a custom logger). - * It provides structured, formatted, and throwable-aware logging across multiple log levels.

- * - *

Design goals:

- *
    - *
  • Consistent logging interface across all plugin components.
  • - *
  • Support for message formatting with {@link String#format} syntax.
  • - *
  • Convenient overloads for attaching {@link Throwable}s and stack traces.
  • - *
  • Simple to implement for different backends (e.g., {@link BukkitPluginLogger}).
  • - *
- * - *

Thread-safety: Implementations are expected to be thread-safe and safe - * for concurrent use across async or scheduled tasks.

- * - * @see BukkitPluginLogger - * @see java.util.logging.Logger - */ public interface PluginLogger { - /** - * Logs a general informational message. - * - * @param message message to log (non-null) - */ void info(@NotNull String message); - - /** - * Logs a formatted informational message. - * - * @param message format string using {@link String#format} syntax (non-null) - * @param args arguments to format (non-null) - */ void info(@NotNull @PrintFormat String message, @NotNull Object... args); - /** - * Logs a warning message, indicating a non-fatal issue. - * - * @param message warning message (non-null) - */ void warn(@NotNull String message); - - /** - * Logs a warning caused by a {@link Throwable}, typically without rethrowing it. - * - * @param throwable the exception or error (non-null) - */ - void warn(@NotNull Throwable throwable); - - /** - * Logs a formatted warning message with an associated {@link Throwable}. - * - * @param throwable cause of the warning (non-null) - * @param message format string (non-null) - * @param args format arguments (non-null) - */ - void warn(@NotNull Throwable throwable, - @NotNull @PrintFormat String message, - @NotNull Object... args); - - /** - * Logs a formatted warning message. - * - * @param message format string (non-null) - * @param args format arguments (non-null) - */ void warn(@NotNull @PrintFormat String message, @NotNull Object... args); + void warn(@NotNull Throwable throwable); + void warn(@NotNull Throwable throwable, @NotNull @PrintFormat String message, @NotNull Object... args); - /** - * Logs an error with a throwable stack trace. - * - * @param throwable exception or error to log (non-null) - */ - void error(@NotNull Throwable throwable); - - /** - * Logs an error message at the highest severity level. - * - * @param message message to log (non-null) - */ void error(@NotNull String message); - - /** - * Logs a formatted error message. - * - * @param message format string (non-null) - * @param args format arguments (non-null) - */ void error(@NotNull @PrintFormat String message, @NotNull Object... args); - - /** - * Logs an error message with the given {@link Throwable}. - * - * @param throwable cause of the error (non-null) - * @param message unformatted message text (non-null) - */ + void error(@NotNull Throwable throwable); + void error(@NotNull Throwable throwable, @NotNull @PrintFormat String message, @NotNull Object... args); void error(@NotNull Throwable throwable, @NotNull String message); - /** - * Logs a formatted error message with the given {@link Throwable}. - * - * @param throwable cause of the error (non-null) - * @param message format string (non-null) - * @param args format arguments (non-null) - */ - void error(@NotNull Throwable throwable, - @NotNull @PrintFormat String message, - @NotNull Object... args); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java index 4c00ebf..5d0efc0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.metrics; -import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java index 8f0c7a4..0a67535 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.placeholder; -import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java index 19a442a..c649bf0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java @@ -11,7 +11,7 @@ public enum DurationFormatStyle { COMPACT { @Override - public String format(@NotNull Duration duration) { + String format(@NotNull Duration duration) { return formatWith(duration, (unit, value) -> value + unit.getAbbreviation(), Separator.SPACE); @@ -19,7 +19,7 @@ public String format(@NotNull Duration duration) { }, LONG { @Override - public String format(@NotNull Duration duration) { + String format(@NotNull Duration duration) { return formatWith(duration, DurationUnit::toDisplayName, Separator.SPACE); @@ -27,7 +27,7 @@ public String format(@NotNull Duration duration) { }, LONG_WITH_AND { @Override - public String format(@NotNull Duration duration) { + String format(@NotNull Duration duration) { return formatWith(duration, DurationUnit::toDisplayName, Separator.AND); @@ -35,14 +35,14 @@ public String format(@NotNull Duration duration) { }, NATURAL { @Override - public String format(@NotNull Duration duration) { + String format(@NotNull Duration duration) { return formatWith(duration, DurationUnit::toDisplayName, Separator.COMMA); } }; - public abstract String format(@NotNull Duration duration); + abstract String format(@NotNull Duration duration); static String formatWith( @NotNull Duration duration, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java index 293424c..d2407d5 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java @@ -41,13 +41,14 @@ final class UserArgument extends ArgumentResolver { } @Override - protected ParseResult parse(Invocation invocation, - Argument context, - String argument) { - + protected ParseResult parse( + Invocation invocation, + Argument context, + String argument + ) { // Main thread -> cache-only (never block the tick) if (server.isPrimaryThread()) { - logger.warn("UserArgument lookup for '%s' on main thread – using cache only.", argument); + logger.warn("UserArgument lookup for '%s' on main thread - using cache only.", argument); return userService.findCachedByName(argument) .map(ParseResult::success) .orElse(ParseResult.failure(messageConfig.playerNotFound)); @@ -62,9 +63,11 @@ protected ParseResult parse(Invocation invocation, } @Override - public SuggestionResult suggest(Invocation invocation, - Argument argument, - SuggestionContext context) { + public SuggestionResult suggest( + Invocation invocation, + Argument argument, + SuggestionContext context + ) { return userService.getCachedUsers() .stream() .map(User::getName) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java index 9ce7704..8088f03 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java @@ -7,6 +7,6 @@ public interface UserFactory { User createFrom(@NotNull Player player); - User createFrom(@NotNull OfflinePlayer offlinePlayer); + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java index d284d5c..1d70577 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java @@ -139,14 +139,12 @@ public CompletableFuture deleteByUuid(@NotNull UUID uuid) { } @Override - public CompletableFuture deleteByName(@NotNull String name) { + public CompletableFuture deleteByName(@NotNull String name) { return repository.deleteByName(name) .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .thenApply(deleteResult -> { - eventCaller.callEvent(new UserDeleteEvent(deleteResult)); - if (deleteResult.isSuccess()) { - cache.invalidateByName(name); - } + eventCaller.callEvent(new UserDeleteEvent(user)); + cache.invalidateByName(name); return deleteResult; }) .exceptionally(e -> { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java index 730d10b..35d61c7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java @@ -23,8 +23,8 @@ @Service(priority = ComponentPriority.LOWEST) public final class CaffeineUserCache implements UserCache { - private static final Duration DEFAULT_EXPIRE_AFTER_ACCESS = Duration.ofHours(2); - private static final Duration DEFAULT_EXPIRE_AFTER_WRITE = Duration.ofHours(12); + private static final Duration EXPIRE_AFTER_ACCESS = Duration.ofHours(2); + private static final Duration EXPIRE_AFTER_WRITE = Duration.ofHours(12); private final Cache cacheByUuid; private final Cache cacheByName; @@ -32,13 +32,13 @@ public final class CaffeineUserCache implements UserCache { @Inject public CaffeineUserCache() { this.cacheByName = Caffeine.newBuilder() - .expireAfterWrite(DEFAULT_EXPIRE_AFTER_WRITE) - .expireAfterAccess(DEFAULT_EXPIRE_AFTER_ACCESS) + .expireAfterWrite(EXPIRE_AFTER_WRITE) + .expireAfterAccess(EXPIRE_AFTER_ACCESS) .build(); this.cacheByUuid = Caffeine.newBuilder() - .expireAfterWrite(DEFAULT_EXPIRE_AFTER_WRITE) - .expireAfterAccess(DEFAULT_EXPIRE_AFTER_ACCESS) + .expireAfterWrite(EXPIRE_AFTER_WRITE) + .expireAfterAccess(EXPIRE_AFTER_ACCESS) .removalListener((UUID key, User user, RemovalCause cause) -> { if (key != null && user != null) { this.cacheByName.invalidate(user.getName()); @@ -89,6 +89,13 @@ public void invalidateByName(@NotNull String name) { } } + @Override + @Subscribe(event = PlayTimeShutdownEvent.class) + public void invalidateAll() { + cacheByUuid.invalidateAll(); + cacheByName.invalidateAll(); + } + @Override public Optional getUserByUuid(@NotNull UUID uuid) { return Optional.ofNullable(cacheByUuid.getIfPresent(uuid)); @@ -124,11 +131,4 @@ public void forEachUser(@NotNull Consumer action) { public Collection getCache() { return List.copyOf(cacheByUuid.asMap().values()); } - - @Override - @Subscribe(event = PlayTimeShutdownEvent.class) - public void invalidateAll() { - cacheByUuid.invalidateAll(); - cacheByName.invalidateAll(); - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java index 98fcdcf..d0cf220 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java @@ -13,20 +13,16 @@ public interface UserCache { void cacheUser(@NotNull User user); void invalidateUser(@NotNull User user); - void invalidateByUuid(@NotNull UUID uuid); - void invalidateByName(@NotNull String name); + void invalidateAll(); Optional getUserByUuid(@NotNull UUID uuid); - Optional getUserByName(@NotNull String name); void updateUserNameMapping(@NotNull User user, @NotNull String oldName); - void forEachUser(@NotNull Consumer action); Collection getCache(); - void invalidateAll(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java index bab36c9..d5de862 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java @@ -23,8 +23,6 @@ @Controller public final class UserJoinController implements Listener { - private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_JOIN; - private final Server server; private final PluginLogger logger; private final UserService userService; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java index 99d8f94..2e93729 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java @@ -13,7 +13,7 @@ public final class UserEntity { @DatabaseField(id = true, canBeNull = false, columnName = UserEntityMeta.Col.UUID) private UUID uuid; - @DatabaseField(canBeNull = false, index = true, columnName = UserEntityMeta.Col.NAME) + @DatabaseField(index = true, canBeNull = false, columnName = UserEntityMeta.Col.NAME) private String name; @DatabaseField(canBeNull = false, columnName = UserEntityMeta.Col.PLAYTIME_MILLIS) @@ -21,7 +21,11 @@ public final class UserEntity { public UserEntity() {} - public UserEntity(@NotNull UUID uuid, @NotNull String name, long playtimeMillis) { + public UserEntity( + @NotNull UUID uuid, + @NotNull String name, + long playtimeMillis + ) { this.uuid = uuid; this.name = name; this.playtimeMillis = playtimeMillis; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java index 2c1d9fb..d290e8f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java @@ -1,15 +1,11 @@ package com.github.imdmk.playtime.user.repository; import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; -import com.github.imdmk.playtime.injector.ComponentPriority; -import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import org.jetbrains.annotations.NotNull; -@Service(priority = ComponentPriority.LOWEST) -public final class UserEntityMapper - implements EntityMapper { +final class UserEntityMapper implements EntityMapper { @Override public UserEntity toEntity(@NotNull User user) { @@ -25,7 +21,7 @@ public User toDomain(@NotNull UserEntity entity) { return new User( entity.getUuid(), entity.getName(), - UserTime.ofMillis(entity.getPlaytimeMillis()) + PlayTime.ofMillis(entity.getPlaytimeMillis()) ); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java index 7baa30d..6db799d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java @@ -28,11 +28,10 @@ public final class UserRepositoryOrmLite public UserRepositoryOrmLite( @NotNull PluginLogger logger, @NotNull TaskScheduler taskScheduler, - @NotNull DatabaseBootstrap databaseBootstrap, - @NotNull UserEntityMapper mapper + @NotNull DatabaseBootstrap databaseBootstrap ) { super(logger, taskScheduler, databaseBootstrap); - this.mapper = mapper; + this.mapper = new UserEntityMapper(); } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java index 8bdaaa4..5b4b2a6 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java @@ -31,9 +31,10 @@ boolean isUsable(int requestedLimit, @NotNull Duration expireAfter, @NotNull Ins return expiresAt.isAfter(now); } + @NotNull @Override @Unmodifiable - public @NotNull List users() { + public List users() { return users; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java index 6ca3862..7ce728e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java @@ -2,6 +2,8 @@ import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.repository.UserRepository; @@ -54,7 +56,7 @@ public CompletableFuture> getTopByPlayTime() { return loadTop(limit) .thenApply(users -> { - CachedLeaderboard updated = new CachedLeaderboard(users, limit, Instant.now()); + final CachedLeaderboard updated = new CachedLeaderboard(users, limit, Instant.now()); cachedLeaderboard.set(updated); return slice(updated.users(), limit); }) @@ -66,6 +68,7 @@ public CompletableFuture> getTopByPlayTime() { @Override + @Subscribe(event = PlayTimeShutdownEvent.class) public void invalidateAll() { cachedLeaderboard.set(null); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java index 6270415..bae6e0f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java @@ -8,7 +8,6 @@ public interface TopUsersCache { CompletableFuture> getTopByPlayTime(); - CompletableFuture> getTopByPlayTime(int limit); void invalidateAll(); diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityMapperTest.java index 0f814b5..c74ca1f 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityMapperTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityMapperTest.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.user.repository; import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserTime; +import com.github.imdmk.playtime.PlayTime; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -16,7 +16,7 @@ class UserEntityMapperTest { @Test void toEntityShouldMapFieldsCorrectly() { var uuid = UUID.randomUUID(); - var user = new User(uuid, "DMK", UserTime.ofMillis(3000)); + var user = new User(uuid, "DMK", PlayTime.ofMillis(3000)); var entity = mapper.toEntity(user); From 6e942114ed16bbc40380cbc542c02b5bbbf37ef4 Mon Sep 17 00:00:00 2001 From: imDMK Date: Mon, 12 Jan 2026 19:47:07 +0100 Subject: [PATCH 11/17] Continue work. --- .../com/github/imdmk/playtime/PlayTime.java | 2 +- .../github/imdmk/playtime/PlayTimeApi.java | 11 +- .../imdmk/playtime/PlayTimeApiProvider.java | 4 - .../imdmk/playtime/PlayTimeService.java | 14 -- .../com/github/imdmk/playtime/user/User.java | 77 -------- .../imdmk/playtime/user/UserService.java | 26 --- .../com/github/imdmk/playtime/UserTest.java | 26 +-- .../imdmk/playtime/PlayTimeChangedEvent.java | 55 ++++++ .../imdmk/playtime/UserDeleteEvent.java | 36 ---- .../github/imdmk/playtime/UserSaveEvent.java | 35 ---- .../imdmk/playtime/PlayTimeApiAdapter.java | 10 -- .../github/imdmk/playtime/PlayTimePlugin.java | 5 +- .../repository/ormlite/EntityMapper.java | 4 +- .../repository/ormlite/OrmLiteRepository.java | 14 +- .../playtime/BukkitPlayTimeService.java | 59 ------- .../feature/playtime/PlayTimeApiAdapter.java | 34 ++++ .../feature/playtime/PlayTimeCommand.java | 75 ++++++++ .../feature/playtime/PlayTimePlaceholder.java | 33 ++++ .../playtime/PlayTimeSaveListener.java | 54 ++++++ .../feature/playtime/PlayTimeUser.java | 59 +++++++ .../feature/playtime/PlayTimeUserEntity.java | 69 ++++++++ .../playtime/PlayTimeUserEntityMapper.java | 25 +++ .../playtime/PlayTimeUserEntityMeta.java} | 6 +- .../feature/playtime/PlayTimeUserFactory.java | 46 ----- .../playtime/PlayTimeUserRepository.java | 19 ++ .../PlayTimeUserRepositoryOrmLite.java | 91 ++++++++++ .../feature/playtime/PlayTimeUserService.java | 72 ++++++++ .../feature/playtime/command/TimeCommand.java | 59 ------- .../playtime/command/TimeResetAllCommand.java | 97 ---------- .../playtime/command/TimeResetCommand.java | 58 ------ .../playtime/command/TimeSetCommand.java | 69 -------- .../playtime/command/TimeTopCommand.java | 52 ------ .../command/TimeTopInvalidateCommand.java | 36 ---- .../feature/playtime/gui/PlayTimeTopGui.java | 98 ++++------ .../playtime/gui/PlayTimeTopGuiConfig.java | 46 +---- .../listener/PlayTimeSaveController.java | 42 ----- .../playtime/messages/ENPlayTimeMessages.java | 104 ++--------- .../playtime/messages/PlayTimeMessages.java | 12 +- .../placeholder/PlayTimePlaceholder.java | 32 ---- .../feature/reload/ReloadCommand.java | 12 +- .../processor/ComponentProcessors.java | 4 - .../injector/subscriber/LocalPublisher.java | 3 +- .../platform/event/BukkitEventCaller.java | 4 +- .../platform/gui/IdentifiableGui.java | 3 +- .../platform/gui/config/GuiConfig.java | 4 +- .../platform/identity/IdentityCache.java | 36 ++++ .../platform/identity/IdentityController.java | 31 ++++ .../litecommands/LiteCommandsConfigurer.java | 2 +- .../argument/UUIDArgumentResolver.java | 34 ++++ .../handler/InvalidUsageHandlerImpl.java | 6 +- .../MissingPermissionsHandlerImpl.java | 6 +- .../handler/NoticeResultHandlerImpl.java | 6 +- .../platform/metrics/BMetricsService.java | 5 +- .../scheduler/BukkitTaskScheduler.java | 18 +- .../platform/scheduler/TaskScheduler.java | 4 - .../{ => shared}/message/MessageConfig.java | 4 +- .../{ => shared}/message/MessageService.java | 2 +- .../time/DurationFormatStyle.java | 2 +- .../{ => shared}/time/DurationSplitter.java | 2 +- .../{ => shared}/time/DurationUnit.java | 2 +- .../playtime/{ => shared}/time/Durations.java | 2 +- .../imdmk/playtime/user/UserArgument.java | 77 -------- .../imdmk/playtime/user/UserFactory.java | 12 -- .../imdmk/playtime/user/UserServiceImpl.java | 155 ---------------- .../user/cache/CaffeineUserCache.java | 134 -------------- .../imdmk/playtime/user/cache/UserCache.java | 28 --- .../user/controller/UserJoinController.java | 110 ------------ .../user/controller/UserQuitController.java | 46 ----- .../playtime/user/repository/UserEntity.java | 78 -------- .../user/repository/UserEntityMapper.java | 27 --- .../user/repository/UserRepository.java | 24 --- .../repository/UserRepositoryOrmLite.java | 167 ------------------ .../playtime/user/top/CachedLeaderboard.java | 40 ----- .../user/top/MemoryTopUsersCache.java | 84 --------- .../playtime/user/top/TopUsersCache.java | 14 -- .../user/top/TopUsersCacheConfig.java | 66 ------- ...PlayTimePlayTimeUserEntityMapperTest.java} | 10 +- ...yTest.java => PlayTimeUserEntityTest.java} | 23 +-- playtime-plugin/build.gradle.kts | 2 +- 79 files changed, 840 insertions(+), 2115 deletions(-) delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java delete mode 100644 playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java create mode 100644 playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/PlayTimeChangedEvent.java delete mode 100644 playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java delete mode 100644 playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUser.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntityMapper.java rename playtime-core/src/main/java/com/github/imdmk/playtime/{user/repository/UserEntityMeta.java => feature/playtime/PlayTimeUserEntityMeta.java} (64%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java rename playtime-core/src/main/java/com/github/imdmk/playtime/{ => shared}/message/MessageConfig.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{ => shared}/message/MessageService.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{ => shared}/time/DurationFormatStyle.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{ => shared}/time/DurationSplitter.java (92%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{ => shared}/time/DurationUnit.java (97%) rename playtime-core/src/main/java/com/github/imdmk/playtime/{ => shared}/time/Durations.java (96%) delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java delete mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java rename playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/{UserEntityMapperTest.java => PlayTimePlayTimeUserEntityMapperTest.java} (77%) rename playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/{UserEntityTest.java => PlayTimeUserEntityTest.java} (64%) diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java index 171fcd1..84b1e69 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTime.java @@ -13,7 +13,7 @@ public record PlayTime(long millis) implements Comparable { public PlayTime { if (millis < 0L) { - throw new IllegalArgumentException("UserTime millis cannot be negative"); + throw new IllegalArgumentException("PlayTime millis cannot be negative"); } } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java index a0a59f7..ddb78ad 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java @@ -1,11 +1,16 @@ package com.github.imdmk.playtime; -import com.github.imdmk.playtime.user.UserService; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; public interface PlayTimeApi { - UserService getUserService(); + CompletableFuture getTime(@NotNull UUID uuid); + + CompletableFuture setTime(@NotNull UUID uuid, @NotNull PlayTime time); - PlayTimeService getPlayTimeService(); + CompletableFuture resetTime(@NotNull UUID uuid); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java index 526a273..e93a32d 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java @@ -29,10 +29,6 @@ static synchronized void register(@NotNull PlayTimeApi api) { API = api; } - static synchronized void forceRegister(@NotNull PlayTimeApi api) { - API = api; - } - static synchronized void unregister() { if (API == null) { throw new IllegalStateException("PlayTimeAPI is not registered."); diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java deleted file mode 100644 index 6a2c11f..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.imdmk.playtime; - -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -public interface PlayTimeService { - - PlayTime getTime(@NotNull UUID uuid); - - void setTime(@NotNull UUID uuid, @NotNull PlayTime time); - - void resetTime(@NotNull UUID uuid); -} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java deleted file mode 100644 index 8cff3a7..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.imdmk.playtime.user; - -import com.github.imdmk.playtime.PlayTime; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.UUID; - -public final class User { - - private final UUID uuid; - private String name; - - private PlayTime playtime; - - public User( - @NotNull UUID uuid, - @NotNull String name, - @NotNull PlayTime playtime - ) { - this.uuid = uuid; - this.name = name; - this.playtime = playtime; - } - - public User( - @NotNull UUID uuid, - @NotNull String name - ) { - this.uuid = uuid; - this.name = name; - this.playtime = PlayTime.ZERO; - } - - @NotNull - public UUID getUuid() { - return uuid; - } - - @NotNull - public String getName() { - return name; - } - - public void setName(@NotNull String name) { - this.name = name; - } - - @NotNull - public PlayTime getPlaytime() { - return playtime; - } - - public void setPlaytime(@NotNull PlayTime playtime) { - this.playtime = playtime; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof User user)) return false; - return Objects.equals(uuid, user.uuid); - } - - @Override - public int hashCode() { - return Objects.hashCode(uuid); - } - - @Override - public String toString() { - return "User{" + - "uuid=" + uuid + - ", name='" + name + '\'' + - ", time=" + playtime.millis() + - '}'; - } -} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java deleted file mode 100644 index a4fa5eb..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.imdmk.playtime.user; - -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -public interface UserService { - - Optional findCachedByUuid(@NotNull UUID uuid); - Optional findCachedByName(@NotNull String name); - Collection getCachedUsers(); - - CompletableFuture> findByUuid(@NotNull UUID uuid); - CompletableFuture> findByName(@NotNull String name); - - CompletableFuture deleteByUuid(@NotNull UUID uuid); - CompletableFuture deleteByName(@NotNull String name); - - CompletableFuture save(@NotNull User user); - - CompletableFuture> findTopByPlayTime(int limit); -} diff --git a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java b/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java index 15fd16c..ed93732 100644 --- a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java +++ b/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java @@ -26,14 +26,14 @@ void shouldInitializeFieldsCorrectly() { assertThat(user.getUuid()).isEqualTo(uuid); assertThat(user.getName()).isEqualTo("Player"); - assertThat(user.getPlaytime()).isEqualTo(time); + assertThat(user.getPlayTime()).isEqualTo(time); } @Test - void shouldCreateUserWithZeroPlaytime() { + void shouldCreateUserWithZeroPlayTime() { User user = new User(UUID.randomUUID(), "Player"); - assertThat(user.getPlaytime()).isEqualTo(PlayTime.ZERO); + assertThat(user.getPlayTime()).isEqualTo(PlayTime.ZERO); } @Test @@ -46,7 +46,7 @@ void shouldThrowWhenNameIsNull() { } @Test - void shouldThrowWhenPlaytimeIsNull() { + void shouldThrowWhenPlayTimeIsNull() { UUID uuid = UUID.randomUUID(); assertThatNullPointerException() @@ -88,31 +88,31 @@ void shouldRejectBlankName() { } @Nested - @DisplayName("Playtime mutation") - class PlaytimeTests { + @DisplayName("PlayTime mutation") + class PlayTimeTests { @Test - void shouldReturnCurrentPlaytimeAsUserTime() { + void shouldReturnCurrentPlayTimeAsUserTime() { User user = new User(UUID.randomUUID(), "Player", PlayTime.ofMillis(1000)); - assertThat(user.getPlaytime().millis()).isEqualTo(1000); + assertThat(user.getPlayTime().millis()).isEqualTo(1000); } @Test - void shouldSetNewPlaytime() { + void shouldSetNewPlayTime() { User user = new User(UUID.randomUUID(), "Player"); - user.setPlaytime(PlayTime.ofMillis(12345)); + user.setPlayTime(PlayTime.ofMillis(12345)); - assertThat(user.getPlaytime().millis()).isEqualTo(12345); + assertThat(user.getPlayTime().millis()).isEqualTo(12345); } @Test - void shouldRejectNullPlaytime() { + void shouldRejectNullPlayTime() { User user = new User(UUID.randomUUID(), "Player"); assertThatNullPointerException() - .isThrownBy(() -> user.setPlaytime(null)) + .isThrownBy(() -> user.setPlayTime(null)) .withMessageContaining("playtime"); } } diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/PlayTimeChangedEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/PlayTimeChangedEvent.java new file mode 100644 index 0000000..658e016 --- /dev/null +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/PlayTimeChangedEvent.java @@ -0,0 +1,55 @@ +package com.github.imdmk.playtime; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public final class PlayTimeChangedEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + private static final boolean ASYNC = true; + + private final UUID playerId; + private final PlayTime newTime; + private final PlayTime oldTime; + + public PlayTimeChangedEvent( + @NotNull UUID playerId, + @NotNull PlayTime newTime, + @NotNull PlayTime oldTime + ) { + super(ASYNC); + this.playerId = playerId; + this.oldTime = oldTime; + this.newTime = newTime; + } + + @NotNull + public UUID getPlayerId() { + return playerId; + } + + @NotNull + public PlayTime getNewTime() { + return newTime; + } + + @NotNull + public PlayTime getOldTime() { + return oldTime; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLERS; + } + +} diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java deleted file mode 100644 index e78fe61..0000000 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.User; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.NotNull; - -public final class UserDeleteEvent extends Event { - - private static final HandlerList HANDLERS = new HandlerList(); - private static final boolean ASYNC = true; - - private final User user; - - public UserDeleteEvent(@NotNull User user) { - super(ASYNC); - this.user = user; - } - - @NotNull - public User getUser() { - return user; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return HANDLERS; - } - - @NotNull - public static HandlerList getHandlerList() { - return HANDLERS; - } - -} diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java deleted file mode 100644 index a041925..0000000 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.User; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.NotNull; - -public final class UserSaveEvent extends Event { - - private static final HandlerList HANDLERS = new HandlerList(); - private static final boolean ASYNC = true; - - private final User user; - - public UserSaveEvent(@NotNull User user) { - super(ASYNC); - this.user = user; - } - - @NotNull - public User getUser() { - return this.user; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return HANDLERS; - } - - @NotNull - public static HandlerList getHandlerList() { - return HANDLERS; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java deleted file mode 100644 index ece0e34..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.UserService; -import org.panda_lang.utilities.inject.annotations.Inject; - -@Inject -record PlayTimeApiAdapter( - UserService getUserService, - PlayTimeService getPlayTimeService -) implements PlayTimeApi {} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index 2542631..52f7d4b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime; +import com.github.imdmk.playtime.feature.playtime.PlayTimeApiAdapter; import com.github.imdmk.playtime.injector.ComponentManager; import com.github.imdmk.playtime.injector.processor.ComponentProcessors; import com.github.imdmk.playtime.injector.subscriber.LocalPublisher; @@ -46,11 +47,11 @@ final class PlayTimePlugin { componentManager.scanAll(); componentManager.processAll(); - this.publisher.publish(new PlayTimeInitializeEvent()); - final PlayTimeApi api = injector.newInstance(PlayTimeApiAdapter.class); PlayTimeApiProvider.register(api); + this.publisher.publish(new PlayTimeInitializeEvent()); + final long elapsedMillis = stopwatch.elapsed(TimeUnit.MILLISECONDS); logger.info("Successfully loaded plugin in " + elapsedMillis + "ms!"); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java index b0b42ba..baf0428 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java @@ -12,10 +12,10 @@ public interface EntityMapper { D toDomain(@NotNull E entity); default List toDomainList(@NotNull List entities) { - return entities.stream().map(this::toDomain).collect(Collectors.toList()); + return entities.stream().map(this::toDomain).toList(); } default List toEntityList(@NotNull List domains) { - return domains.stream().map(this::toEntity).collect(Collectors.toList()); + return domains.stream().map(this::toEntity).toList(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java index 2e881cf..05b81b2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java @@ -28,10 +28,10 @@ public abstract class OrmLiteRepository protected final PluginLogger logger; protected final TaskScheduler scheduler; - private final DatabaseBootstrap databaseBootstrap; - protected volatile Dao dao; + private final DatabaseBootstrap databaseBootstrap; + @Inject protected OrmLiteRepository( @NotNull PluginLogger logger, @@ -46,7 +46,9 @@ protected OrmLiteRepository( protected abstract Class entityClass(); - protected abstract List> entitySubClasses(); + protected List> entitySubClasses() { + return List.of(); + } @Override public void start() throws RepositoryInitializationException { @@ -86,6 +88,10 @@ public void close() { } protected CompletableFuture execute(@NotNull Supplier supplier) { + if (dao == null) { + throw new IllegalStateException("Repository not initialized or already closed"); + } + final CompletableFuture future = new CompletableFuture<>(); scheduler.runAsync(() -> { try { @@ -97,7 +103,7 @@ protected CompletableFuture execute(@NotNull Supplier supplier) { return future.orTimeout(EXECUTE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); } - private void configureOrmLiteLogger() { + private static void configureOrmLiteLogger() { Logger.setGlobalLogLevel(Level.ERROR); // only errors } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java deleted file mode 100644 index 61a7f53..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime; - -import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.ComponentPriority; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.PlayTime; -import org.bukkit.OfflinePlayer; -import org.bukkit.Server; -import org.bukkit.Statistic; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.UUID; - -@Service(priority = ComponentPriority.LOWEST) -final class BukkitPlayTimeService implements PlayTimeService { - - private static final Statistic PLAYTIME_STATISTIC = Statistic.PLAY_ONE_MINUTE; - - private final Server server; - - @Inject - BukkitPlayTimeService(@NotNull Server server) { - this.server = server; - } - - @Override - public @NotNull PlayTime getTime(@NotNull UUID uuid) { - checkPrimaryThread(); - - final int timeTicks = getOffline(uuid).getStatistic(PLAYTIME_STATISTIC); - if (timeTicks <= 0) { - return PlayTime.ZERO; - } - - return PlayTime.ofTicks(timeTicks); - } - - @Override - public void setTime(@NotNull UUID uuid, @NotNull PlayTime time) { - checkPrimaryThread(); - getOffline(uuid).setStatistic(PLAYTIME_STATISTIC, time.toTicks()); - } - - @Override - public void resetTime(@NotNull UUID uuid) { - setTime(uuid, PlayTime.ZERO); - } - - private OfflinePlayer getOffline(UUID uuid) { - return server.getOfflinePlayer(uuid); - } - - private void checkPrimaryThread() { - if (!server.isPrimaryThread()) { - throw new UnsupportedOperationException("BukkitPlaytimeService must be called from the primary thread."); - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java new file mode 100644 index 0000000..4db898c --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java @@ -0,0 +1,34 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.PlayTimeApi; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class PlayTimeApiAdapter implements PlayTimeApi { + + private final PlayTimeUserService userService; + + @Inject + PlayTimeApiAdapter(@NotNull PlayTimeUserService userService) { + this.userService = userService; + } + + @Override + public CompletableFuture getTime(@NotNull UUID uuid) { + return userService.getPlayTime(uuid); + } + + @Override + public CompletableFuture setTime(@NotNull UUID uuid, @NotNull PlayTime time) { + return userService.setPlayTime(uuid, time); + } + + @Override + public CompletableFuture resetTime(@NotNull UUID uuid) { + return userService.setPlayTime(uuid, PlayTime.ZERO); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java new file mode 100644 index 0000000..5422acc --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java @@ -0,0 +1,75 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; +import com.github.imdmk.playtime.shared.message.MessageService; +import com.github.imdmk.playtime.shared.time.Durations; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.annotations.execute.Execute; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.time.Duration; +import java.util.UUID; + +@LiteCommand +@Command(name = "playtime") +final class PlayTimeCommand { + + private final MessageService messageService; + private final PlayTimeUserService userService; + + @Inject + PlayTimeCommand(@NotNull MessageService messageService, @NotNull PlayTimeUserService userService) { + this.messageService = messageService; + this.userService = userService; + } + + @Execute + void playTime(@Context Player player) { + userService.getPlayTime(player.getUniqueId()) + .thenAccept(playTime -> messageService.create() + .viewer(player) + .notice(n -> n.playtimeMessages.playerPlayTimeSelf()) + .placeholder("{PLAYER_PLAYTIME}", Durations.format(playTime.toDuration())) + .send()) + .exceptionally(e -> { + messageService.send(player, notice -> notice.actionExecutionError); + return null; + }); + } + + @Execute + void playTimeOther(@Context CommandSender sender, @Arg UUID playerId) { + userService.getPlayTime(playerId) + .thenAccept(playTime -> messageService.create() + .viewer(sender) + .notice(n -> n.playtimeMessages.playerPlayTimeTarget()) + .placeholder("{PLAYER_PLAYTIME}", Durations.format(playTime.toDuration())) + .send()) + .exceptionally(e -> { + messageService.send(sender, notice -> notice.actionExecutionError); + return null; + }); + } + + @Execute + void setPlayTime(@Context CommandSender sender, @Arg UUID playerId, @Arg Duration duration) { + final PlayTime playtime = PlayTime.of(duration); + + userService.setPlayTime(playerId, playtime) + .thenAccept(v -> messageService.create() + .viewer(sender) + .notice(n -> n.playtimeMessages.playerPlayTimeUpdated()) + .placeholder("{PLAYER_PLAYTIME}", Durations.format(duration)) + .send()) + .exceptionally(e -> { + messageService.send(sender, notice -> notice.actionExecutionError); + return null; + }); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java new file mode 100644 index 0000000..40109c4 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java @@ -0,0 +1,33 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; +import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; +import com.github.imdmk.playtime.shared.time.Durations; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@Placeholder +final class PlayTimePlaceholder implements PluginPlaceholder { + + private static final String IDENTIFIER = "playtime"; + + private final PlayTimeUserService userService; + + @Inject + PlayTimePlaceholder(@NotNull PlayTimeUserService userService) { + this.userService = userService; + } + + @Override + public String identifier() { + return IDENTIFIER; + } + + @Override + public String request(@NotNull Player player, @NotNull String params) { + return Durations.format( + userService.getPlayTime(player.getUniqueId()).toDuration() + ); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java new file mode 100644 index 0000000..ff2bb11 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java @@ -0,0 +1,54 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.injector.annotations.Controller; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.UUID; + +@Controller +final class PlayTimeSaveListener implements Listener { + + private static final Statistic PLAYTIME_STATISTIC = Statistic.PLAY_ONE_MINUTE; + + private final PlayTimeUserService userService; + + @Inject + PlayTimeSaveListener(PlayTimeUserService userService) { + this.userService = userService; + } + + @EventHandler + void onPlayerJoin(PlayerJoinEvent event) { + final Player player = event.getPlayer(); + final UUID uuid = player.getUniqueId(); + + final PlayTimeUser user = userService.getOrCreate( + uuid, + PlayTime.ofTicks(player.getStatistic(PLAYTIME_STATISTIC)) + ); + + player.setStatistic( + PLAYTIME_STATISTIC, + user.getPlayTime().toTicks() + ); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + final Player player = event.getPlayer(); + final UUID uuid = player.getUniqueId(); + + final int playTimeTicks = player.getStatistic(PLAYTIME_STATISTIC); + final PlayTime playTime = PlayTime.ofTicks(playTimeTicks); + + userService.setPlayTime(uuid, playTime); + } + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUser.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUser.java new file mode 100644 index 0000000..fb713b9 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUser.java @@ -0,0 +1,59 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.PlayTime; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.UUID; + +public final class PlayTimeUser { + + private final UUID uuid; + private PlayTime playTime; + + PlayTimeUser(@NotNull UUID uuid) { + this.uuid = uuid; + this.playTime = PlayTime.ZERO; + } + + PlayTimeUser(@NotNull UUID uuid, @NotNull PlayTime playTime) { + this.uuid = uuid; + this.playTime = playTime; + } + + @NotNull + public UUID getUuid() { + return uuid; + } + + @NotNull + public PlayTime getPlayTime() { + return playTime; + } + + public void setPlayTime(@NotNull PlayTime playTime) { + this.playTime = playTime; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PlayTimeUser that)) { + return false; + } + + return Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public String toString() { + return "PlayTimeUser{" + + "uuid=" + uuid + + ", playTime=" + playTime + + '}'; + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java new file mode 100644 index 0000000..884fd92 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java @@ -0,0 +1,69 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.UUID; + +@DatabaseTable(tableName = PlayTimeUserEntityMeta.TABLE) +final class PlayTimeUserEntity { + + @DatabaseField(id = true, canBeNull = false, columnName = PlayTimeUserEntityMeta.Col.UUID) + private UUID uuid; + + @DatabaseField(canBeNull = false, columnName = PlayTimeUserEntityMeta.Col.PLAYTIME_MILLIS) + private long playtimeMillis; + + PlayTimeUserEntity() {} + + PlayTimeUserEntity( + @NotNull UUID uuid, + long playtimeMillis + ) { + this.uuid = uuid; + this.playtimeMillis = playtimeMillis; + } + + public UUID getUuid() { + return this.uuid; + } + + public void setUuid(@NotNull UUID uuid) { + this.uuid = uuid; + } + + public long getPlayTimeMillis() { + return playtimeMillis; + } + + public void setPlayTimeMillis(long playtimeMillis) { + this.playtimeMillis = playtimeMillis; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PlayTimeUserEntity other)) { + return false; + } + + return uuid.equals(other.uuid); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public String toString() { + return "UserEntity{" + + "uuid=" + this.uuid + + ", spentMillis=" + this.playtimeMillis + + '}'; + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntityMapper.java new file mode 100644 index 0000000..1de94fe --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntityMapper.java @@ -0,0 +1,25 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; +import org.jetbrains.annotations.NotNull; + +final class PlayTimeUserEntityMapper + implements EntityMapper { + + @Override + public PlayTimeUserEntity toEntity(@NotNull PlayTimeUser user) { + return new PlayTimeUserEntity( + user.getUuid(), + user.getPlayTime().millis() + ); + } + + @Override + public PlayTimeUser toDomain(@NotNull PlayTimeUserEntity entity) { + return new PlayTimeUser( + entity.getUuid(), + PlayTime.ofMillis(entity.getPlayTimeMillis()) + ); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntityMeta.java similarity index 64% rename from playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntityMeta.java index ea61d5b..79f1c65 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntityMeta.java @@ -1,8 +1,8 @@ -package com.github.imdmk.playtime.user.repository; +package com.github.imdmk.playtime.feature.playtime; import com.github.imdmk.playtime.database.repository.ormlite.EntityMeta; -interface UserEntityMeta extends EntityMeta { +interface PlayTimeUserEntityMeta extends EntityMeta { String TABLE = "advanced_playtime_users"; @@ -10,8 +10,6 @@ interface Col { String UUID = "uuid"; - String NAME = "name"; - String PLAYTIME_MILLIS = "playtimeMillis"; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java deleted file mode 100644 index 85f0e01..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime; - -import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.ComponentPriority; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserFactory; -import com.github.imdmk.playtime.PlayTime; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.Optional; -import java.util.UUID; - -@Service(priority = ComponentPriority.LOW) -public final class PlayTimeUserFactory implements UserFactory { - - private static final String UNKNOWN_PLAYER_NAME_FORMAT = "Unknown:%s"; - - private final PlayTimeService playtimeService; - - @Inject - public PlayTimeUserFactory(@NotNull PlayTimeService playtimeService) { - this.playtimeService = playtimeService; - } - - @Override - public @NotNull User createFrom(@NotNull Player player) { - final UUID uuid = player.getUniqueId(); - final String name = player.getName(); - final PlayTime time = playtimeService.getTime(uuid); - - return new User(uuid, name, time); - } - - @Override - public @NotNull User createFrom(@NotNull OfflinePlayer player) { - final UUID uuid = player.getUniqueId(); - final String name = Optional.ofNullable(player.getName()).orElse(UNKNOWN_PLAYER_NAME_FORMAT.formatted(uuid)); - final PlayTime time = playtimeService.getTime(uuid); - - return new User(uuid, name, time); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java new file mode 100644 index 0000000..ec2ac16 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java @@ -0,0 +1,19 @@ +package com.github.imdmk.playtime.feature.playtime; + +import org.jetbrains.annotations.NotNull; +import panda.std.reactive.Completable; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +interface PlayTimeUserRepository { + + CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture> findAll(); + + CompletableFuture deleteByUuid(@NotNull UUID uuid); + + CompletableFuture save(@NotNull PlayTimeUser user); +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java new file mode 100644 index 0000000..a8209e6 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java @@ -0,0 +1,91 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.database.DatabaseBootstrap; +import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; +import com.github.imdmk.playtime.injector.annotations.Repository; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Repository +final class PlayTimeUserRepositoryOrmLite + extends OrmLiteRepository + implements PlayTimeUserRepository { + + private final PlayTimeUserEntityMapper mapper; + + @Inject + PlayTimeUserRepositoryOrmLite( + @NotNull PluginLogger logger, + @NotNull TaskScheduler taskScheduler, + @NotNull DatabaseBootstrap databaseBootstrap + ) { + super(logger, taskScheduler, databaseBootstrap); + this.mapper = new PlayTimeUserEntityMapper(); + } + + @Override + protected Class entityClass() { + return PlayTimeUserEntity.class; + } + + @Override + public CompletableFuture> findByUuid(@NotNull UUID uuid) { + return execute(() -> { + try { + return Optional.ofNullable(dao.queryForId(uuid)) + .map(mapper::toDomain); + } catch (SQLException e) { + logger.error(e, "Failed to find user with uuid %s", uuid); + throw new IllegalStateException("Database failure", e); + } + }); + } + + @Override + public CompletableFuture> findAll() { + return execute(() -> { + try { + return dao.queryForAll().stream() + .map(mapper::toDomain) + .toList(); + } catch (SQLException e) { + logger.error(e, "Failed to query all users"); + throw new IllegalStateException("Database failure", e); + } + }); + } + + @Override + public CompletableFuture deleteByUuid(@NotNull UUID uuid) { + return execute(() -> { + try { + final int rows = dao.deleteById(uuid); + return rows > 0; + } catch (SQLException e) { + logger.error(e, "Failed to delete user with uuid %s", uuid); + throw new IllegalStateException("Database failure", e); + } + }); + } + + @Override + public CompletableFuture save(@NotNull PlayTimeUser user) { + execute(() -> { + try { + dao.createOrUpdate(mapper.toEntity(user)); + return null; + } catch (SQLException e) { + logger.error(e, "Failed to save user with uuid %s", user.getUuid()); + throw new IllegalStateException("Database failure", e); + } + }); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java new file mode 100644 index 0000000..0f54103 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java @@ -0,0 +1,72 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.injector.annotations.Service; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Service +class PlayTimeUserService { + + private final PlayTimeUserRepository repository; + + @Inject + PlayTimeUserService(@NotNull PlayTimeUserRepository repository) { + this.repository = repository; + } + + CompletableFuture getOrCreate( + @NotNull UUID uuid, + @NotNull PlayTime initialPlayTime + ) { + return repository.findByUuid(uuid) + .thenCompose(optional -> { + if (optional.isPresent()) { + return CompletableFuture.completedFuture(optional.get()); + } + + PlayTimeUser user = new PlayTimeUser(uuid, initialPlayTime); + return repository.save(user) + .thenApply(v -> user); + }); + } + + CompletableFuture getOrCreate(@NotNull UUID uuid) { + return getOrCreate(uuid, PlayTime.ZERO); + } + + CompletableFuture getPlayTime(@NotNull UUID uuid) { + return getOrCreate(uuid) + .thenApply(PlayTimeUser::getPlayTime); + } + + CompletableFuture setPlayTime( + @NotNull UUID uuid, + @NotNull PlayTime playTime + ) { + return getOrCreate(uuid) + .thenCompose(user -> { + user.setPlayTime(playTime); + return repository.save(user); + }); + } + + CompletableFuture addPlayTime( + @NotNull UUID uuid, + @NotNull PlayTime delta + ) { + return getOrCreate(uuid) + .thenCompose(user -> { + user.setPlayTime(user.getPlayTime().plus(delta)); + return repository.save(user); + }); + } + + CompletableFuture delete(@NotNull UUID uuid) { + return repository.deleteByUuid(uuid); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java deleted file mode 100644 index 4e4f153..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.command; - -import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.time.Durations; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.PlayTime; -import dev.rollczi.litecommands.annotations.argument.Arg; -import dev.rollczi.litecommands.annotations.async.Async; -import dev.rollczi.litecommands.annotations.command.Command; -import dev.rollczi.litecommands.annotations.context.Context; -import dev.rollczi.litecommands.annotations.execute.Execute; -import dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -@LiteCommand -@Command(name = "playtime") -public final class TimeCommand { - - private final MessageService messageService; - private final PlayTimeService playtimeService; - - @Inject - public TimeCommand( - @NotNull MessageService messageService, - @NotNull PlayTimeService playtimeService - ) { - this.messageService = messageService; - this.playtimeService = playtimeService; - } - - @Execute - @Permission("command.playtime") - void selfPlaytime(@Context Player viewer) { - final PlayTime time = playtimeService.getTime(viewer.getUniqueId()); - - messageService.create() - .notice(n -> n.playtimeMessages.playerPlaytimeSelf()) - .viewer(viewer) - .placeholder("{PLAYER_PLAYTIME}", Durations.format(time.toDuration())) - .send(); - } - - @Execute - @Permission("command.playtime.target") - void targetPlaytime(@Context Player viewer, @Arg @Async User target) { - final PlayTime time = playtimeService.getTime(target.getUuid()); - - messageService.create() - .notice(n -> n.playtimeMessages.playerPlaytimeTarget()) - .viewer(viewer) - .placeholder("{PLAYER_NAME}", target.getName()) - .placeholder("{PLAYER_PLAYTIME}", Durations.format(time.toDuration())) - .send(); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java deleted file mode 100644 index 1d117b2..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.command; - -import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.PlayTime; -import com.github.imdmk.playtime.user.repository.UserRepository; -import dev.rollczi.litecommands.annotations.command.Command; -import dev.rollczi.litecommands.annotations.context.Context; -import dev.rollczi.litecommands.annotations.execute.Execute; -import dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.OfflinePlayer; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.Arrays; -import java.util.concurrent.CompletableFuture; - -@LiteCommand -@Command(name = "playtime reset-all") -@Permission("command.playtime.reset.all") -public final class TimeResetAllCommand { - - private static final UserSaveReason SAVE_REASON = UserSaveReason.RESET_COMMAND; - - private final Server server; - private final PluginLogger logger; - private final MessageService messageService; - private final PlayTimeService playtimeService; - private final UserService userService; - private final UserRepository userRepository; - private final TaskScheduler taskScheduler; - - @Inject - public TimeResetAllCommand( - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull MessageService messageService, - @NotNull PlayTimeService playtimeService, - @NotNull UserService userService, - @NotNull UserRepository userRepository, - @NotNull TaskScheduler taskScheduler - ) { - this.server = server; - this.logger = logger; - this.messageService = messageService; - this.playtimeService = playtimeService; - this.userService = userService; - this.userRepository = userRepository; - this.taskScheduler = taskScheduler; - } - - @Execute - void resetAll(@Context CommandSender sender) { - messageService.send(sender, n -> n.playtimeMessages.playerPlaytimeResetAllStarted()); - - taskScheduler.runSync(this::resetOfflinePlayers); - userRepository.findAll() - .thenCompose(users -> { - if (users.isEmpty()) { - return CompletableFuture.completedFuture(false); - } - - return CompletableFuture.allOf(users.stream() - .map(this::resetUser) - .toArray(CompletableFuture[]::new)) - .thenApply(v -> true); - }) - .whenComplete((b, e) -> { - if (e != null) { - logger.error(e, "Failed to reset playtime for all users"); - messageService.send(sender, n -> n.playtimeMessages.playerPlaytimeResetAllFailed()); - return; - } - - messageService.send(sender, n -> n.playtimeMessages.playerPlaytimeResetAllFinished()); - }); - } - - private CompletableFuture resetUser(User user) { - user.setPlaytime(PlayTime.ZERO); - return userService.save(user, SAVE_REASON); - } - - private void resetOfflinePlayers() { - Arrays.stream(server.getOfflinePlayers()) - .map(OfflinePlayer::getUniqueId) - .forEach(playtimeService::resetTime); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java deleted file mode 100644 index b61e397..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.command; - -import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.PlayTime; -import dev.rollczi.litecommands.annotations.argument.Arg; -import dev.rollczi.litecommands.annotations.async.Async; -import dev.rollczi.litecommands.annotations.command.Command; -import dev.rollczi.litecommands.annotations.context.Context; -import dev.rollczi.litecommands.annotations.execute.Execute; -import dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -@LiteCommand -@Command(name = "playtime reset") -@Permission("command.playtime.reset") -public final class TimeResetCommand { - - private static final UserSaveReason SAVE_REASON = UserSaveReason.RESET_COMMAND; - - private final PluginLogger logger; - private final MessageService messageService; - private final UserService userService; - - @Inject - public TimeResetCommand( - @NotNull PluginLogger logger, - @NotNull MessageService messageService, - @NotNull UserService userService - ) { - this.logger = logger; - this.messageService = messageService; - this.userService = userService; - } - - @Execute - void reset(@Context CommandSender sender, @Arg @Async User target) { - target.setPlaytime(PlayTime.ZERO); - - userService.save(target, SAVE_REASON) - .thenAccept(user -> messageService.create() - .viewer(sender) - .notice(n -> n.playtimeMessages.playerPlaytimeReset()) - .placeholder("{PLAYER_NAME}", user.getName()) - .send()) - .exceptionally(e -> { - logger.error(e, "An error occurred while trying to reset user playtime"); - messageService.send(sender, n -> n.actionExecutionError); - return null; - }); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java deleted file mode 100644 index 899051d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.command; - -import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.time.Durations; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.PlayTime; -import dev.rollczi.litecommands.annotations.argument.Arg; -import dev.rollczi.litecommands.annotations.async.Async; -import dev.rollczi.litecommands.annotations.command.Command; -import dev.rollczi.litecommands.annotations.context.Context; -import dev.rollczi.litecommands.annotations.execute.Execute; -import dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.time.Duration; - -@LiteCommand -@Command(name = "playtime set") -@Permission("command.playtime.set") -public final class TimeSetCommand { - - private final PluginLogger logger; - private final MessageService messageService; - private final PlayTimeService playTimeService; - private final UserService userService; - - @Inject - public TimeSetCommand( - @NotNull PluginLogger logger, - @NotNull MessageService messageService, - @NotNull PlayTimeService playTimeService, - @NotNull UserService userService - ) { - this.logger = logger; - this.messageService = messageService; - this.playTimeService = playTimeService; - this.userService = userService; - } - - @Execute - void setPlaytime(@Context CommandSender sender, @Arg @Async User target, @Arg Duration time) { - final Duration normalizedTime = Durations.clamp(time); - final PlayTime newTime = PlayTime.ofDuration(normalizedTime); - - target.setPlaytime(newTime); - playTimeService.setTime(target.getUuid(), newTime); - - userService.save(target, UserSaveReason.SET_COMMAND) - .thenAccept(v -> messageService.create() - .notice(n -> n.playtimeMessages.playerPlaytimeUpdated()) - .placeholder("{PLAYER_NAME}", target.getName()) - .placeholder("{PLAYER_PLAYTIME}", Durations.format(normalizedTime)) - .viewer(sender) - .send() - ) - .exceptionally(e -> { - logger.error(e, "Failed to save user on playtime set command (target=%s)", target.getName()); - messageService.send(sender, n -> n.actionExecutionError); - return null; - }); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java deleted file mode 100644 index 58627b3..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.command; - -import com.github.imdmk.playtime.feature.playtime.gui.PlayTimeTopGui; -import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.platform.gui.view.GuiOpener; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.user.UserService; -import dev.rollczi.litecommands.annotations.command.Command; -import dev.rollczi.litecommands.annotations.context.Context; -import dev.rollczi.litecommands.annotations.execute.Execute; -import dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -@LiteCommand -@Command(name = "playtime top") -@Permission("command.playtime.top") -public final class TimeTopCommand { - - private static final int TOP_QUERY_LIMIT = 100; - - private final PluginLogger logger; - private final MessageService messageService; - private final UserService userService; - private final GuiOpener guiOpener; - - @Inject - public TimeTopCommand( - @NotNull PluginLogger logger, - @NotNull MessageService messageService, - @NotNull UserService userService, - @NotNull GuiOpener guiOpener - ) { - this.logger = logger; - this.messageService = messageService; - this.userService = userService; - this.guiOpener = guiOpener; - } - - @Execute - void playtimeTop(@Context Player viewer) { - userService.findTopByPlayTime(TOP_QUERY_LIMIT) - .thenAccept(topUsers -> guiOpener.open(PlayTimeTopGui.class, viewer, topUsers)) - .exceptionally(e -> { - logger.error(e, "Failed to open PlaytimeTopGui for viewer=%s", viewer.getName()); - messageService.send(viewer, n -> n.actionExecutionError); - return null; - }); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java deleted file mode 100644 index c184b93..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.command; - -import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.user.top.TopUsersCache; -import dev.rollczi.litecommands.annotations.command.Command; -import dev.rollczi.litecommands.annotations.context.Context; -import dev.rollczi.litecommands.annotations.execute.Execute; -import dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -@LiteCommand -@Command(name = "playtime top invalidate") -@Permission("command.playtime.top.invalidate") -public final class TimeTopInvalidateCommand { - - private final MessageService messageService; - private final TopUsersCache topUsersCache; - - @Inject - public TimeTopInvalidateCommand( - @NotNull MessageService messageService, - @NotNull TopUsersCache topUsersCache - ) { - this.messageService = messageService; - this.topUsersCache = topUsersCache; - } - - @Execute - void invalidateCache(@Context CommandSender sender) { - topUsersCache.invalidateAll(); - messageService.send(sender, n -> n.playtimeMessages.topUsersCacheInvalidated()); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java index 0bffa2d..6d74248 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.feature.playtime.gui; +import com.github.imdmk.playtime.feature.playtime.PlayTimeUser; import com.github.imdmk.playtime.injector.annotations.Gui; -import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.adventure.AdventureFormatter; import com.github.imdmk.playtime.platform.adventure.AdventurePlaceholders; import com.github.imdmk.playtime.platform.gui.GuiType; @@ -9,8 +9,6 @@ import com.github.imdmk.playtime.platform.gui.factory.GuiFactory; import com.github.imdmk.playtime.platform.gui.item.ItemGui; import com.github.imdmk.playtime.platform.gui.item.ItemGuiTransformer; -import com.github.imdmk.playtime.platform.gui.item.ItemVariantPermissionResolver; -import com.github.imdmk.playtime.platform.gui.item.ItemVariantResolver; import com.github.imdmk.playtime.platform.gui.render.GuiRenderer; import com.github.imdmk.playtime.platform.gui.render.RenderContext; import com.github.imdmk.playtime.platform.gui.render.RenderOptions; @@ -18,15 +16,12 @@ import com.github.imdmk.playtime.platform.gui.view.AbstractGui; import com.github.imdmk.playtime.platform.gui.view.ParameterizedGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.time.Durations; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.shared.time.Durations; import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.builder.item.SkullBuilder; import dev.triumphteam.gui.guis.BaseGui; import dev.triumphteam.gui.guis.GuiItem; +import org.bukkit.OfflinePlayer; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; @@ -39,109 +34,82 @@ @Gui public final class PlayTimeTopGui extends AbstractGui - implements ParameterizedGui> { + implements ParameterizedGui> { - private static final String GUI_IDENTIFIER = "playtime-top"; + private static final String GUI_ID = "playtime-top"; - private static final GuiRenderer GUI_RENDERER = TriumphGuiRenderer.newRenderer(); + private static final GuiRenderer RENDERER = TriumphGuiRenderer.newRenderer(); private static final RenderOptions RENDER_OPTIONS = RenderOptions.defaultHide(); - private static final ItemVariantResolver ITEM_VARIANT_RESOLVER = new ItemVariantPermissionResolver(); private final Server server; - private final PlayTimeTopGuiConfig topGuiConfig; - private final MessageService messageService; - private final UserService userService; + private final PlayTimeTopGuiConfig guiConfig; @Inject - public PlayTimeTopGui( + PlayTimeTopGui( @NotNull Server server, - @NotNull NavigationBarConfig navigationBarConfig, - @NotNull PlayTimeTopGuiConfig topGuiConfig, - @NotNull TaskScheduler taskScheduler, - @NotNull MessageService messageService, - @NotNull UserService userService + @NotNull PlayTimeTopGuiConfig guiConfig, + @NotNull NavigationBarConfig config, + @NotNull TaskScheduler taskScheduler ) { - super(navigationBarConfig, taskScheduler, GUI_RENDERER, RENDER_OPTIONS); + super(config, taskScheduler, RENDERER, RENDER_OPTIONS); this.server = server; - this.topGuiConfig = topGuiConfig; - this.messageService = messageService; - this.userService = userService; + this.guiConfig = guiConfig; } @Override - public @NotNull BaseGui createGui(@NotNull Player viewer, @NotNull List users) { - return GuiFactory.build(topGuiConfig, BaseGui::disableAllInteractions); + public BaseGui createGui(@NotNull Player viewer, @NotNull List topUsers) { + return GuiFactory.build(guiConfig, BaseGui::disableAllInteractions); } @Override - public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull List users) { - if (topGuiConfig.fillBorder) { - final GuiItem borderItem = ItemGuiTransformer.toGuiItem(topGuiConfig.borderItem); + public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull List topUsers) { + if (guiConfig.fillBorder) { + final GuiItem borderItem = ItemGuiTransformer.toGuiItem(guiConfig.borderItem); gui.getFiller().fillBorder(borderItem); } - placeExit(gui, viewer, e -> gui.close(viewer)); + placeExit(gui, viewer, exit -> gui.close(viewer)); - if (topGuiConfig.type == GuiType.PAGINATED) { + if (guiConfig.type == GuiType.PAGINATED) { placeNext(gui, viewer); placePrevious(gui, viewer); } final RenderContext context = RenderContext.defaultContext(viewer); - final ItemGui item = resolveItemFor(viewer, context); + final ItemGui item = guiConfig.playerEntryItem; - for (int i = 0; i < users.size(); i++) { - final User user = users.get(i); + for (int i = 0; i < topUsers.size(); i++) { final int position = i + 1; - final AdventurePlaceholders placeholders = createPlaceholders(user, position); + final PlayTimeUser user = topUsers.get(i); + final OfflinePlayer player = server.getOfflinePlayer(user.getUuid()); - final Consumer onClick = (click) -> { - if (click.getClick() != topGuiConfig.resetClickType) { - return; - } - - gui.close(viewer); - - user.setPlaytime(PlayTime.ZERO); - userService.save(user, UserSaveReason.GUI_RESET_CLICK) - .thenAccept(result -> messageService.send(viewer, n -> n.playtimeMessages.playerPlaytimeReset())) - .exceptionally(e -> { - messageService.send(viewer, n -> n.actionExecutionError); - return null; - }); - }; + final AdventurePlaceholders placeholders = createPlaceholders(player, user, position); + final Consumer clickHandler = (event -> event.setCancelled(true)); final Consumer> editor = (builder) -> { if (builder instanceof SkullBuilder skullBuilder) { - skullBuilder.owner(server.getOfflinePlayer(user.getUuid())); + skullBuilder.owner(player); } builder.name(AdventureFormatter.format(item.name(), placeholders)); builder.lore(AdventureFormatter.format(item.lore(), placeholders)); }; - renderer.addItem(gui, item, context, renderOptions, onClick, editor); + renderer.addItem(gui, item, context, renderOptions, clickHandler, editor); } } - private ItemGui resolveItemFor(Player viewer, RenderContext context) { - final ItemGui adminItem = topGuiConfig.playerEntryAdminItem; - final ItemGui item = topGuiConfig.playerEntryItem; - return ITEM_VARIANT_RESOLVER.resolve(viewer, context, List.of(adminItem), item); - } - - private AdventurePlaceholders createPlaceholders(User topUser, int position) { + private AdventurePlaceholders createPlaceholders(OfflinePlayer offlinePlayer, PlayTimeUser user, int position) { return AdventurePlaceholders.builder() - .with("{PLAYER_NAME}", topUser.getName()) + .with("{PLAYER_NAME}", offlinePlayer.getName() == null ? "Unknown" : offlinePlayer.getName()) .with("{PLAYER_POSITION}", position) - .with("{PLAYER_PLAYTIME}", Durations.format(topUser.getPlaytime().toDuration())) - .with("{CLICK_RESET}", topGuiConfig.resetClickType.name()) + .with("{PLAYER_PLAYTIME}", Durations.format(user.getPlayTime().toDuration())) .build(); } @Override - public @NotNull String getId() { - return GUI_IDENTIFIER; + public String getId() { + return GUI_ID; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java index 920203b..aea6fe1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java @@ -8,9 +8,7 @@ import eu.okaeri.configs.annotation.Comment; import net.kyori.adventure.text.Component; import org.bukkit.Material; -import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemFlag; -import org.jetbrains.annotations.NotNull; import java.util.Collections; @@ -84,51 +82,13 @@ public final class PlayTimeTopGuiConfig .addFlags(ItemFlag.HIDE_ATTRIBUTES) .build(); - @Comment({ - "#", - "# Base item for a single player entry in the top playtime list (admin view).", - "# Used when the viewer has management permission and can reset playtime.", - "#", - "# Placeholders:", - "# {PLAYER_POSITION} - numeric position on the leaderboard (1, 2, 3, ...).", - "# {PLAYER_NAME} - player nickname.", - "# {PLAYER_PLAYTIME} - formatted playtime (e.g. 5h 32m).", - "# {CLICK_RESET} - name/description of the click used to reset (e.g. SHIFT + Right Click).", - "#", - "# requiredPermission in item defines to see the admin version of the item.", - "#" - }) - public ItemGui playerEntryAdminItem = ItemGui.builder() - .material(Material.PLAYER_HEAD) - .name(AdventureComponents.withoutItalics( - "#{PLAYER_POSITION} - {PLAYER_NAME}" - )) - .lore(AdventureComponents.withoutItalics( - " ", - "Playtime: {PLAYER_PLAYTIME}", - " ", - "Click {CLICK_RESET} to reset {PLAYER_NAME}'s playtime." - )) - .addFlags(ItemFlag.HIDE_ATTRIBUTES) - .requiredPermission("playtime.user.manage") - .build(); - - @Comment({ - "#", - "# Click type used to trigger playtime reset in the admin view.", - "# Must match how {CLICK_RESET} is described in messages/lore.", - "# Example: SHIFT_RIGHT, SHIFT_LEFT, RIGHT, LEFT, etc.", - "#" - }) - public ClickType resetClickType = ClickType.SHIFT_RIGHT; - @Override - public @NotNull Component title() { + public Component title() { return title; } @Override - public @NotNull GuiType type() { + public GuiType type() { return type; } @@ -137,4 +97,4 @@ public int rows() { return rows; } -} +} \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java deleted file mode 100644 index 3202894..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.listener; - -import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.annotations.Controller; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserService; -import com.github.imdmk.playtime.PlayTime; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerQuitEvent; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.UUID; - -@Controller -public final class PlayTimeSaveController implements Listener { - - private final UserService userService; - private final PlayTimeService playtimeService; - - @Inject - public PlayTimeSaveController( - @NotNull UserService userService, - @NotNull PlayTimeService playtimeService - ) { - this.userService = userService; - this.playtimeService = playtimeService; - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent event) { - final UUID uuid = event.getPlayer().getUniqueId(); - - final User user = userService.findCachedByUuid(uuid).orElseThrow(); - final PlayTime time = playtimeService.getTime(uuid); - - user.setPlaytime(time); - userService.save(user, UserSaveReason.PLAYER_LEAVE); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java index b4e1ff4..298681a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java @@ -4,7 +4,9 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; -public final class ENPlayTimeMessages extends OkaeriConfig implements PlayTimeMessages { +public final class ENPlayTimeMessages + extends OkaeriConfig + implements PlayTimeMessages { @Comment({ "#", @@ -14,7 +16,7 @@ public final class ENPlayTimeMessages extends OkaeriConfig implements PlayTimeMe "# {PLAYER_PLAYTIME} - formatted playtime of the player (e.g. 5h 32m).", "#" }) - Notice playerPlaytimeSelf = Notice.chat( + Notice playerPlayTimeSelf = Notice.chat( "You have spent {PLAYER_PLAYTIME} on this server." ); @@ -23,12 +25,11 @@ public final class ENPlayTimeMessages extends OkaeriConfig implements PlayTimeMe "# Sent to a command executor when they check another player's playtime.", "#", "# Placeholders:", - "# {PLAYER_NAME} - target player's nickname.", "# {PLAYER_PLAYTIME} - formatted playtime of the target player.", "#" }) - Notice playerPlaytimeTarget = Notice.chat( - "Player {PLAYER_NAME} has spent {PLAYER_PLAYTIME} on this server." + Notice playerPlayTimeTarget = Notice.chat( + "Target player has spent {PLAYER_PLAYTIME} on this server." ); @Comment({ @@ -36,100 +37,25 @@ public final class ENPlayTimeMessages extends OkaeriConfig implements PlayTimeMe "# Sent to a command executor after manually setting a player's playtime.", "#", "# Placeholders:", - "# {PLAYER_NAME} - target player's nickname.", "# {PLAYER_PLAYTIME} - new formatted playtime value.", "#" }) - Notice playerPlaytimeUpdated = Notice.chat( - "Updated playtime for player {PLAYER_NAME} to {PLAYER_PLAYTIME}." + Notice playerPlayTimeUpdated = Notice.chat( + "Updated playtime for player to {PLAYER_PLAYTIME}." ); - @Comment({ - "#", - "# Sent to a command executor after resetting a single player's playtime to zero.", - "#", - "# Placeholders:", - "# {PLAYER_NAME} - target player's nickname.", - "#" - }) - Notice playerPlaytimeReset = Notice.chat( - "Playtime for player {PLAYER_NAME} has been reset to ZERO." - ); - - @Comment({ - "#", - "# Sent when a global reset of all players' playtime is triggered.", - "#" - }) - Notice playerPlaytimeResetAllStarted = Notice.chat( - "Global playtime reset started... Please wait." - ); - - @Comment({ - "#", - "# Sent to the executor if the global playtime reset fails.", - "#" - }) - Notice playerPlaytimeResetAllFailed = Notice.chat( - "An error occurred while resetting playtime for all players. " - + "Check console for details." - ); - - @Comment({ - "#", - "# Sent when the global playtime reset finishes successfully.", - "#" - }) - Notice playerPlaytimeResetAllFinished = Notice.chat( - "Successfully reset playtime for all stored players." - ); - - @Comment({ - "#", - "# Sent after invalidating the Top users playtime cache.", - "#" - }) - Notice topUsersCacheInvalidated = Notice.chat( - "Successfully invalidated the PlayTime Top cache." - ); - - @Override - public Notice playerPlaytimeSelf() { - return playerPlaytimeSelf; - } - - @Override - public Notice playerPlaytimeTarget() { - return playerPlaytimeTarget; - } - - @Override - public Notice playerPlaytimeUpdated() { - return playerPlaytimeUpdated; - } - - @Override - public Notice playerPlaytimeReset() { - return playerPlaytimeReset; - } - - @Override - public Notice playerPlaytimeResetAllStarted() { - return playerPlaytimeResetAllStarted; - } - @Override - public Notice playerPlaytimeResetAllFailed() { - return playerPlaytimeResetAllFailed; + public Notice playerPlayTimeSelf() { + return playerPlayTimeSelf; } @Override - public Notice playerPlaytimeResetAllFinished() { - return playerPlaytimeResetAllFinished; + public Notice playerPlayTimeTarget() { + return playerPlayTimeTarget; } @Override - public Notice topUsersCacheInvalidated() { - return topUsersCacheInvalidated; + public Notice playerPlayTimeUpdated() { + return playerPlayTimeUpdated; } -} +} \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java index 070db18..e5d2b62 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java @@ -4,15 +4,9 @@ public interface PlayTimeMessages { - Notice playerPlaytimeSelf(); - Notice playerPlaytimeTarget(); + Notice playerPlayTimeSelf(); + Notice playerPlayTimeTarget(); - Notice playerPlaytimeUpdated(); - Notice playerPlaytimeReset(); + Notice playerPlayTimeUpdated(); - Notice playerPlaytimeResetAllStarted(); - Notice playerPlaytimeResetAllFailed(); - Notice playerPlaytimeResetAllFinished(); - - Notice topUsersCacheInvalidated(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java deleted file mode 100644 index 76d414d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.placeholder; - -import com.github.imdmk.playtime.PlayTimeService; -import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; -import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; -import com.github.imdmk.playtime.time.Durations; -import com.github.imdmk.playtime.PlayTime; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -@Placeholder -public final class PlayTimePlaceholder implements PluginPlaceholder { - - private final PlayTimeService playtimeService; - - @Inject - public PlayTimePlaceholder(@NotNull PlayTimeService playtimeService) { - this.playtimeService = playtimeService; - } - - @Override - public @NotNull String identifier() { - return "advancedplaytime"; - } - - @Override - public @NotNull String request(@NotNull Player player, @NotNull String params) { - final PlayTime time = playtimeService.getTime(player.getUniqueId()); - return Durations.format(time.toDuration()); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java index 8045c0d..0085e44 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java @@ -1,21 +1,23 @@ package com.github.imdmk.playtime.feature.reload; +import com.github.imdmk.playtime.config.ConfigAccessException; import com.github.imdmk.playtime.config.ConfigService; -import com.github.imdmk.playtime.message.MessageService; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import com.github.imdmk.playtime.shared.message.MessageService; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.permission.Permission; -import eu.okaeri.configs.exception.OkaeriException; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; +@LiteCommand @Command(name = "playtime reload") @Permission("command.playtime.reload") -public final class ReloadCommand { +final class ReloadCommand { private final PluginLogger logger; private final ConfigService configService; @@ -23,7 +25,7 @@ public final class ReloadCommand { private final MessageService messageService; @Inject - public ReloadCommand( + ReloadCommand( @NotNull PluginLogger logger, @NotNull ConfigService configService, @NotNull TaskScheduler taskScheduler, @@ -41,7 +43,7 @@ void reload(@Context CommandSender sender) { try { configService.loadAll(); messageService.send(sender, n -> n.reloadMessages.configReloadedSuccess()); - } catch (OkaeriException e) { + } catch (ConfigAccessException e) { logger.error(e, "Failed to reload plugin configuration files"); messageService.send(sender, n -> n.reloadMessages.configReloadFailed()); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java index 8031b99..14c9b97 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -30,16 +30,12 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.Resources; import org.panda_lang.utilities.inject.annotations.Inject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.util.List; public final class ComponentProcessors { - private static final Logger log = LoggerFactory.getLogger(ComponentProcessors.class); - private ComponentProcessors() { throw new UnsupportedOperationException("This is utility class and cannot be instantiated."); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java index 27bb088..51b54e7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -53,7 +53,6 @@ public E publish(@NotNull E event) { return event; } - private record SubscriberMethod(@NotNull Object instance, @NotNull Method method) { - } + private record SubscriberMethod(@NotNull Object instance, @NotNull Method method) { } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java index 775e007..87ca029 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java @@ -9,13 +9,13 @@ import org.panda_lang.utilities.inject.annotations.Inject; @Service(priority = ComponentPriority.LOW) -public final class BukkitEventCaller implements EventCaller { +final class BukkitEventCaller implements EventCaller { private final Server server; private final TaskScheduler scheduler; @Inject - public BukkitEventCaller(@NotNull Server server, @NotNull TaskScheduler scheduler) { + BukkitEventCaller(@NotNull Server server, @NotNull TaskScheduler scheduler) { this.server = server; this.scheduler = scheduler; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java index d27510a..8e9e94a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java @@ -1,8 +1,7 @@ package com.github.imdmk.playtime.platform.gui; -import org.jetbrains.annotations.NotNull; public interface IdentifiableGui { - @NotNull String getId(); + String getId(); } \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java index fec1fbe..dddc8c7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java @@ -14,7 +14,7 @@ @ConfigFile public final class GuiConfig extends ConfigSection { - @Comment({"#", "# Playtime top GUI", "#"}) + @Comment({"#", "# PlayTime top GUI", "#"}) public PlayTimeTopGuiConfig playTimeTopGui = new PlayTimeTopGuiConfig(); @Comment({"#", "# Navigation Bar", "#"}) @@ -32,6 +32,6 @@ public final class GuiConfig extends ConfigSection { @Override public @NotNull String fileName() { - return "guiConfig.yml"; + return "gui.yml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java new file mode 100644 index 0000000..10ed303 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java @@ -0,0 +1,36 @@ +package com.github.imdmk.playtime.platform.identity; + +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public final class IdentityCache { + + private final Map nameToUuid = new ConcurrentHashMap<>(); + + public void update(@NotNull Player player) { + nameToUuid.put(player.getName(), player.getUniqueId()); + } + + public void remove(@NotNull Player player) { + nameToUuid.remove(player.getName()); + } + + public Optional getUuidByName(@NotNull String name) { + return Optional.ofNullable(nameToUuid.get(name)); + } + + @Subscribe(event = PlayTimeShutdownEvent.class) + public void shutdown() { + nameToUuid.clear(); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java new file mode 100644 index 0000000..43c18cf --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java @@ -0,0 +1,31 @@ +package com.github.imdmk.playtime.platform.identity; + +import com.github.imdmk.playtime.injector.annotations.Controller; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@Controller +final class IdentityController implements Listener { + + private final IdentityCache cache; + + @Inject + IdentityController(@NotNull IdentityCache cache) { + this.cache = cache; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + cache.update(event.getPlayer()); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + cache.remove(event.getPlayer()); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java index 4d2563f..65f1679 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java @@ -21,7 +21,7 @@ public final class LiteCommandsConfigurer { private LiteCommands liteCommands; @Inject - public LiteCommandsConfigurer(@NotNull Plugin plugin, @NotNull Server server) { + LiteCommandsConfigurer(@NotNull Plugin plugin, @NotNull Server server) { this.builder = LiteBukkitFactory.builder(FALLBACK_PREFIX, plugin, server); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java new file mode 100644 index 0000000..1664b70 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java @@ -0,0 +1,34 @@ +package com.github.imdmk.playtime.platform.litecommands.argument; + +import com.github.imdmk.playtime.injector.annotations.lite.LiteArgument; +import com.github.imdmk.playtime.platform.identity.IdentityCache; +import com.github.imdmk.playtime.shared.message.MessageConfig; +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.argument.parser.ParseResult; +import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; +import dev.rollczi.litecommands.invocation.Invocation; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.UUID; + +@LiteArgument(type = UUID.class) +class UUIDArgumentResolver extends ArgumentResolver { + + private final IdentityCache cache; + private final MessageConfig messageConfig; + + @Inject + UUIDArgumentResolver(IdentityCache cache, @NotNull MessageConfig messageConfig) { + this.cache = cache; + this.messageConfig = messageConfig; + } + + @Override + protected ParseResult parse(Invocation invocation, Argument context, String argument) { + return cache.getUuidByName(argument) + .map(ParseResult::success) + .orElse(ParseResult.failure(messageConfig.playerNotFound)); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/InvalidUsageHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/InvalidUsageHandlerImpl.java index ab840a1..3579f1c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/InvalidUsageHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/InvalidUsageHandlerImpl.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.platform.litecommands.handler; import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; -import com.github.imdmk.playtime.message.MessageService; +import com.github.imdmk.playtime.shared.message.MessageService; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invalidusage.InvalidUsageHandler; @@ -11,11 +11,11 @@ import org.jetbrains.annotations.NotNull; @LiteHandler(value = CommandSender.class) -public final class InvalidUsageHandlerImpl implements InvalidUsageHandler { +final class InvalidUsageHandlerImpl implements InvalidUsageHandler { private final MessageService messageService; - public InvalidUsageHandlerImpl(@NotNull MessageService messageService) { + InvalidUsageHandlerImpl(@NotNull MessageService messageService) { this.messageService = messageService; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/MissingPermissionsHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/MissingPermissionsHandlerImpl.java index ae6e77b..51210a0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/MissingPermissionsHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/MissingPermissionsHandlerImpl.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.platform.litecommands.handler; import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; -import com.github.imdmk.playtime.message.MessageService; +import com.github.imdmk.playtime.shared.message.MessageService; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.permission.MissingPermissions; @@ -10,11 +10,11 @@ import org.jetbrains.annotations.NotNull; @LiteHandler(value = CommandSender.class) -public final class MissingPermissionsHandlerImpl implements MissingPermissionsHandler { +final class MissingPermissionsHandlerImpl implements MissingPermissionsHandler { private final MessageService messageService; - public MissingPermissionsHandlerImpl(@NotNull MessageService messageService) { + MissingPermissionsHandlerImpl(@NotNull MessageService messageService) { this.messageService = messageService; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/NoticeResultHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/NoticeResultHandlerImpl.java index e75ba6b..83d19be 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/NoticeResultHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/handler/NoticeResultHandlerImpl.java @@ -2,7 +2,7 @@ import com.eternalcode.multification.notice.Notice; import com.github.imdmk.playtime.injector.annotations.lite.LiteHandler; -import com.github.imdmk.playtime.message.MessageService; +import com.github.imdmk.playtime.shared.message.MessageService; import dev.rollczi.litecommands.handler.result.ResultHandler; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invocation.Invocation; @@ -10,11 +10,11 @@ import org.jetbrains.annotations.NotNull; @LiteHandler(value = Notice.class) -public final class NoticeResultHandlerImpl implements ResultHandler { +final class NoticeResultHandlerImpl implements ResultHandler { private final MessageService messageService; - public NoticeResultHandlerImpl(@NotNull MessageService messageService) { + NoticeResultHandlerImpl(@NotNull MessageService messageService) { this.messageService = messageService; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java index 5d0efc0..7706566 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java @@ -9,14 +9,13 @@ import org.panda_lang.utilities.inject.annotations.Inject; @Service -public class BMetricsService { +class BMetricsService { private static final int METRICS_ID = 19362; - private final Metrics metrics; @Inject - public BMetricsService(@NotNull Plugin plugin) { + BMetricsService(@NotNull Plugin plugin) { this.metrics = new Metrics(plugin, METRICS_ID); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java index a003024..b3343ac 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java @@ -2,7 +2,7 @@ import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.time.Durations; +import com.github.imdmk.playtime.shared.time.Durations; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; @@ -12,29 +12,29 @@ import java.time.Duration; @Service(priority = ComponentPriority.LOWEST) -public final class BukkitTaskScheduler implements TaskScheduler { +final class BukkitTaskScheduler implements TaskScheduler { private final Plugin plugin; private final BukkitScheduler scheduler; @Inject - public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler scheduler) { + BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler scheduler) { this.plugin = plugin; this.scheduler = scheduler; } @Override - public @NotNull BukkitTask runSync(@NotNull Runnable runnable) { + public BukkitTask runSync(@NotNull Runnable runnable) { return scheduler.runTask(plugin, runnable); } @Override - public @NotNull BukkitTask runAsync(@NotNull Runnable runnable) { + public BukkitTask runAsync(@NotNull Runnable runnable) { return scheduler.runTaskAsynchronously(plugin, runnable); } @Override - public @NotNull BukkitTask runLaterAsync( + public BukkitTask runLaterAsync( @NotNull Runnable runnable, @NotNull Duration delay ) { @@ -42,7 +42,7 @@ public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler sche } @Override - public @NotNull BukkitTask runLaterSync( + public BukkitTask runLaterSync( @NotNull Runnable runnable, @NotNull Duration delay ) { @@ -50,7 +50,7 @@ public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler sche } @Override - public @NotNull BukkitTask runTimerSync( + public BukkitTask runTimerSync( @NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period @@ -59,7 +59,7 @@ public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler sche } @Override - public @NotNull BukkitTask runTimerAsync( + public BukkitTask runTimerAsync( @NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java index 42ca5f1..5611189 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java @@ -8,18 +8,14 @@ public interface TaskScheduler { BukkitTask runSync(@NotNull Runnable runnable); - BukkitTask runAsync(@NotNull Runnable runnable); BukkitTask runLaterAsync(@NotNull Runnable runnable, @NotNull Duration delay); - BukkitTask runLaterSync(@NotNull Runnable runnable, @NotNull Duration delay); BukkitTask runTimerSync(@NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period); - BukkitTask runTimerAsync(@NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period); void cancelTask(int taskId); - void cancelAllTasks(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/message/MessageConfig.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/shared/message/MessageConfig.java index 1b8b7a3..e655569 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/message/MessageConfig.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.message; +package com.github.imdmk.playtime.shared.message; import com.eternalcode.multification.notice.Notice; import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults; @@ -84,7 +84,7 @@ public final class MessageConfig extends ConfigSection { + "If the problem persists, contact an administrator." ); - @Comment({" ", "# Playtime messages", " "}) + @Comment({" ", "# PlayTime messages", " "}) public ENPlayTimeMessages playtimeMessages = new ENPlayTimeMessages(); @Comment({" ", "# Reload messages", " "}) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/message/MessageService.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/shared/message/MessageService.java index 21a860d..55f61d2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/message/MessageService.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.message; +package com.github.imdmk.playtime.shared.message; import com.eternalcode.multification.adventure.AudienceConverter; import com.eternalcode.multification.bukkit.BukkitMultification; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java index c649bf0..f444656 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.time; +package com.github.imdmk.playtime.shared.time; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java similarity index 92% rename from playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java index c361463..6a5db0a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.time; +package com.github.imdmk.playtime.shared.time; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java similarity index 97% rename from playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java index cb3bea7..561b0ff 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.time; +package com.github.imdmk.playtime.shared.time; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java similarity index 96% rename from playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java index 4d42879..d2eb1bb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.time; +package com.github.imdmk.playtime.shared.time; import org.jetbrains.annotations.NotNull; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java deleted file mode 100644 index d2407d5..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.imdmk.playtime.user; - -import com.github.imdmk.playtime.injector.annotations.lite.LiteArgument; -import com.github.imdmk.playtime.message.MessageConfig; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import dev.rollczi.litecommands.argument.Argument; -import dev.rollczi.litecommands.argument.parser.ParseResult; -import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; -import dev.rollczi.litecommands.invocation.Invocation; -import dev.rollczi.litecommands.suggestion.SuggestionContext; -import dev.rollczi.litecommands.suggestion.SuggestionResult; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.time.Duration; -import java.util.concurrent.TimeUnit; - -@LiteArgument(type = User.class) -final class UserArgument extends ArgumentResolver { - - private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(2); - - private final PluginLogger logger; - private final Server server; - private final MessageConfig messageConfig; - private final UserService userService; - - @Inject - UserArgument( - @NotNull PluginLogger logger, - @NotNull Server server, - @NotNull MessageConfig messageConfig, - @NotNull UserService userService - ) { - this.logger = logger; - this.server = server; - this.messageConfig = messageConfig; - this.userService = userService; - } - - @Override - protected ParseResult parse( - Invocation invocation, - Argument context, - String argument - ) { - // Main thread -> cache-only (never block the tick) - if (server.isPrimaryThread()) { - logger.warn("UserArgument lookup for '%s' on main thread - using cache only.", argument); - return userService.findCachedByName(argument) - .map(ParseResult::success) - .orElse(ParseResult.failure(messageConfig.playerNotFound)); - } - - // Off-thread -> full lookup (cache → DB) - return userService.findByName(argument) - .orTimeout(LOOKUP_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - .join() - .map(ParseResult::success) - .orElse(ParseResult.failure(messageConfig.playerNotFound)); - } - - @Override - public SuggestionResult suggest( - Invocation invocation, - Argument argument, - SuggestionContext context - ) { - return userService.getCachedUsers() - .stream() - .map(User::getName) - .collect(SuggestionResult.collector()); - } -} - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java deleted file mode 100644 index 8088f03..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.imdmk.playtime.user; - -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public interface UserFactory { - - User createFrom(@NotNull Player player); - User createFrom(@NotNull OfflinePlayer offlinePlayer); - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java deleted file mode 100644 index 1d70577..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.github.imdmk.playtime.user; - -import com.github.imdmk.playtime.UserDeleteEvent; -import com.github.imdmk.playtime.UserSaveEvent; -import com.github.imdmk.playtime.injector.ComponentPriority; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.platform.event.BukkitEventCaller; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.user.cache.UserCache; -import com.github.imdmk.playtime.user.repository.UserRepository; -import com.github.imdmk.playtime.user.top.TopUsersCache; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.time.Duration; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -@Service(priority = ComponentPriority.HIGH) -final class UserServiceImpl implements UserService { - - private static final Duration TIMEOUT = Duration.ofSeconds(2L); - - private final PluginLogger logger; - private final UserCache cache; - private final TopUsersCache topUsersCache; - private final UserRepository repository; - private final BukkitEventCaller eventCaller; - - @Inject - UserServiceImpl( - @NotNull PluginLogger logger, - @NotNull UserCache cache, - @NotNull TopUsersCache topUsersCache, - @NotNull UserRepository repository, - @NotNull BukkitEventCaller eventCaller - ) { - this.logger = logger; - this.cache = cache; - this.topUsersCache = topUsersCache; - this.repository = repository; - this.eventCaller = eventCaller; - } - - @Override - public Optional findCachedByUuid(@NotNull UUID uuid) { - return cache.getUserByUuid(uuid); - } - - @Override - public Optional findCachedByName(@NotNull String name) { - return cache.getUserByName(name); - } - - @Override - @Unmodifiable - public Collection getCachedUsers() { - return cache.getCache(); // returns unmodifiable - } - - @Override - public CompletableFuture> findByUuid(@NotNull UUID uuid) { - final Optional cached = cache.getUserByUuid(uuid); - if (cached.isPresent()) { - return CompletableFuture.completedFuture(cached); - } - - return repository.findByUuid(uuid) - .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - .thenApply(opt -> { - opt.ifPresent(cache::cacheUser); - return opt; - }) - .exceptionally(e -> { - logger.error(e, "An error occurred while trying to find user with id %s", uuid); - throw new RuntimeException(e); - }); - } - - @Override - public CompletableFuture> findByName(@NotNull String name) { - final Optional cached = cache.getUserByName(name); - if (cached.isPresent()) { - return CompletableFuture.completedFuture(cached); - } - - return repository.findByName(name) - .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - .thenApply(opt -> { - opt.ifPresent(cache::cacheUser); - return opt; - }) - .exceptionally(e -> { - logger.error(e, "An error occurred while trying to find user with name %s", name); - throw new RuntimeException(e); - }); - } - - @Override - public CompletableFuture> findTopByPlayTime(int limit) { - return topUsersCache.getTopByPlayTime(limit); - } - - @Override - public CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason) { - return repository.save(user) - .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - .thenApply(saved -> { - eventCaller.callEvent(new UserSaveEvent(user, reason)); - cache.cacheUser(saved); - return saved; - }) - .exceptionally(e -> { - logger.error(e, "Failed to save user %s", user.getUuid()); - throw new RuntimeException(e); - }); - } - - @Override - public CompletableFuture deleteByUuid(@NotNull UUID uuid) { - return repository.deleteByUuid(uuid) - .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - .thenApply(result -> { - eventCaller.callEvent(new UserDeleteEvent(result)); - if (result.isSuccess()) { - cache.invalidateByUuid(uuid); - } - return result; - }) - .exceptionally(e -> { - logger.error(e, "An error occurred while trying to delete user by uuid %s", uuid); - throw new RuntimeException(e); - }); - } - - @Override - public CompletableFuture deleteByName(@NotNull String name) { - return repository.deleteByName(name) - .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) - .thenApply(deleteResult -> { - eventCaller.callEvent(new UserDeleteEvent(user)); - cache.invalidateByName(name); - return deleteResult; - }) - .exceptionally(e -> { - logger.error(e, "An error occurred while trying to delete user by name %s", name); - throw new RuntimeException(e); - }); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java deleted file mode 100644 index 35d61c7..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.github.imdmk.playtime.user.cache; - -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.RemovalCause; -import com.github.imdmk.playtime.injector.ComponentPriority; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.subscriber.Subscribe; -import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; -import com.github.imdmk.playtime.user.User; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Consumer; - -@Service(priority = ComponentPriority.LOWEST) -public final class CaffeineUserCache implements UserCache { - - private static final Duration EXPIRE_AFTER_ACCESS = Duration.ofHours(2); - private static final Duration EXPIRE_AFTER_WRITE = Duration.ofHours(12); - - private final Cache cacheByUuid; - private final Cache cacheByName; - - @Inject - public CaffeineUserCache() { - this.cacheByName = Caffeine.newBuilder() - .expireAfterWrite(EXPIRE_AFTER_WRITE) - .expireAfterAccess(EXPIRE_AFTER_ACCESS) - .build(); - - this.cacheByUuid = Caffeine.newBuilder() - .expireAfterWrite(EXPIRE_AFTER_WRITE) - .expireAfterAccess(EXPIRE_AFTER_ACCESS) - .removalListener((UUID key, User user, RemovalCause cause) -> { - if (key != null && user != null) { - this.cacheByName.invalidate(user.getName()); - } - }) - .build(); - } - - @Override - public void cacheUser(@NotNull User user) { - final UUID uuid = user.getUuid(); - final String name = user.getName(); - - final User previous = cacheByUuid.getIfPresent(uuid); - if (previous != null) { - final String oldName = previous.getName(); - if (!oldName.equals(name)) { - cacheByName.invalidate(oldName); - } - } - - cacheByUuid.put(uuid, user); - cacheByName.put(name, uuid); - } - - @Override - public void invalidateUser(@NotNull User user) { - cacheByUuid.invalidate(user.getUuid()); - cacheByName.invalidate(user.getName()); - } - - @Override - public void invalidateByUuid(@NotNull UUID uuid) { - final User cached = cacheByUuid.getIfPresent(uuid); - cacheByUuid.invalidate(uuid); - if (cached != null) { - cacheByName.invalidate(cached.getName()); - } - } - - @Override - public void invalidateByName(@NotNull String name) { - final UUID uuid = cacheByName.getIfPresent(name); - if (uuid != null) { - invalidateByUuid(uuid); - } else { - cacheByName.invalidate(name); - } - } - - @Override - @Subscribe(event = PlayTimeShutdownEvent.class) - public void invalidateAll() { - cacheByUuid.invalidateAll(); - cacheByName.invalidateAll(); - } - - @Override - public Optional getUserByUuid(@NotNull UUID uuid) { - return Optional.ofNullable(cacheByUuid.getIfPresent(uuid)); - } - - @Override - public Optional getUserByName(@NotNull String name) { - final UUID uuid = cacheByName.getIfPresent(name); - return uuid == null ? Optional.empty() : Optional.ofNullable(cacheByUuid.getIfPresent(uuid)); - } - - @Override - public void updateUserNameMapping(@NotNull User user, @NotNull String oldName) { - final String newName = user.getName(); - if (!oldName.equals(newName)) { - cacheByName.invalidate(oldName); - cacheByName.put(newName, user.getUuid()); - } - - cacheByUuid.put(user.getUuid(), user); - } - - @Override - public void forEachUser(@NotNull Consumer action) { - // Snapshot to avoid iterating over a live view while mutating the cache - for (final User user : new ArrayList<>(cacheByUuid.asMap().values())) { - action.accept(user); - } - } - - @Override - @Unmodifiable - public Collection getCache() { - return List.copyOf(cacheByUuid.asMap().values()); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java deleted file mode 100644 index d0cf220..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.imdmk.playtime.user.cache; - -import com.github.imdmk.playtime.user.User; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Consumer; - -public interface UserCache { - - void cacheUser(@NotNull User user); - - void invalidateUser(@NotNull User user); - void invalidateByUuid(@NotNull UUID uuid); - void invalidateByName(@NotNull String name); - void invalidateAll(); - - Optional getUserByUuid(@NotNull UUID uuid); - Optional getUserByName(@NotNull String name); - - void updateUserNameMapping(@NotNull User user, @NotNull String oldName); - void forEachUser(@NotNull Consumer action); - - Collection getCache(); - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java deleted file mode 100644 index d5de862..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.github.imdmk.playtime.user.controller; - -import com.github.imdmk.playtime.injector.annotations.Controller; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserFactory; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserService; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.server.ServerLoadEvent; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.Optional; -import java.util.UUID; - -@Controller -public final class UserJoinController implements Listener { - - private final Server server; - private final PluginLogger logger; - private final UserService userService; - private final UserFactory userFactory; - private final TaskScheduler taskScheduler; - - @Inject - public UserJoinController( - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull UserService userService, - @NotNull UserFactory userFactory, - @NotNull TaskScheduler taskScheduler - ) { - this.server = server; - this.logger = logger; - this.userService = userService; - this.userFactory = userFactory; - this.taskScheduler = taskScheduler; - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerJoin(PlayerJoinEvent event) { - handlePlayerJoin(event.getPlayer()); - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onServerReload(ServerLoadEvent event) { - server.getOnlinePlayers().forEach(this::handlePlayerJoin); - } - - private void handlePlayerJoin(Player player) { - final UUID uuid = player.getUniqueId(); - - userService.findByUuid(uuid) - .whenComplete((optional, e) -> { - if (e != null) { - logger.error(e, "Failed to load user on join uuid=%s", uuid); - return; - } - - taskScheduler.runSync(() -> handleLoadedUser(player, optional)); - }); - } - - private void handleLoadedUser(Player player, Optional optionalUser) { - optionalUser.ifPresentOrElse( - user -> handleExistingUser(player, user), - () -> handleNewUser(player) - ); - } - - private void handleNewUser(Player player) { - final User user = userFactory.createFrom(player); - saveUser(user, "on join (new)"); - } - - private void handleExistingUser(Player player, User user) { - final String name = player.getName(); - if (!updateNameIfChanged(user, name)) { - return; - } - - saveUser(user, "on join (update name)"); - } - - private void saveUser(User user, String context) { - userService.save(user, SAVE_REASON) - .whenComplete((r, e) -> { - if (e != null) { - logger.error(e, "Failed to save user %s uuid=%s", context, user.getUuid()); - } - }); - } - - private boolean updateNameIfChanged(User user, String name) { - final String oldName = user.getName(); - if (oldName.equals(name)) { - return false; - } - - user.setName(name); - return true; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java deleted file mode 100644 index 5f5b410..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.imdmk.playtime.user.controller; - -import com.github.imdmk.playtime.injector.annotations.Controller; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserService; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerQuitEvent; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.UUID; - -@Controller -public final class UserQuitController implements Listener { - - private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_LEAVE; - - private final PluginLogger logger; - private final UserService userService; - - @Inject - public UserQuitController(@NotNull PluginLogger logger, @NotNull UserService userService) { - this.logger = logger; - this.userService = userService; - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerQuit(PlayerQuitEvent event) { - final Player player = event.getPlayer(); - - final UUID uuid = player.getUniqueId(); - final String name = player.getName(); - - userService.findCachedByUuid(uuid).ifPresent(user -> userService.save(user, SAVE_REASON) - .whenComplete((u, e) -> { - if (e != null) { - logger.error(e, "Failed to save user on quit %s (%s)", name, uuid); - } - }) - ); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java deleted file mode 100644 index 2e93729..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.github.imdmk.playtime.user.repository; - -import com.j256.ormlite.field.DatabaseField; -import com.j256.ormlite.table.DatabaseTable; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.UUID; - -@DatabaseTable(tableName = UserEntityMeta.TABLE) -public final class UserEntity { - - @DatabaseField(id = true, canBeNull = false, columnName = UserEntityMeta.Col.UUID) - private UUID uuid; - - @DatabaseField(index = true, canBeNull = false, columnName = UserEntityMeta.Col.NAME) - private String name; - - @DatabaseField(canBeNull = false, columnName = UserEntityMeta.Col.PLAYTIME_MILLIS) - private long playtimeMillis; - - public UserEntity() {} - - public UserEntity( - @NotNull UUID uuid, - @NotNull String name, - long playtimeMillis - ) { - this.uuid = uuid; - this.name = name; - this.playtimeMillis = playtimeMillis; - } - - public UUID getUuid() { - return this.uuid; - } - - public void setUuid(@NotNull UUID uuid) { - this.uuid = uuid; - } - - public String getName() { - return this.name; - } - - public void setName(@NotNull String name) { - this.name = name; - } - - public long getPlaytimeMillis() { - return playtimeMillis; - } - - public void setPlaytimeMillis(long playtimeMillis) { - this.playtimeMillis = playtimeMillis; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof UserEntity other)) return false; - return this.uuid.equals(other.uuid); - } - - @Override - public int hashCode() { - return Objects.hash(this.uuid); - } - - @Override - public String toString() { - return "UserEntity{" + - "uuid=" + this.uuid + - ", name='" + this.name + '\'' + - ", spentMillis=" + this.playtimeMillis + - '}'; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java deleted file mode 100644 index d290e8f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.imdmk.playtime.user.repository; - -import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.PlayTime; -import org.jetbrains.annotations.NotNull; - -final class UserEntityMapper implements EntityMapper { - - @Override - public UserEntity toEntity(@NotNull User user) { - return new UserEntity( - user.getUuid(), - user.getName(), - user.getPlaytime().millis() - ); - } - - @Override - public User toDomain(@NotNull UserEntity entity) { - return new User( - entity.getUuid(), - entity.getName(), - PlayTime.ofMillis(entity.getPlaytimeMillis()) - ); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java deleted file mode 100644 index fca03c0..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.imdmk.playtime.user.repository; - -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserDeleteResult; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -public interface UserRepository { - - CompletableFuture> findByUuid(@NotNull UUID uuid); - CompletableFuture> findByName(@NotNull String name); - CompletableFuture> findAll(); - - CompletableFuture> findTopByPlayTime(long limit); - - CompletableFuture deleteByUuid(@NotNull UUID uuid); - CompletableFuture deleteByName(@NotNull String name); - - CompletableFuture save(@NotNull User user); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java deleted file mode 100644 index 6db799d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.github.imdmk.playtime.user.repository; - -import com.github.imdmk.playtime.database.DatabaseBootstrap; -import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; -import com.github.imdmk.playtime.injector.annotations.Repository; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserDeleteResult; -import com.github.imdmk.playtime.user.UserDeleteStatus; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.sql.SQLException; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -@Repository -public final class UserRepositoryOrmLite - extends OrmLiteRepository - implements UserRepository { - - private final UserEntityMapper mapper; - - @Inject - public UserRepositoryOrmLite( - @NotNull PluginLogger logger, - @NotNull TaskScheduler taskScheduler, - @NotNull DatabaseBootstrap databaseBootstrap - ) { - super(logger, taskScheduler, databaseBootstrap); - this.mapper = new UserEntityMapper(); - } - - @Override - protected Class entityClass() { - return UserEntity.class; - } - - @Override - protected List> entitySubClasses() { - return List.of(); - } - - @Override - public CompletableFuture> findByUuid(@NotNull UUID uuid) { - return execute(() -> { - try { - return Optional.ofNullable(dao.queryForId(uuid)) - .map(mapper::toDomain); - } catch (SQLException e) { - logger.error(e, "Failed to find user by uuid: %s", uuid); - throw new RuntimeException(e); - } - }); - } - - @Override - public CompletableFuture> findByName(@NotNull String name) { - return execute(() -> { - try { - final UserEntity entity = dao.queryBuilder() - .where().eq(UserEntityMeta.Col.NAME, name) - .queryForFirst(); - return Optional.ofNullable(entity).map(mapper::toDomain); - } catch (SQLException e) { - logger.error(e, "Failed to find user by name: %s", name); - throw new RuntimeException(e); - } - }); - } - - @Override - public CompletableFuture> findAll() { - return execute(() -> { - try { - return mapper.toDomainList(dao.queryForAll()); - } catch (SQLException e) { - logger.error(e, "Failed to find all users"); - throw new RuntimeException(e); - } - }); - } - - @Override - public CompletableFuture> findTopByPlayTime(long limit) { - if (limit <= 0) { - return CompletableFuture.completedFuture(List.of()); - } - - return execute(() -> { - try { - return mapper.toDomainList( - dao.queryBuilder() - .orderBy(UserEntityMeta.Col.PLAYTIME_MILLIS, false) // DESC - .orderBy(UserEntityMeta.Col.UUID, true) // deterministic tiebreaker - .limit(limit) - .query() - ); - } catch (SQLException e) { - logger.error(e, "Failed to find top users (limit %s) by spent time", limit); - throw new RuntimeException(e); - } - }); - } - - @Override - public CompletableFuture save(@NotNull User user) { - return execute(() -> { - try { - dao.createOrUpdate(mapper.toEntity(user)); - return user; - } catch (SQLException e) { - logger.error(e, "Failed to save user: %s", user.getUuid()); - throw new RuntimeException(e); - } - }); - } - - @Override - public CompletableFuture deleteByUuid(@NotNull UUID uuid) { - return execute(() -> { - try { - final UserEntity userEntity = dao.queryForId(uuid); - if (userEntity == null) { - return new UserDeleteResult(null, UserDeleteStatus.NOT_FOUND); - } - - final User user = mapper.toDomain(userEntity); - final int rows = dao.deleteById(uuid); - return rows > 0 - ? new UserDeleteResult(user, UserDeleteStatus.DELETED) - : new UserDeleteResult(user, UserDeleteStatus.FAILED); - } catch (SQLException e) { - logger.error(e, "Failed to delete user by uuid: %s", uuid); - throw new RuntimeException(e); - } - }); - } - - @Override - public CompletableFuture deleteByName(@NotNull String name) { - return execute(() -> { - try { - final UserEntity userEntity = dao.queryBuilder() - .where().eq(UserEntityMeta.Col.NAME, name) - .queryForFirst(); - if (userEntity == null) { - return new UserDeleteResult(null, UserDeleteStatus.NOT_FOUND); - } - - final User user = mapper.toDomain(userEntity); - final int rows = dao.delete(userEntity); - return rows > 0 - ? new UserDeleteResult(user, UserDeleteStatus.DELETED) - : new UserDeleteResult(user, UserDeleteStatus.FAILED); - } catch (SQLException e) { - logger.error(e, "Failed to delete user by name: %s", name); - throw new RuntimeException(e); - } - }); - } - - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java deleted file mode 100644 index 5b4b2a6..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.imdmk.playtime.user.top; - -import com.github.imdmk.playtime.user.User; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; - -import java.time.Duration; -import java.time.Instant; -import java.util.List; - -record CachedLeaderboard( - @NotNull List users, - int limit, - @NotNull Instant loadedAt -) { - - CachedLeaderboard { - users = List.copyOf(users); - } - - boolean isUsable(int requestedLimit, @NotNull Duration expireAfter, @NotNull Instant now) { - if (this.limit < requestedLimit) { - return false; - } - - if (expireAfter.isZero() || expireAfter.isNegative()) { - return true; - } - - final Instant expiresAt = this.loadedAt.plus(expireAfter); - return expiresAt.isAfter(now); - } - - @NotNull - @Override - @Unmodifiable - public List users() { - return users; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java deleted file mode 100644 index 7ce728e..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.imdmk.playtime.user.top; - -import com.github.imdmk.playtime.injector.ComponentPriority; -import com.github.imdmk.playtime.injector.annotations.Service; -import com.github.imdmk.playtime.injector.subscriber.Subscribe; -import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.repository.UserRepository; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -@Service(priority = ComponentPriority.NORMAL) -public final class MemoryTopUsersCache implements TopUsersCache { - - private final PluginLogger logger; - private final TopUsersCacheConfig config; - private final UserRepository userRepository; - - private final AtomicReference cachedLeaderboard = new AtomicReference<>(); - - public MemoryTopUsersCache( - @NotNull PluginLogger logger, - @NotNull TopUsersCacheConfig config, - @NotNull UserRepository userRepository - ) { - this.logger = logger; - this.config = config; - this.userRepository = userRepository; - } - - @Override - public CompletableFuture> getTopByPlayTime() { - return getTopByPlayTime(config.topUsersQueryLimit); - } - - @Override - public @NotNull CompletableFuture> getTopByPlayTime(int limit) { - if (limit <= 0) { - return CompletableFuture.completedFuture(List.of()); - } - - final CachedLeaderboard cached = cachedLeaderboard.get(); - final Duration expireAfter = config.topUsersCacheExpireAfter; - final Instant now = Instant.now(); - - if (cached != null && cached.isUsable(limit, expireAfter, now)) { - return CompletableFuture.completedFuture(slice(cached.users(), limit)); - } - - return loadTop(limit) - .thenApply(users -> { - final CachedLeaderboard updated = new CachedLeaderboard(users, limit, Instant.now()); - cachedLeaderboard.set(updated); - return slice(updated.users(), limit); - }) - .exceptionally(e -> { - logger.error(e, "Failed to load users top leaderboard (limit=%d)", limit); - return List.of(); - }); - } - - - @Override - @Subscribe(event = PlayTimeShutdownEvent.class) - public void invalidateAll() { - cachedLeaderboard.set(null); - } - - private CompletableFuture> loadTop(int limit) { - return userRepository.findTopByPlayTime(limit) - .orTimeout(config.topUsersQueryTimeout.toMillis(), TimeUnit.MILLISECONDS); - } - - private static List slice(List users, int limit) { - return users.size() <= limit ? users : users.subList(0, limit); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java deleted file mode 100644 index bae6e0f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.imdmk.playtime.user.top; - -import com.github.imdmk.playtime.user.User; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public interface TopUsersCache { - - CompletableFuture> getTopByPlayTime(); - CompletableFuture> getTopByPlayTime(int limit); - - void invalidateAll(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java deleted file mode 100644 index c034e01..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.github.imdmk.playtime.user.top; - -import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.injector.annotations.ConfigFile; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; - -@ConfigFile -public final class TopUsersCacheConfig extends ConfigSection { - - @Comment({ - "#", - "# Maximum number of top users fetched from the storage when building the leaderboard.", - "# This defines how many players will appear in the Top Playtime ranking.", - "#", - "# Notes:", - "# - Querying more players increases DB load but gives smoother scrolling if GUI supports it.", - "# - 30–50 is usually optimal for most servers.", - "#" - }) - public int topUsersQueryLimit = 30; - - @Comment({ - "#", - "# Duration after which the cached Top Users leaderboard expires.", - "#", - "# How it works:", - "# - When the first player opens /playtime top (or any top-playtime GUI/command),", - "# the plugin loads top users from the DB and caches the result.", - "# - Subsequent calls read the cache instantly (no DB hit).", - "# - After this duration passes, the cache auto-invalidates and the next request reloads it.", - "#", - "# Recommended:", - "# 5-15m depending on server size.", - "#" - }) - public Duration topUsersCacheExpireAfter = Duration.ofMinutes(10); - - @Comment({ - "#", - "# Maximum allowed time to execute the database query that loads the Top Users.", - "#", - "# If the DB does not return results within this time window,", - "# the fetch attempt is cancelled and the plugin will return an error message", - "# instead of blocking the main thread or freezing the server.", - "#", - "# Notes:", - "# - Keep this low. 2–5 seconds is a safe range.", - "# - Protects the server from slow/no-response database calls.", - "#" - }) - public Duration topUsersQueryTimeout = Duration.ofSeconds(3); - - @Override - public @NotNull OkaeriSerdesPack serdesPack() { - return registry -> {}; - } - - @Override - public @NotNull String fileName() { - return "leaderboard.yml"; - } -} diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java similarity index 77% rename from playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityMapperTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java index c74ca1f..8b5dcc1 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityMapperTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java @@ -1,5 +1,7 @@ package com.github.imdmk.playtime.user.repository; +import com.github.imdmk.playtime.feature.playtime.PlayTimeUserEntity; +import com.github.imdmk.playtime.feature.playtime.repository.UserEntityMapper; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.PlayTime; import org.junit.jupiter.api.Test; @@ -9,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -class UserEntityMapperTest { +class PlayTimePlayTimeUserEntityMapperTest { private final UserEntityMapper mapper = new UserEntityMapper(); @@ -22,19 +24,19 @@ void toEntityShouldMapFieldsCorrectly() { assertThat(entity.getUuid()).isEqualTo(uuid); assertThat(entity.getName()).isEqualTo("DMK"); - assertThat(entity.getPlaytimeMillis()).isEqualTo(3000); + assertThat(entity.getPlayTimeMillis()).isEqualTo(3000); } @Test void toDomainShouldMapFieldsCorrectly() { var uuid = UUID.randomUUID(); - var entity = new UserEntity(uuid, "XYZ", 5000); + var entity = new PlayTimeUserEntity(uuid, "XYZ", 5000); var user = mapper.toDomain(entity); assertThat(user.getUuid()).isEqualTo(uuid); assertThat(user.getName()).isEqualTo("XYZ"); - assertThat(user.getPlaytime().millis()).isEqualTo(5000); + assertThat(user.getPlayTime().millis()).isEqualTo(5000); } @Test diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimeUserEntityTest.java similarity index 64% rename from playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimeUserEntityTest.java index 51dbbb9..343d612 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/UserEntityTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimeUserEntityTest.java @@ -1,43 +1,44 @@ package com.github.imdmk.playtime.user.repository; +import com.github.imdmk.playtime.feature.playtime.PlayTimeUserEntity; import org.junit.jupiter.api.Test; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -class UserEntityTest { +class PlayTimeUserEntityTest { @Test void constructorShouldSetFields() { var uuid = UUID.randomUUID(); - var entity = new UserEntity(uuid, "DMK", 12345); + var entity = new PlayTimeUserEntity(uuid, "DMK", 12345); assertThat(entity.getUuid()).isEqualTo(uuid); assertThat(entity.getName()).isEqualTo("DMK"); - assertThat(entity.getPlaytimeMillis()).isEqualTo(12345); + assertThat(entity.getPlayTimeMillis()).isEqualTo(12345); } @Test void settersShouldUpdateFields() { - var entity = new UserEntity(); + var entity = new PlayTimeUserEntity(); var uuid = UUID.randomUUID(); entity.setUuid(uuid); entity.setName("DMK"); - entity.setPlaytimeMillis(500); + entity.setPlayTimeMillis(500); assertThat(entity.getUuid()).isEqualTo(uuid); assertThat(entity.getName()).isEqualTo("DMK"); - assertThat(entity.getPlaytimeMillis()).isEqualTo(500); + assertThat(entity.getPlayTimeMillis()).isEqualTo(500); } @Test void equalsShouldCompareByUuidOnly() { var uuid = UUID.randomUUID(); - var a = new UserEntity(uuid, "A", 1); - var b = new UserEntity(uuid, "B", 9999); + var a = new PlayTimeUserEntity(uuid, "A", 1); + var b = new PlayTimeUserEntity(uuid, "B", 9999); assertThat(a).isEqualTo(b); assertThat(a.hashCode()).isEqualTo(b.hashCode()); @@ -45,8 +46,8 @@ void equalsShouldCompareByUuidOnly() { @Test void equalsShouldReturnFalseForDifferentUuid() { - var a = new UserEntity(UUID.randomUUID(), "A", 1); - var b = new UserEntity(UUID.randomUUID(), "A", 1); + var a = new PlayTimeUserEntity(UUID.randomUUID(), "A", 1); + var b = new PlayTimeUserEntity(UUID.randomUUID(), "A", 1); assertThat(a).isNotEqualTo(b); } @@ -54,7 +55,7 @@ void equalsShouldReturnFalseForDifferentUuid() { @Test void toStringShouldContainFields() { var uuid = UUID.randomUUID(); - var e = new UserEntity(uuid, "X", 123); + var e = new PlayTimeUserEntity(uuid, "X", 123); var s = e.toString(); diff --git a/playtime-plugin/build.gradle.kts b/playtime-plugin/build.gradle.kts index 70455de..38250a0 100644 --- a/playtime-plugin/build.gradle.kts +++ b/playtime-plugin/build.gradle.kts @@ -15,7 +15,7 @@ tasks.build { } tasks.withType { - archiveFileName.set("AdvancedPlaytime v${project.version} (MC 1.21).jar") + archiveFileName.set("AdvancedPlayTime v${project.version} (MC 1.21).jar") mergeServiceFiles() From 9a76ae5c09e21f2900d67ac571ed116afd7812d9 Mon Sep 17 00:00:00 2001 From: imDMK Date: Tue, 13 Jan 2026 17:48:05 +0100 Subject: [PATCH 12/17] Finish work. --- .../github/imdmk/playtime/PlayTimeApi.java | 1 + .../imdmk/playtime/config/ConfigSection.java | 5 +- .../imdmk/playtime/config/ConfigService.java | 2 +- .../playtime/database/DatabaseBootstrap.java | 2 +- .../repository/ormlite/EntityMapper.java | 1 - .../feature/playtime/PlayTimeApiAdapter.java | 5 ++ .../feature/playtime/PlayTimeCommand.java | 25 +++++-- .../feature/playtime/PlayTimePlaceholder.java | 15 ++-- .../playtime/PlayTimeQueryService.java | 38 ++++++++++ .../playtime/PlayTimeSaveListener.java | 46 +++++++----- .../feature/playtime/PlayTimeUserCache.java | 36 ++++++++++ .../feature/playtime/PlayTimeUserEntity.java | 1 - .../playtime/PlayTimeUserRepository.java | 4 +- .../PlayTimeUserRepositoryOrmLite.java | 25 ++++++- .../feature/playtime/PlayTimeUserService.java | 15 ++-- .../playtime/messages/ENPlayTimeMessages.java | 2 +- .../playtime/messages/PlayTimeMessages.java | 1 - .../playtime/top/CachedPlayTimeTop.java | 18 +++++ .../playtime/top/PlayTimeTopCache.java | 33 +++++++++ .../playtime/top/PlayTimeTopCommand.java | 49 +++++++++++++ .../playtime/top/PlayTimeTopConfig.java | 71 +++++++++++++++++++ .../playtime/top/PlayTimeTopService.java | 68 ++++++++++++++++++ .../{ => top}/gui/PlayTimeTopGui.java | 2 +- .../{ => top}/gui/PlayTimeTopGuiConfig.java | 28 ++++++-- .../adventure/AdventureFormatter.java | 20 +++--- .../playtime/platform/gui/GuiRegistry.java | 2 +- .../platform/gui/config/GuiConfig.java | 9 +-- .../playtime/platform/gui/view/GuiOpener.java | 2 - .../platform/identity/IdentityCache.java | 24 ++++++- .../platform/identity/IdentityController.java | 4 +- .../platform/identity/IdentityService.java | 32 +++++++++ .../litecommands/LiteCommandsConfigurer.java | 8 ++- .../argument/UUIDArgumentResolver.java | 11 ++- .../platform/metrics/BMetricsService.java | 4 +- .../placeholder/PlaceholderService.java | 2 +- .../playtime/BukkitPlayTimeAdapter.java | 38 ++++++++++ .../platform/playtime/PlayTimeAdapter.java | 14 ++++ .../scheduler/BukkitTaskScheduler.java | 7 ++ .../PlayTimePlayTimeUserEntityMapperTest.java | 3 +- playtime-plugin/build.gradle.kts | 5 -- 40 files changed, 586 insertions(+), 92 deletions(-) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeQueryService.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserCache.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/CachedPlayTimeTop.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCache.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCommand.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopConfig.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopService.java rename playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/{ => top}/gui/PlayTimeTopGui.java (98%) rename playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/{ => top}/gui/PlayTimeTopGuiConfig.java (76%) create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityService.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/BukkitPlayTimeAdapter.java create mode 100644 playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/PlayTimeAdapter.java diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java index ddb78ad..08898e0 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java @@ -10,6 +10,7 @@ public interface PlayTimeApi { CompletableFuture getTime(@NotNull UUID uuid); CompletableFuture setTime(@NotNull UUID uuid, @NotNull PlayTime time); + CompletableFuture addTime(@NotNull UUID uuid, @NotNull PlayTime delta); CompletableFuture resetTime(@NotNull UUID uuid); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java index 2a6cfa8..47038ce 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java @@ -2,12 +2,11 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; public abstract class ConfigSection extends OkaeriConfig { - public abstract @NotNull OkaeriSerdesPack serdesPack(); + public abstract OkaeriSerdesPack serdesPack(); - public abstract @NotNull String fileName(); + public abstract String fileName(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index 5c21f7e..4dd928c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -75,7 +75,7 @@ public Set getConfigs() { } @Subscribe(event = PlayTimeShutdownEvent.class) - public void shutdown() { + private void shutdown() { configs.clear(); byType.clear(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java index e6758c0..6ca7d86 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java @@ -58,7 +58,7 @@ public ConnectionSource getConnection() { } @Subscribe(event = PlayTimeShutdownEvent.class) - public void shutdown() { + private void shutdown() { dataConnector.close(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java index baf0428..eacea6f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java @@ -3,7 +3,6 @@ import org.jetbrains.annotations.NotNull; import java.util.List; -import java.util.stream.Collectors; public interface EntityMapper { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java index 4db898c..205bd6d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeApiAdapter.java @@ -27,6 +27,11 @@ public CompletableFuture setTime(@NotNull UUID uuid, @NotNull PlayTime tim return userService.setPlayTime(uuid, time); } + @Override + public CompletableFuture addTime(@NotNull UUID uuid, @NotNull PlayTime delta) { + return userService.addPlayTime(uuid, delta); + } + @Override public CompletableFuture resetTime(@NotNull UUID uuid) { return userService.setPlayTime(uuid, PlayTime.ZERO); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java index 5422acc..83597e7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeCommand.java @@ -2,6 +2,7 @@ import com.github.imdmk.playtime.PlayTime; import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; +import com.github.imdmk.playtime.platform.identity.IdentityService; import com.github.imdmk.playtime.shared.message.MessageService; import com.github.imdmk.playtime.shared.time.Durations; import dev.rollczi.litecommands.annotations.argument.Arg; @@ -21,17 +22,26 @@ final class PlayTimeCommand { private final MessageService messageService; + private final IdentityService identityService; private final PlayTimeUserService userService; + private final PlayTimeQueryService playTimeQueryService; @Inject - PlayTimeCommand(@NotNull MessageService messageService, @NotNull PlayTimeUserService userService) { + PlayTimeCommand( + @NotNull MessageService messageService, + @NotNull IdentityService identityService, + @NotNull PlayTimeUserService userService, + @NotNull PlayTimeQueryService playTimeQueryService + ) { this.messageService = messageService; + this.identityService = identityService; this.userService = userService; + this.playTimeQueryService = playTimeQueryService; } @Execute void playTime(@Context Player player) { - userService.getPlayTime(player.getUniqueId()) + playTimeQueryService.getCurrentPlayTime(player.getUniqueId()) .thenAccept(playTime -> messageService.create() .viewer(player) .notice(n -> n.playtimeMessages.playerPlayTimeSelf()) @@ -45,10 +55,11 @@ void playTime(@Context Player player) { @Execute void playTimeOther(@Context CommandSender sender, @Arg UUID playerId) { - userService.getPlayTime(playerId) + playTimeQueryService.getCurrentPlayTime(playerId) .thenAccept(playTime -> messageService.create() .viewer(sender) .notice(n -> n.playtimeMessages.playerPlayTimeTarget()) + .placeholder("{PLAYER_NAME}", identityService.resolvePlayerName(playerId)) .placeholder("{PLAYER_PLAYTIME}", Durations.format(playTime.toDuration())) .send()) .exceptionally(e -> { @@ -58,14 +69,14 @@ void playTimeOther(@Context CommandSender sender, @Arg UUID playerId) { } @Execute - void setPlayTime(@Context CommandSender sender, @Arg UUID playerId, @Arg Duration duration) { - final PlayTime playtime = PlayTime.of(duration); + void setPlayTime(@Context CommandSender sender, @Arg UUID playerId, @Arg Duration time) { + final PlayTime playTime = PlayTime.of(time); - userService.setPlayTime(playerId, playtime) + userService.setPlayTime(playerId, playTime) .thenAccept(v -> messageService.create() .viewer(sender) .notice(n -> n.playtimeMessages.playerPlayTimeUpdated()) - .placeholder("{PLAYER_PLAYTIME}", Durations.format(duration)) + .placeholder("{PLAYER_PLAYTIME}", Durations.format(time)) .send()) .exceptionally(e -> { messageService.send(sender, notice -> notice.actionExecutionError); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java index 40109c4..dc76062 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimePlaceholder.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.feature.playtime; +import com.github.imdmk.playtime.PlayTime; import com.github.imdmk.playtime.injector.annotations.placeholderapi.Placeholder; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import com.github.imdmk.playtime.shared.time.Durations; @@ -12,11 +13,11 @@ final class PlayTimePlaceholder implements PluginPlaceholder { private static final String IDENTIFIER = "playtime"; - private final PlayTimeUserService userService; + private final PlayTimeUserCache cache; @Inject - PlayTimePlaceholder(@NotNull PlayTimeUserService userService) { - this.userService = userService; + PlayTimePlaceholder(@NotNull PlayTimeUserCache cache) { + this.cache = cache; } @Override @@ -26,8 +27,10 @@ public String identifier() { @Override public String request(@NotNull Player player, @NotNull String params) { - return Durations.format( - userService.getPlayTime(player.getUniqueId()).toDuration() - ); + final PlayTime cachedPlayTime = cache.get(player.getUniqueId()) + .map(PlayTimeUser::getPlayTime) + .orElse(PlayTime.ZERO); + + return Durations.format(cachedPlayTime.toDuration()); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeQueryService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeQueryService.java new file mode 100644 index 0000000..1f04035 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeQueryService.java @@ -0,0 +1,38 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.injector.ComponentPriority; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.platform.playtime.PlayTimeAdapter; +import org.bukkit.Server; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Service(priority = ComponentPriority.HIGH) +final class PlayTimeQueryService { + + private final Server server; + private final PlayTimeAdapter playTimeAdapter; + private final PlayTimeUserService userService; + + @Inject + PlayTimeQueryService( + @NotNull Server server, + @NotNull PlayTimeAdapter playTimeAdapter, + @NotNull PlayTimeUserService userService + ) { + this.server = server; + this.playTimeAdapter = playTimeAdapter; + this.userService = userService; + } + + public CompletableFuture getCurrentPlayTime(@NotNull UUID uuid) { + return Optional.ofNullable(server.getPlayer(uuid)) + .map(online -> CompletableFuture.completedFuture(playTimeAdapter.read(online))) + .orElseGet(() -> userService.getPlayTime(uuid)); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java index ff2bb11..b51ce49 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeSaveListener.java @@ -2,12 +2,14 @@ import com.github.imdmk.playtime.PlayTime; import com.github.imdmk.playtime.injector.annotations.Controller; -import org.bukkit.Statistic; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.github.imdmk.playtime.platform.playtime.PlayTimeAdapter; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; import java.util.UUID; @@ -15,12 +17,18 @@ @Controller final class PlayTimeSaveListener implements Listener { - private static final Statistic PLAYTIME_STATISTIC = Statistic.PLAY_ONE_MINUTE; - + private final PluginLogger logger; + private final PlayTimeAdapter playTimeAdapter; private final PlayTimeUserService userService; @Inject - PlayTimeSaveListener(PlayTimeUserService userService) { + PlayTimeSaveListener( + @NotNull PluginLogger logger, + @NotNull PlayTimeAdapter playTimeAdapter, + @NotNull PlayTimeUserService userService + ) { + this.logger = logger; + this.playTimeAdapter = playTimeAdapter; this.userService = userService; } @@ -28,27 +36,27 @@ final class PlayTimeSaveListener implements Listener { void onPlayerJoin(PlayerJoinEvent event) { final Player player = event.getPlayer(); final UUID uuid = player.getUniqueId(); - - final PlayTimeUser user = userService.getOrCreate( - uuid, - PlayTime.ofTicks(player.getStatistic(PLAYTIME_STATISTIC)) - ); - - player.setStatistic( - PLAYTIME_STATISTIC, - user.getPlayTime().toTicks() - ); + final PlayTime playTime = playTimeAdapter.read(player); + + userService.getOrCreate(uuid, playTime) + .thenAccept(user -> playTimeAdapter.write(player, user.getPlayTime())) + .exceptionally(e -> { + logger.error(e, "Failed to get user with uuid %s on PlayerJoinEvent", uuid); + return null; + }); } @EventHandler - public void onPlayerQuit(PlayerQuitEvent event) { + void onPlayerQuit(PlayerQuitEvent event) { final Player player = event.getPlayer(); final UUID uuid = player.getUniqueId(); + final PlayTime playTime = playTimeAdapter.read(player); - final int playTimeTicks = player.getStatistic(PLAYTIME_STATISTIC); - final PlayTime playTime = PlayTime.ofTicks(playTimeTicks); - - userService.setPlayTime(uuid, playTime); + userService.setPlayTime(uuid, playTime) + .exceptionally(e -> { + logger.error(e, "Failed to set user playTime with uuid %s on PlayerQuitEvent", uuid); + return null; + }); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserCache.java new file mode 100644 index 0000000..a06e3e5 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserCache.java @@ -0,0 +1,36 @@ +package com.github.imdmk.playtime.feature.playtime; + +import com.github.imdmk.playtime.injector.ComponentPriority; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service(priority = ComponentPriority.LOWEST) +final class PlayTimeUserCache { + + private final Map cache = new ConcurrentHashMap<>(); + + public void put(@NotNull UUID uuid, @NotNull PlayTimeUser user) { + cache.put(uuid, user); + } + + public Optional get(@NotNull UUID uuid) { + return Optional.ofNullable(cache.get(uuid)); + } + + void remove(@NotNull UUID uuid) { + cache.remove(uuid); + } + + @Subscribe(event = PlayTimeShutdownEvent.class) + private void shutdown() { + cache.clear(); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java index 884fd92..ade9dc0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserEntity.java @@ -4,7 +4,6 @@ import com.j256.ormlite.table.DatabaseTable; import org.jetbrains.annotations.NotNull; -import java.util.Objects; import java.util.UUID; @DatabaseTable(tableName = PlayTimeUserEntityMeta.TABLE) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java index ec2ac16..7ff733e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepository.java @@ -1,16 +1,16 @@ package com.github.imdmk.playtime.feature.playtime; import org.jetbrains.annotations.NotNull; -import panda.std.reactive.Completable; import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; -interface PlayTimeUserRepository { +public interface PlayTimeUserRepository { CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture> findTopByPlayTime(int limit); CompletableFuture> findAll(); CompletableFuture deleteByUuid(@NotNull UUID uuid); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java index a8209e6..d41386c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserRepositoryOrmLite.java @@ -49,6 +49,29 @@ public CompletableFuture> findByUuid(@NotNull UUID uuid) }); } + @Override + public CompletableFuture> findTopByPlayTime(int limit) { + if (limit <= 0) { + return CompletableFuture.completedFuture(List.of()); + } + + return execute(() -> { + try { + return dao.queryBuilder() + .orderBy(PlayTimeUserEntityMeta.Col.PLAYTIME_MILLIS, false) + .limit((long) limit) + .query() + .stream() + .map(mapper::toDomain) + .toList(); + } catch (SQLException e) { + logger.error(e, "Failed to query top playtime users (limit=%d)", limit); + throw new IllegalStateException("Database failure", e); + } + }); + } + + @Override public CompletableFuture> findAll() { return execute(() -> { @@ -78,7 +101,7 @@ public CompletableFuture deleteByUuid(@NotNull UUID uuid) { @Override public CompletableFuture save(@NotNull PlayTimeUser user) { - execute(() -> { + return execute(() -> { try { dao.createOrUpdate(mapper.toEntity(user)); return null; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java index 0f54103..4981dbb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserService.java @@ -9,12 +9,14 @@ import java.util.concurrent.CompletableFuture; @Service -class PlayTimeUserService { +final class PlayTimeUserService { + private final PlayTimeUserCache cache; private final PlayTimeUserRepository repository; @Inject - PlayTimeUserService(@NotNull PlayTimeUserRepository repository) { + PlayTimeUserService(@NotNull PlayTimeUserCache cache, @NotNull PlayTimeUserRepository repository) { + this.cache = cache; this.repository = repository; } @@ -22,16 +24,19 @@ CompletableFuture getOrCreate( @NotNull UUID uuid, @NotNull PlayTime initialPlayTime ) { - return repository.findByUuid(uuid) + return cache.get(uuid) + .map(CompletableFuture::completedFuture) + .orElseGet(() -> repository.findByUuid(uuid) .thenCompose(optional -> { if (optional.isPresent()) { return CompletableFuture.completedFuture(optional.get()); } - PlayTimeUser user = new PlayTimeUser(uuid, initialPlayTime); + final PlayTimeUser user = new PlayTimeUser(uuid, initialPlayTime); return repository.save(user) .thenApply(v -> user); - }); + })); + } CompletableFuture getOrCreate(@NotNull UUID uuid) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java index 298681a..af98816 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/ENPlayTimeMessages.java @@ -29,7 +29,7 @@ public final class ENPlayTimeMessages "#" }) Notice playerPlayTimeTarget = Notice.chat( - "Target player has spent {PLAYER_PLAYTIME} on this server." + "Player {PLAYER_NAME} has spent {PLAYER_PLAYTIME} on this server." ); @Comment({ diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java index e5d2b62..152834e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/messages/PlayTimeMessages.java @@ -6,7 +6,6 @@ public interface PlayTimeMessages { Notice playerPlayTimeSelf(); Notice playerPlayTimeTarget(); - Notice playerPlayTimeUpdated(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/CachedPlayTimeTop.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/CachedPlayTimeTop.java new file mode 100644 index 0000000..1284ab7 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/CachedPlayTimeTop.java @@ -0,0 +1,18 @@ +package com.github.imdmk.playtime.feature.playtime.top; + +import com.github.imdmk.playtime.feature.playtime.PlayTimeUser; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +record CachedPlayTimeTop( + @NotNull List users, + @NotNull Instant loadedAt +) { + boolean isExpired(@NotNull Duration ttl, @NotNull Instant now) { + return ttl.isPositive() && loadedAt.plus(ttl).isBefore(now); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCache.java new file mode 100644 index 0000000..7e63812 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCache.java @@ -0,0 +1,33 @@ +package com.github.imdmk.playtime.feature.playtime.top; + +import com.github.imdmk.playtime.feature.playtime.PlayTimeUser; +import com.github.imdmk.playtime.injector.ComponentPriority; +import com.github.imdmk.playtime.injector.annotations.Service; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +@Service(priority = ComponentPriority.LOW) +final class PlayTimeTopCache { + + private final AtomicReference snapshot = new AtomicReference<>(); + + Optional get() { + return Optional.ofNullable(snapshot.get()); + } + + void update(@NotNull List users) { + snapshot.set(new CachedPlayTimeTop( + List.copyOf(users), + Instant.now() + )); + } + + void invalidate() { + snapshot.set(null); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCommand.java new file mode 100644 index 0000000..0fbe592 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopCommand.java @@ -0,0 +1,49 @@ +package com.github.imdmk.playtime.feature.playtime.top; + +import com.github.imdmk.playtime.feature.playtime.top.gui.PlayTimeTopGui; +import com.github.imdmk.playtime.injector.annotations.lite.LiteCommand; +import com.github.imdmk.playtime.platform.gui.view.GuiOpener; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.github.imdmk.playtime.shared.message.MessageService; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.permission.Permission; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@LiteCommand +@Command(name = "playtime top") +@Permission("command.playtime.top") +final class PlayTimeTopCommand { + + private final PluginLogger logger; + private final MessageService messageService; + private final GuiOpener guiOpener; + private final PlayTimeTopService topService; + + @Inject + PlayTimeTopCommand( + @NotNull PluginLogger logger, + @NotNull MessageService messageService, + @NotNull GuiOpener guiOpener, + @NotNull PlayTimeTopService topService + ) { + this.logger = logger; + this.messageService = messageService; + this.guiOpener = guiOpener; + this.topService = topService; + } + + @Execute + void openGui(@Context Player viewer) { + topService.getTop() + .thenAccept(topUsers -> guiOpener.open(PlayTimeTopGui.class, viewer, topUsers)) + .exceptionally(e -> { + messageService.send(viewer, notice -> notice.actionExecutionError); + logger.error(e, "Failed to open top users"); + return null; + }); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopConfig.java new file mode 100644 index 0000000..d2efdf7 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopConfig.java @@ -0,0 +1,71 @@ +package com.github.imdmk.playtime.feature.playtime.top; + +import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; +import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; + +import java.time.Duration; + +@ConfigFile +public final class PlayTimeTopConfig extends ConfigSection { + + @Comment({ + "#", + "# How many users are fetched from the database when building the leaderboard snapshot.", + "#", + "# This affects DATABASE LOAD only.", + "# The fetched list is cached and reused for GUI, commands and pagination.", + "#", + "# Recommended:", + "# - Small servers: 20–30", + "# - Medium servers: 30–50", + "# - Large servers: 50–100", + "#" + }) + public int topUsersQueryLimit = 50; + + @Comment({ + "#", + "# How many users are displayed in the /playtime top GUI.", + "#", + "# This does NOT affect database queries.", + "# It only slices the cached leaderboard snapshot.", + "#" + }) + public int topUsersGuiLimit = 10; + + @Comment({ + "#", + "# How long the cached leaderboard snapshot stays valid.", + "#", + "# After this time expires, the next request will reload data from the database.", + "#", + "# Recommended:", + "# - 5–15 minutes", + "#" + }) + public Duration topUsersCacheExpireAfter = Duration.ofMinutes(10); + + @Comment({ + "#", + "# Maximum time allowed for the database query that loads the leaderboard.", + "#", + "# If exceeded, the operation fails instead of blocking the server.", + "#", + "# Recommended:", + "# - 2–5 seconds", + "#" + }) + public Duration topUsersQueryTimeout = Duration.ofSeconds(3); + + @Override + public OkaeriSerdesPack serdesPack() { + return registry -> {}; + } + + @Override + public String fileName() { + return "playtime-top.yml"; + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopService.java new file mode 100644 index 0000000..37755be --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/PlayTimeTopService.java @@ -0,0 +1,68 @@ +package com.github.imdmk.playtime.feature.playtime.top; + +import com.github.imdmk.playtime.feature.playtime.PlayTimeUser; +import com.github.imdmk.playtime.feature.playtime.PlayTimeUserRepository; +import com.github.imdmk.playtime.injector.annotations.Service; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Service +final class PlayTimeTopService { + + private final PlayTimeTopCache cache; + private final PlayTimeTopConfig config; + private final PlayTimeUserRepository repository; + + @Inject + PlayTimeTopService( + @NotNull PlayTimeTopCache cache, + @NotNull PlayTimeTopConfig config, + @NotNull PlayTimeUserRepository repository + ) { + this.cache = cache; + this.config = config; + this.repository = repository; + } + + CompletableFuture> getTop() { + return getTopForDisplay(config.topUsersGuiLimit); + } + + CompletableFuture> getTopForDisplay(int displayLimit) { + if (displayLimit <= 0) { + return CompletableFuture.completedFuture(List.of()); + } + + final Instant now = Instant.now(); + + final Optional cached = cache.get(); + if (cached.isPresent() && !cached.get().isExpired(config.topUsersCacheExpireAfter, now)) { + return CompletableFuture.completedFuture( + slice(cached.get().users(), displayLimit) + ); + } + + return repository.findTopByPlayTime(config.topUsersQueryLimit) + .orTimeout(config.topUsersQueryTimeout.toMillis(), TimeUnit.MILLISECONDS) + .thenApply(users -> { + cache.update(users); + return slice(users, displayLimit); + }); + } + + void invalidateCache() { + cache.invalidate(); + } + + private static List slice(List users, int limit) { + return users.size() <= limit ? users : users.subList(0, limit); + } +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/gui/PlayTimeTopGui.java similarity index 98% rename from playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/gui/PlayTimeTopGui.java index 6d74248..f0c4fa0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/gui/PlayTimeTopGui.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.feature.playtime.gui; +package com.github.imdmk.playtime.feature.playtime.top.gui; import com.github.imdmk.playtime.feature.playtime.PlayTimeUser; import com.github.imdmk.playtime.injector.annotations.Gui; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/gui/PlayTimeTopGuiConfig.java similarity index 76% rename from playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/gui/PlayTimeTopGuiConfig.java index aea6fe1..efbf111 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/top/gui/PlayTimeTopGuiConfig.java @@ -1,19 +1,25 @@ -package com.github.imdmk.playtime.feature.playtime.gui; +package com.github.imdmk.playtime.feature.playtime.top.gui; +import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; +import com.github.imdmk.playtime.platform.adventure.AdventureComponentSerializer; import com.github.imdmk.playtime.platform.adventure.AdventureComponents; import com.github.imdmk.playtime.platform.gui.GuiType; import com.github.imdmk.playtime.platform.gui.config.ConfigurableGui; import com.github.imdmk.playtime.platform.gui.item.ItemGui; -import eu.okaeri.configs.OkaeriConfig; +import com.github.imdmk.playtime.platform.gui.item.ItemGuiSerializer; +import com.github.imdmk.playtime.platform.serdes.EnchantmentSerializer; +import com.github.imdmk.playtime.platform.serdes.SoundSerializer; import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.inventory.ItemFlag; import java.util.Collections; -public final class PlayTimeTopGuiConfig - extends OkaeriConfig +@ConfigFile +public final class PlayTimeTopGuiConfig extends ConfigSection implements ConfigurableGui { @Comment({ @@ -97,4 +103,18 @@ public int rows() { return rows; } + @Override + public OkaeriSerdesPack serdesPack() { + return registry -> { + registry.register(new AdventureComponentSerializer()); + registry.register(new ItemGuiSerializer()); + registry.register(new EnchantmentSerializer()); + registry.register(new SoundSerializer()); + }; + } + + @Override + public String fileName() { + return "topGuiConfig.yml"; + } } \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java index 2988ba1..024a8d5 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java @@ -1,11 +1,10 @@ package com.github.imdmk.playtime.platform.adventure; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.jetbrains.annotations.NotNull; -import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; public final class AdventureFormatter { @@ -29,15 +28,14 @@ public static Component format(@NotNull Component input, @NotNull AdventurePlace return input; } - // Sort keys by descending length to avoid substring overlap - final List> ordered = placeholders.asMap().entrySet().stream() - .sorted(Comparator.>comparingInt(e -> e.getKey().length()).reversed()) - .toList(); + final MiniMessage miniMessage = AdventureComponents.miniMessage(); - return input.replaceText(builder -> { - for (var entry : ordered) { - builder.matchLiteral(entry.getKey()).replacement(entry.getValue()); - } - }); + String raw = miniMessage.serialize(input); + for (final var entry : placeholders.asMap().entrySet()) { + raw = raw.replace(entry.getKey(), miniMessage.serialize(entry.getValue())); + } + + return miniMessage.deserialize(raw); } + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java index 568964f..1dd0944 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java @@ -56,7 +56,7 @@ public IdentifiableGui unregister(@NotNull String id) { } @Subscribe(event = PlayTimeShutdownEvent.class) - public void unregisterAll() { + private void shutdown() { byId.clear(); byClass.clear(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java index dddc8c7..98a0138 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.platform.gui.config; import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.feature.playtime.gui.PlayTimeTopGuiConfig; import com.github.imdmk.playtime.injector.annotations.ConfigFile; import com.github.imdmk.playtime.platform.adventure.AdventureComponentSerializer; import com.github.imdmk.playtime.platform.gui.item.ItemGuiSerializer; @@ -9,19 +8,15 @@ import com.github.imdmk.playtime.platform.serdes.SoundSerializer; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; @ConfigFile public final class GuiConfig extends ConfigSection { - @Comment({"#", "# PlayTime top GUI", "#"}) - public PlayTimeTopGuiConfig playTimeTopGui = new PlayTimeTopGuiConfig(); - @Comment({"#", "# Navigation Bar", "#"}) public NavigationBarConfig navigationBar = new NavigationBarConfig(); @Override - public @NotNull OkaeriSerdesPack serdesPack() { + public OkaeriSerdesPack serdesPack() { return registry -> { registry.register(new AdventureComponentSerializer()); registry.register(new ItemGuiSerializer()); @@ -31,7 +26,7 @@ public final class GuiConfig extends ConfigSection { } @Override - public @NotNull String fileName() { + public String fileName() { return "gui.yml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java index ee7759f..f7d30f2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java @@ -2,9 +2,7 @@ import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.platform.gui.GuiRegistry; -import com.github.imdmk.playtime.platform.gui.IdentifiableGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import dev.triumphteam.gui.guis.BaseGui; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java index 10ed303..b362eec 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityCache.java @@ -1,35 +1,55 @@ package com.github.imdmk.playtime.platform.identity; +import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -@Service +@Service(priority = ComponentPriority.LOWEST) public final class IdentityCache { private final Map nameToUuid = new ConcurrentHashMap<>(); + private final Map uuidToName = new ConcurrentHashMap<>(); public void update(@NotNull Player player) { nameToUuid.put(player.getName(), player.getUniqueId()); + uuidToName.put(player.getUniqueId(), player.getName()); } public void remove(@NotNull Player player) { nameToUuid.remove(player.getName()); + uuidToName.remove(player.getUniqueId()); } public Optional getUuidByName(@NotNull String name) { return Optional.ofNullable(nameToUuid.get(name)); } + public Optional getNameByUuid(@NotNull UUID playerId) { + return Optional.ofNullable(uuidToName.get(playerId)); + } + + @Unmodifiable + public Set getPlayerUuids() { + return Set.copyOf(uuidToName.keySet()); + } + + @Unmodifiable + public Set getPlayerNames() { + return Set.copyOf(nameToUuid.keySet()); + } + @Subscribe(event = PlayTimeShutdownEvent.class) - public void shutdown() { + private void shutdown() { nameToUuid.clear(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java index 43c18cf..9de6765 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityController.java @@ -19,12 +19,12 @@ final class IdentityController implements Listener { } @EventHandler - public void onJoin(PlayerJoinEvent event) { + public void onPlayerJoin(PlayerJoinEvent event) { cache.update(event.getPlayer()); } @EventHandler - public void onQuit(PlayerQuitEvent event) { + public void onPlayerQuit(PlayerQuitEvent event) { cache.remove(event.getPlayer()); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityService.java new file mode 100644 index 0000000..73240ff --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/identity/IdentityService.java @@ -0,0 +1,32 @@ +package com.github.imdmk.playtime.platform.identity; + +import com.github.imdmk.playtime.injector.ComponentPriority; +import com.github.imdmk.playtime.injector.annotations.Service; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; + +@Service(priority = ComponentPriority.LOW) +public final class IdentityService { + + private static final String UNKNOWN_PLAYER_NAME = "Unknown"; + + private final Server server; + private final IdentityCache cache; + + public IdentityService(@NotNull Server server, @NotNull IdentityCache cache) { + this.server = server; + this.cache = cache; + } + + public String resolvePlayerName(@NotNull UUID playerId) { + final Optional cached = cache.getNameByUuid(playerId); + return cached.orElseGet(() -> Optional.ofNullable(server.getPlayer(playerId)) + .map(Player::getName) + .orElse(UNKNOWN_PLAYER_NAME)); + + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java index 65f1679..103a298 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/LiteCommandsConfigurer.java @@ -4,6 +4,7 @@ import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.injector.subscriber.Subscribe; import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeInitializeEvent; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import dev.rollczi.litecommands.LiteCommands; import dev.rollczi.litecommands.LiteCommandsBuilder; import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; @@ -37,8 +38,13 @@ public LiteCommands liteCommands() { } @Subscribe(event = PlayTimeInitializeEvent.class) - private void onInitialize() { + private void initialize() { this.liteCommands = builder.build(); } + + @Subscribe(event = PlayTimeShutdownEvent.class) + private void shutdown() { + liteCommands.unregister(); + } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java index 1664b70..850eaba 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/argument/UUIDArgumentResolver.java @@ -7,6 +7,8 @@ import dev.rollczi.litecommands.argument.parser.ParseResult; import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.suggestion.SuggestionContext; +import dev.rollczi.litecommands.suggestion.SuggestionResult; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; @@ -20,7 +22,7 @@ class UUIDArgumentResolver extends ArgumentResolver { private final MessageConfig messageConfig; @Inject - UUIDArgumentResolver(IdentityCache cache, @NotNull MessageConfig messageConfig) { + UUIDArgumentResolver(@NotNull IdentityCache cache, @NotNull MessageConfig messageConfig) { this.cache = cache; this.messageConfig = messageConfig; } @@ -31,4 +33,9 @@ protected ParseResult parse(Invocation invocation, Argument .map(ParseResult::success) .orElse(ParseResult.failure(messageConfig.playerNotFound)); } -} + + @Override + public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { + return SuggestionResult.of(cache.getPlayerNames()); + } +} \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java index 7706566..8dfb90b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/metrics/BMetricsService.java @@ -9,7 +9,7 @@ import org.panda_lang.utilities.inject.annotations.Inject; @Service -class BMetricsService { +final class BMetricsService { private static final int METRICS_ID = 19362; private final Metrics metrics; @@ -20,7 +20,7 @@ class BMetricsService { } @Subscribe(event = PlayTimeShutdownEvent.class) - void shutdown() { + private void shutdown() { metrics.shutdown(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java index 0a67535..3a87d15 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PlaceholderService.java @@ -30,7 +30,7 @@ public void unregister(@NotNull PluginPlaceholder placeholder) { } @Subscribe(event = PlayTimeShutdownEvent.class) - public void unregisterAll() { + private void shutdown() { registry.unregisterAll(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/BukkitPlayTimeAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/BukkitPlayTimeAdapter.java new file mode 100644 index 0000000..f8918a1 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/BukkitPlayTimeAdapter.java @@ -0,0 +1,38 @@ +package com.github.imdmk.playtime.platform.playtime; + +import com.github.imdmk.playtime.PlayTime; +import com.github.imdmk.playtime.injector.ComponentPriority; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@Service(priority = ComponentPriority.LOW) +final class BukkitPlayTimeAdapter implements PlayTimeAdapter { + + private final TaskScheduler scheduler; + + @Inject + BukkitPlayTimeAdapter(@NotNull TaskScheduler scheduler) { + this.scheduler = scheduler; + } + + @NotNull + @Override + public PlayTime read(@NotNull Player player) { + return PlayTime.ofTicks(player.getStatistic(Statistic.PLAY_ONE_MINUTE)); + } + + @Override + public void write(@NotNull Player player, @NotNull PlayTime playTime) { + scheduler.runSync(() -> { + if (!player.isOnline()) { + return; + } + + player.setStatistic(Statistic.PLAY_ONE_MINUTE, playTime.toTicks()); + }); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/PlayTimeAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/PlayTimeAdapter.java new file mode 100644 index 0000000..f9de911 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/playtime/PlayTimeAdapter.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.platform.playtime; + +import com.github.imdmk.playtime.PlayTime; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public interface PlayTimeAdapter { + + @NotNull PlayTime read(@NotNull Player player); + + void write(@NotNull Player player, @NotNull PlayTime playTime); + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java index b3343ac..c424072 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java @@ -2,6 +2,8 @@ import com.github.imdmk.playtime.injector.ComponentPriority; import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.shared.time.Durations; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; @@ -76,4 +78,9 @@ public void cancelTask(int taskId) { public void cancelAllTasks() { scheduler.cancelTasks(plugin); } + + @Subscribe(event = PlayTimeShutdownEvent.class) + private void shutdown() { + cancelAllTasks(); + } } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java index 8b5dcc1..d3c701b 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java @@ -1,14 +1,13 @@ package com.github.imdmk.playtime.user.repository; +import com.github.imdmk.playtime.PlayTime; import com.github.imdmk.playtime.feature.playtime.PlayTimeUserEntity; import com.github.imdmk.playtime.feature.playtime.repository.UserEntityMapper; import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.PlayTime; import org.junit.jupiter.api.Test; import java.util.UUID; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class PlayTimePlayTimeUserEntityMapperTest { diff --git a/playtime-plugin/build.gradle.kts b/playtime-plugin/build.gradle.kts index 38250a0..98100aa 100644 --- a/playtime-plugin/build.gradle.kts +++ b/playtime-plugin/build.gradle.kts @@ -51,11 +51,6 @@ tasks.withType { ).forEach { pkg -> relocate(pkg, "$relocationPrefix.$pkg") } - -// minimize { -// exclude { dependency -> dependency.moduleGroup == "com.github.imdmk.playtime" } -// exclude(dependency("com.github.ben-manes.caffeine:caffeine")) -// } } bukkit { From 673d86a4669dd9402ccf7baf4e44c735843c3055 Mon Sep 17 00:00:00 2001 From: imDMK Date: Tue, 13 Jan 2026 17:51:07 +0100 Subject: [PATCH 13/17] Fix tests. --- .../github/imdmk/playtime/PlayTimeTest.java | 45 +---- .../com/github/imdmk/playtime/UserTest.java | 155 ---------------- .../DataSourceConfigurerFactoryTest.java | 35 ---- .../repository/RepositoryManagerTest.java | 105 ----------- .../repository/ormlite/EntityMapperTest.java | 63 ------- .../ormlite/OrmLiteRepositoryTest.java | 169 ------------------ .../adventure/AdventureComponentsTest.java | 62 ------- .../adventure/AdventureFormatterTest.java | 75 -------- .../adventure/AdventurePlaceholdersTest.java | 82 --------- .../time/DurationFormatStyleTest.java | 41 ----- .../playtime/time/DurationSplitterTest.java | 34 ---- .../imdmk/playtime/time/DurationUnitTest.java | 34 ---- .../imdmk/playtime/time/DurationsTest.java | 63 ------- .../user/cache/CaffeineUserCacheTest.java | 154 ---------------- .../PlayTimePlayTimeUserEntityMapperTest.java | 53 ------ .../repository/PlayTimeUserEntityTest.java | 67 ------- 16 files changed, 3 insertions(+), 1234 deletions(-) delete mode 100644 playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureComponentsTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureFormatterTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventurePlaceholdersTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/user/cache/CaffeineUserCacheTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java delete mode 100644 playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimeUserEntityTest.java diff --git a/playtime-api/src/test/java/com/github/imdmk/playtime/PlayTimeTest.java b/playtime-api/src/test/java/com/github/imdmk/playtime/PlayTimeTest.java index a46cb51..808d105 100644 --- a/playtime-api/src/test/java/com/github/imdmk/playtime/PlayTimeTest.java +++ b/playtime-api/src/test/java/com/github/imdmk/playtime/PlayTimeTest.java @@ -32,7 +32,7 @@ void shouldRejectNegativeMillis() { @Test void shouldCreateFromSeconds() { - PlayTime t = PlayTime.ofSeconds(2); + PlayTime t = PlayTime.of(Duration.ofSeconds(2)); assertThat(t.millis()).isEqualTo(2000); } @@ -45,14 +45,14 @@ void shouldCreateFromTicks() { @Test void shouldCreateFromDuration() { Duration d = Duration.ofMillis(1234); - PlayTime t = PlayTime.from(d); + PlayTime t = PlayTime.of(d); assertThat(t.millis()).isEqualTo(1234); } @Test void shouldRejectNullDuration() { assertThatNullPointerException() - .isThrownBy(() -> PlayTime.from(null)) + .isThrownBy(() -> PlayTime.of(null)) .withMessageContaining("duration"); } } @@ -124,45 +124,6 @@ void shouldRejectNullInMinus() { } } - @Nested - @DisplayName("Min/Max") - class MinMaxTests { - - @Test - void shouldReturnMin() { - PlayTime a = PlayTime.ofMillis(500); - PlayTime b = PlayTime.ofMillis(1000); - - assertThat(a.min(b)).isEqualTo(a); - assertThat(b.min(a)).isEqualTo(a); - } - - @Test - void shouldReturnMax() { - PlayTime a = PlayTime.ofMillis(500); - PlayTime b = PlayTime.ofMillis(1000); - - assertThat(a.max(b)).isEqualTo(b); - assertThat(b.max(a)).isEqualTo(b); - } - - @Test - void shouldRejectNullInMin() { - PlayTime a = PlayTime.ZERO; - - assertThatNullPointerException() - .isThrownBy(() -> a.min(null)); - } - - @Test - void shouldRejectNullInMax() { - PlayTime a = PlayTime.ZERO; - - assertThatNullPointerException() - .isThrownBy(() -> a.max(null)); - } - } - @Nested @DisplayName("Comparison") class ComparisonTests { diff --git a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java b/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java deleted file mode 100644 index ed93732..0000000 --- a/playtime-api/src/test/java/com/github/imdmk/playtime/UserTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.User; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -class UserTest { - - @Nested - @DisplayName("Constructor") - class ConstructorTests { - - @Test - void shouldInitializeFieldsCorrectly() { - UUID uuid = UUID.randomUUID(); - PlayTime time = PlayTime.ofMillis(5000); - - User user = new User(uuid, "Player", time); - - assertThat(user.getUuid()).isEqualTo(uuid); - assertThat(user.getName()).isEqualTo("Player"); - assertThat(user.getPlayTime()).isEqualTo(time); - } - - @Test - void shouldCreateUserWithZeroPlayTime() { - User user = new User(UUID.randomUUID(), "Player"); - - assertThat(user.getPlayTime()).isEqualTo(PlayTime.ZERO); - } - - @Test - void shouldThrowWhenNameIsNull() { - UUID uuid = UUID.randomUUID(); - - assertThatNullPointerException() - .isThrownBy(() -> new User(uuid, null, PlayTime.ZERO)) - .withMessageContaining("name"); - } - - @Test - void shouldThrowWhenPlayTimeIsNull() { - UUID uuid = UUID.randomUUID(); - - assertThatNullPointerException() - .isThrownBy(() -> new User(uuid, "Player", null)) - .withMessageContaining("playtime"); - } - } - - @Nested - @DisplayName("Name mutation") - class NameTests { - - @Test - void shouldUpdateName() { - User user = new User(UUID.randomUUID(), "Old"); - - user.setName("New"); - - assertThat(user.getName()).isEqualTo("New"); - } - - @Test - void shouldRejectNullName() { - User user = new User(UUID.randomUUID(), "Old"); - - assertThatNullPointerException() - .isThrownBy(() -> user.setName(null)) - .withMessageContaining("name"); - } - - @Test - void shouldRejectBlankName() { - User user = new User(UUID.randomUUID(), "Old"); - - assertThatIllegalArgumentException() - .isThrownBy(() -> user.setName(" ")) - .withMessageContaining("blank"); - } - } - - @Nested - @DisplayName("PlayTime mutation") - class PlayTimeTests { - - @Test - void shouldReturnCurrentPlayTimeAsUserTime() { - User user = new User(UUID.randomUUID(), "Player", PlayTime.ofMillis(1000)); - - assertThat(user.getPlayTime().millis()).isEqualTo(1000); - } - - @Test - void shouldSetNewPlayTime() { - User user = new User(UUID.randomUUID(), "Player"); - - user.setPlayTime(PlayTime.ofMillis(12345)); - - assertThat(user.getPlayTime().millis()).isEqualTo(12345); - } - - @Test - void shouldRejectNullPlayTime() { - User user = new User(UUID.randomUUID(), "Player"); - - assertThatNullPointerException() - .isThrownBy(() -> user.setPlayTime(null)) - .withMessageContaining("playtime"); - } - } - - @Nested - @DisplayName("Equality & Hashcode") - class EqualityTests { - - @Test - void usersWithSameUuidShouldBeEqual() { - UUID uuid = UUID.randomUUID(); - - User u1 = new User(uuid, "A"); - User u2 = new User(uuid, "B"); - - assertThat(u1).isEqualTo(u2); - assertThat(u1).hasSameHashCodeAs(u2); - } - - @Test - void usersWithDifferentUuidShouldNotBeEqual() { - User u1 = new User(UUID.randomUUID(), "A"); - User u2 = new User(UUID.randomUUID(), "A"); - - assertThat(u1).isNotEqualTo(u2); - } - } - - @Test - void toStringShouldContainKeyInformation() { - User user = new User(UUID.randomUUID(), "Player", PlayTime.ofMillis(100)); - - String str = user.toString(); - - assertThat(str) - .contains("uuid=") - .contains("name='Player'") - .contains("playtimeMillis=100"); - } -} diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java deleted file mode 100644 index d12ac1d..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.imdmk.playtime.database.driver; - -import com.github.imdmk.playtime.database.DatabaseMode; -import com.github.imdmk.playtime.database.configurer.DataSourceConfigurerFactory; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -class DataSourceConfigurerFactoryTest { - - @Test - void shouldReturnConfigurerForEachSupportedMode() { - for (DatabaseMode mode : DatabaseMode.values()) { - assertThatCode(() -> DataSourceConfigurerFactory.getFor(mode)) - .doesNotThrowAnyException(); - } - } - - @Test - void shouldRejectNullMode() { - assertThatNullPointerException() - .isThrownBy(() -> DataSourceConfigurerFactory.getFor(null)); - } - - @Test - void shouldThrowForUnsupportedMode() { - // Just to be sure — if enum expands in future - // create invalid fake enum value - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> DataSourceConfigurerFactory.getFor(DatabaseMode.valueOf("NON_EXISTENT"))); // if ever added - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java deleted file mode 100644 index c821959..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.github.imdmk.playtime.database.repository; - -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.j256.ormlite.support.ConnectionSource; -import org.junit.jupiter.api.Test; - -import java.sql.SQLException; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -class RepositoryManagerTest { - - @Test - void registerShouldStoreRepository() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryBootstrap repo = mock(RepositoryBootstrap.class); - - RepositoryManager manager = new RepositoryManager(logger); - - manager.register(repo); - - // Registering same repo twice should log warning - manager.register(repo); - - verify(logger).warn( - eq("Repository %s already registered — skipping"), - eq(repo.getClass().getSimpleName()) - ); - } - - @Test - void startAllShouldInvokeStartOnEachRepository() throws Exception { - PluginLogger logger = mock(PluginLogger.class); - RepositoryBootstrap repo1 = mock(RepositoryBootstrap.class); - RepositoryBootstrap repo2 = mock(RepositoryBootstrap.class); - ConnectionSource source = mock(ConnectionSource.class); - - RepositoryManager manager = new RepositoryManager(logger); - manager.register(repo1, repo2); - - manager.startAll(source); - - verify(repo1).start(source); - verify(repo2).start(source); - } - - @Test - void startAllShouldLogAndRethrowSQLException() throws Exception { - PluginLogger logger = mock(PluginLogger.class); - RepositoryBootstrap faulty = mock(RepositoryBootstrap.class); - ConnectionSource source = mock(ConnectionSource.class); - - doThrow(new SQLException("boom")).when(faulty).start(source); - - RepositoryManager manager = new RepositoryManager(logger); - manager.register(faulty); - - assertThatThrownBy(() -> manager.startAll(source)) - .isInstanceOf(SQLException.class); - - verify(logger).error(any(Exception.class), - eq("Failed to start repository: %s"), - eq(faulty.getClass().getSimpleName())); - } - - @Test - void closeShouldInvokeCloseOnEachRepository() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryBootstrap repo1 = mock(RepositoryBootstrap.class); - RepositoryBootstrap repo2 = mock(RepositoryBootstrap.class); - - RepositoryManager manager = new RepositoryManager(logger); - manager.register(repo1, repo2); - - manager.close(); - - verify(repo1).close(); - verify(repo2).close(); - } - - @Test - void closeShouldLogWarningWhenRepositoryThrows() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryBootstrap repo = mock(RepositoryBootstrap.class); - - doThrow(new RuntimeException("err")).when(repo).close(); - - RepositoryManager manager = new RepositoryManager(logger); - manager.register(repo); - - manager.close(); - - verify(logger).warn( - any(Exception.class), - eq("Error while closing repository: %s"), - eq(repo.getClass().getSimpleName()) - ); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java deleted file mode 100644 index 9e387bb..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.imdmk.playtime.database.repository.ormlite; - -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -class EntityMapperTest { - - private static class FakeMapper implements EntityMapper { - - @Override - public @NotNull String toEntity(@NotNull Integer domain) { - return "E" + domain; - } - - @Override - public @NotNull Integer toDomain(@NotNull String entity) { - return Integer.parseInt(entity.substring(1)); - } - } - - @Test - void toEntityListShouldMapAllDomainObjects() { - EntityMapper mapper = new FakeMapper(); - - List domain = List.of(1, 2, 3); - List entities = mapper.toEntityList(domain); - - assertThat(entities).containsExactly("E1", "E2", "E3"); - } - - @Test - void toDomainListShouldMapAllEntities() { - EntityMapper mapper = new FakeMapper(); - - List entities = List.of("E5", "E10", "E99"); - List domain = mapper.toDomainList(entities); - - assertThat(domain).containsExactly(5, 10, 99); - } - - @Test - void toEntityListShouldHandleEmptyList() { - EntityMapper mapper = new FakeMapper(); - - List result = mapper.toEntityList(List.of()); - - assertThat(result).isEmpty(); - } - - @Test - void toDomainListShouldHandleEmptyList() { - EntityMapper mapper = new FakeMapper(); - - List result = mapper.toDomainList(List.of()); - - assertThat(result).isEmpty(); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java deleted file mode 100644 index 134d3c1..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.github.imdmk.playtime.database.repository.ormlite; - -import com.github.imdmk.playtime.database.repository.RepositoryContext; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.j256.ormlite.dao.Dao; -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -class OrmLiteRepositoryTest { - - public static class TestDaoRepository extends OrmLiteRepository { - - public TestDaoRepository(PluginLogger logger, RepositoryContext context) { - super(logger, context); - } - - @Override - protected Class entityClass() { - return String.class; - } - - @Override - protected List> entitySubClasses() { - return List.of(); - } - - public R callWithDao(Supplier supplier) { - return withDao(supplier); - } - - public CompletableFuture callExecuteSupplier(Supplier supplier) { - return executeAsync(supplier); - } - - public CompletableFuture callExecuteSupplierTimeout(Supplier supplier, Duration timeout) { - return executeAsync(supplier, timeout); - } - - public CompletableFuture callExecuteRunnable(Runnable runnable) { - return executeAsync(runnable); - } - - public CompletableFuture callExecuteRunnableTimeout(Runnable runnable, Duration timeout) { - return executeAsync(runnable, timeout); - } - - public void setDaoForTest() { - this.dao = mockDao(); - } - - @SuppressWarnings("unchecked") - private Dao mockDao() { - return mock(Dao.class); - } - } - - private ExecutorService executor() { - return Executors.newSingleThreadExecutor(); - } - - @Test - void executeAsyncSupplierShouldReturnValue() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryContext ctx = new RepositoryContext(executor()); - - TestDaoRepository repo = new TestDaoRepository(logger, ctx); - - CompletableFuture future = - repo.callExecuteSupplier(() -> 42); - - assertThat(future.join()).isEqualTo(42); - } - - @Test - void executeAsyncSupplierShouldWrapException() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryContext ctx = new RepositoryContext(executor()); - - TestDaoRepository repo = new TestDaoRepository(logger, ctx); - - CompletableFuture f = - repo.callExecuteSupplier(() -> { - throw new IllegalStateException("boom"); - }); - - assertThatThrownBy(f::join) - .isInstanceOf(CompletionException.class) - .hasCauseInstanceOf(IllegalStateException.class); - - verify(logger).error(any(Throwable.class), eq("Async DAO operation failed")); - } - - @Test - void executeAsyncSupplierShouldTimeout() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryContext ctx = new RepositoryContext(executor()); - - TestDaoRepository repo = new TestDaoRepository(logger, ctx); - - CompletableFuture f = - repo.callExecuteSupplierTimeout(() -> { - try { - Thread.sleep(200); - } catch (InterruptedException ignored) {} - return 1; - }, Duration.ofMillis(50)); - - assertThatThrownBy(f::join) - .isInstanceOf(CompletionException.class) - .hasCauseInstanceOf(TimeoutException.class); - - verify(logger).warn( - eq("Async DAO operation timed out after %s ms"), - eq(50L) - ); - } - - @Test - void executeAsyncRunnableShouldRunSuccessfully() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryContext ctx = new RepositoryContext(executor()); - TestDaoRepository repo = new TestDaoRepository(logger, ctx); - - CompletableFuture f = - repo.callExecuteRunnable(() -> {}); - - assertThat(f).succeedsWithin(Duration.ofSeconds(1)); - } - - @Test - void withDaoShouldThrowIfDaoNotInitialized() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryContext ctx = new RepositoryContext(executor()); - TestDaoRepository repo = new TestDaoRepository(logger, ctx); - - assertThatThrownBy(() -> repo.callWithDao(() -> "x")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("DAO not initialized"); - } - - @Test - void withDaoShouldRunWhenDaoIsAvailable() { - PluginLogger logger = mock(PluginLogger.class); - RepositoryContext ctx = new RepositoryContext(executor()); - TestDaoRepository repo = new TestDaoRepository(logger, ctx); - - repo.setDaoForTest(); - - String result = repo.callWithDao(() -> "OK"); - - assertThat(result).isEqualTo("OK"); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureComponentsTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureComponentsTest.java deleted file mode 100644 index 5f5aafc..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureComponentsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.imdmk.playtime.shared.adventure; - -import com.github.imdmk.playtime.platform.adventure.AdventureComponents; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.TextDecoration; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -class AdventureComponentsTest { - - @Test - void text_single_shouldDeserialize() { - Component c = AdventureComponents.text("Hello"); - - assertThat(c) - .extracting(comp -> comp.decoration(TextDecoration.BOLD)) - .isNotNull(); - } - - @Test - void text_varargs_shouldDeserializeList() { - List list = AdventureComponents.text("A", "B"); - - assertThat(list).hasSize(2); - } - - @Test - void text_iterable_shouldDeserializeAll() { - List list = AdventureComponents.text(List.of("X", "Y")); - - assertThat(list).hasSize(2); - } - - @Test - void withoutItalics_component_shouldDisableItalic() { - Component c = AdventureComponents.text("Hello"); - Component result = AdventureComponents.withoutItalics(c); - - assertThat(result.decoration(TextDecoration.ITALIC)).isEqualTo(TextDecoration.State.FALSE); - } - - @Test - void withoutItalics_stringVarargs_shouldDisableItalic() { - List out = AdventureComponents.withoutItalics("A", "B"); - - assertThat(out).hasSize(2); - out.forEach(c -> - assertThat(c.decoration(TextDecoration.ITALIC)).isEqualTo(TextDecoration.State.FALSE)); - } - - @Test - void serialize_shouldReturnMiniMessage() { - Component c = Component.text("Test"); - String s = AdventureComponents.serialize(c); - - assertThat(s).isEqualTo("Test"); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureFormatterTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureFormatterTest.java deleted file mode 100644 index a332f16..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventureFormatterTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.imdmk.playtime.shared.adventure; - -import com.github.imdmk.playtime.platform.adventure.AdventureFormatter; -import com.github.imdmk.playtime.platform.adventure.AdventurePlaceholders; -import net.kyori.adventure.text.Component; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -class AdventureFormatterTest { - - @Test - void format_string_shouldApplyPlaceholder() { - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%name%", "DMK") - .build(); - - Component result = AdventureFormatter.format("Hello %name%", ph); - - assertThat(result.toString()).contains("DMK"); - } - - @Test - void format_component_shouldApplyPlaceholder() { - Component base = Component.text("XP: %xp%"); - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%xp%", "1500") - .build(); - - Component out = AdventureFormatter.format(base, ph); - - assertThat(out.toString()).contains("1500"); - } - - @Test - void format_list_shouldApplyPlaceholdersToAll() { - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%v%", "VALUE") - .build(); - - List out = AdventureFormatter.format( - List.of(Component.text("A %v%"), Component.text("B %v%")), - ph - ); - - assertThat(out).hasSize(2); - assertThat(out.get(0).toString()).contains("VALUE"); - assertThat(out.get(1).toString()).contains("VALUE"); - } - - @Test - void format_shouldHandleOverlappingKeysByLength() { - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%player%", "DOM") - .with("%player_name%", "DMK") - .build(); - - Component base = Component.text("Hello %player_name% !"); - Component out = AdventureFormatter.format(base, ph); - - assertThat(out.toString()).contains("DMK"); - assertThat(out.toString()).doesNotContain("DOM"); - } - - @Test - void format_emptyPlaceholders_shouldReturnSameComponent() { - Component c = Component.text("Test"); - Component out = AdventureFormatter.format(c, AdventurePlaceholders.empty()); - - assertThat(out).isSameAs(c); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventurePlaceholdersTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventurePlaceholdersTest.java deleted file mode 100644 index 1788086..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/adventure/AdventurePlaceholdersTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.github.imdmk.playtime.shared.adventure; - -import com.github.imdmk.playtime.platform.adventure.AdventurePlaceholders; -import net.kyori.adventure.text.Component; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class AdventurePlaceholdersTest { - - @Test - void empty_shouldReturnSingleton() { - AdventurePlaceholders p1 = AdventurePlaceholders.empty(); - AdventurePlaceholders p2 = AdventurePlaceholders.empty(); - - assertThat(p1).isSameAs(p2); - assertThat(p1.size()).isZero(); - } - - @Test - void builder_withStringAndComponent_shouldStoreMapping() { - Component value = Component.text("Hello"); - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%player%", value) - .build(); - - assertThat(ph.asMap()) - .containsEntry("%player%", value); - } - - @Test - void builder_withStringStringShouldConvertToTextComponent() { - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%x%", "Hello") - .build(); - - Component comp = ph.asMap().get("%x%"); - assertThat(comp).isNotNull(); - assertThat(comp.toString()).contains("Hello"); - } - - @Test - void builder_withObjectShouldConvertToStringComponent() { - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%n%", 123) - .build(); - - Component comp = ph.asMap().get("%n%"); - assertThat(comp.toString()).contains("123"); - } - - @Test - void builder_withOtherPlaceholdersShouldMerge() { - AdventurePlaceholders base = AdventurePlaceholders.builder() - .with("%a%", "A") - .build(); - - AdventurePlaceholders merged = AdventurePlaceholders.builder() - .with("%b%", "B") - .with(base) - .build(); - - assertThat(merged.asMap()) - .containsEntry("%a%", Component.text("A")) - .containsEntry("%b%", Component.text("B")); - } - - @Test - void asMap_shouldBeUnmodifiable() { - AdventurePlaceholders ph = AdventurePlaceholders.builder() - .with("%x%", "1") - .build(); - - Map map = ph.asMap(); - - assertThrows(UnsupportedOperationException.class, () -> map.put("%y%", Component.text("2"))); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java deleted file mode 100644 index 00c02d6..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.imdmk.playtime.shared.time; - -import org.junit.jupiter.api.Test; - -import java.time.Duration; - -import static org.assertj.core.api.Assertions.assertThat; - -class DurationFormatStyleTest { - - @Test - void compact_formatShouldUseAbbreviations() { - String out = DurationFormatStyle.COMPACT.format(Duration.ofSeconds(3665)); - assertThat(out).isEqualTo("1h 1m 5s"); - } - - @Test - void long_formatShouldUseFullNames() { - String out = DurationFormatStyle.LONG.format(Duration.ofMinutes(61)); - assertThat(out).isEqualTo("1 hour 1 minute"); - } - - @Test - void longWithAnd_formatShouldUseAndSeparator() { - String out = DurationFormatStyle.LONG_WITH_AND.format(Duration.ofSeconds(62)); - assertThat(out).isEqualTo("1 minute and 2 seconds"); - } - - @Test - void natural_formatShouldUseCommaSeparator() { - String out = DurationFormatStyle.NATURAL.format(Duration.ofHours(24 + 2)); - assertThat(out).isEqualTo("1 day, 2 hours"); - } - - @Test - void formatWith_noNonZeroUnitsShouldReturnEmpty() { - String out = DurationFormatStyle.NATURAL.format(Duration.ZERO); - assertThat(out).isEmpty(); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java deleted file mode 100644 index c9af105..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.imdmk.playtime.shared.time; - -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -class DurationSplitterTest { - - @Test - void split_shouldReturnCorrectParts() { - Duration d = Duration.ofDays(2) - .plusHours(5) - .plusMinutes(30) - .plusSeconds(10); - - Map parts = DurationSplitter.split(d); - - assertThat(parts.get(DurationUnit.DAY)).isEqualTo(2); - assertThat(parts.get(DurationUnit.HOUR)).isEqualTo(5); - assertThat(parts.get(DurationUnit.MINUTE)).isEqualTo(30); - assertThat(parts.get(DurationUnit.SECOND)).isEqualTo(10); - } - - @Test - void split_zeroDurationShouldReturnAllZero() { - Map parts = DurationSplitter.split(Duration.ZERO); - - assertThat(parts.values()).allMatch(v -> v == 0); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java deleted file mode 100644 index 4fb45ce..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.imdmk.playtime.shared.time; - -import org.junit.jupiter.api.Test; - -import java.time.Duration; - -class DurationUnitTest { - - @Test - void extract_shouldReturnCorrectValues() { - Duration d = Duration.ofDays(1) - .plusHours(2) - .plusMinutes(3) - .plusSeconds(4); - - assertThat(DurationUnit.DAY.extract(d)).isEqualTo(1); - assertThat(DurationUnit.HOUR.extract(d)).isEqualTo(2); - assertThat(DurationUnit.MINUTE.extract(d)).isEqualTo(3); - assertThat(DurationUnit.SECOND.extract(d)).isEqualTo(4); - } - - @Test - void toDisplayName_shouldUseSingularOrPluralProperly() { - assertThat(DurationUnit.HOUR.toDisplayName(1)).isEqualTo("1 hour"); - assertThat(DurationUnit.HOUR.toDisplayName(5)).isEqualTo("5 hours"); - } - - @Test - void getAbbreviation_shouldReturnCorrectStrings() { - assertThat(DurationUnit.DAY.getAbbreviation()).isEqualTo("d"); - assertThat(DurationUnit.SECOND.getAbbreviation()).isEqualTo("s"); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java deleted file mode 100644 index 0bf8823..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.imdmk.playtime.shared.time; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.Duration; - -import static org.assertj.core.api.Assertions.assertThat; - -class DurationsTest { - - @BeforeEach - void resetDefaultStyle() { - Durations.setDefaultFormatStyle(DurationFormatStyle.NATURAL); - } - - @Test - void format_zeroOrNegative_shouldReturnLessThanOneSecond() { - assertThat(Durations.format(Duration.ZERO)).isEqualTo("<1s"); - assertThat(Durations.format(Duration.ofMillis(-500))).isEqualTo("<1s"); - } - - @Test - void format_withDefaultStyleShouldUseNatural() { - String result = Durations.format(Duration.ofSeconds(65)); - assertThat(result).isEqualTo("1 minute, 5 seconds"); - } - - @Test - void format_withCustomStyleShouldFormatProperly() { - String result = Durations.format(Duration.ofSeconds(3661), DurationFormatStyle.COMPACT); - assertThat(result).isEqualTo("1h 1m 1s"); - } - - @Test - void setDefaultFormatStyle_shouldAffectGlobalFormatting() { - Durations.setDefaultFormatStyle(DurationFormatStyle.COMPACT); - String result = Durations.format(Duration.ofMinutes(3)); - - assertThat(result).isEqualTo("3m"); - } - - @Test - void clamp_negative_shouldReturnZero() { - assertThat(Durations.clamp(Duration.ofMillis(-1))) - .isEqualTo(Duration.ZERO); - } - - @Test - void clamp_aboveMax_shouldClampToMax() { - Duration tenYears = Duration.ofDays(5000); - Duration clamped = Durations.clamp(tenYears); - - assertThat(clamped).isEqualTo(Duration.ofDays(3650)); - } - - @Test - void clamp_withinRange_shouldReturnSame() { - Duration d = Duration.ofHours(12); - assertThat(Durations.clamp(d)).isEqualTo(d); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/user/cache/CaffeineUserCacheTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/user/cache/CaffeineUserCacheTest.java deleted file mode 100644 index c3b3922..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/user/cache/CaffeineUserCacheTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.github.imdmk.playtime.user.cache; - -import com.github.imdmk.playtime.user.User; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -class CaffeineUserCacheTest { - - private CaffeineUserCache cache; - - @BeforeEach - void setup() { - cache = new CaffeineUserCache( - Duration.ofHours(1), - Duration.ofHours(1) - ); - } - - private static User user(UUID uuid, String name) { - return new User(uuid, name); - } - - @Test - void cacheUser_shouldStoreUserInBothIndexes() { - var u = user(UUID.randomUUID(), "DMK"); - - cache.cacheUser(u); - - assertThat(cache.getUserByUuid(u.getUuid())) - .contains(u); - - assertThat(cache.getUserByName("DMK")) - .contains(u); - } - - @Test - void cacheUser_shouldReplaceOldNameMappingOnNameChange() { - var uuid = UUID.randomUUID(); - var oldUser = user(uuid, "DMK"); - var newUser = user(uuid, "DMKNew"); - - cache.cacheUser(oldUser); - cache.cacheUser(newUser); - - assertThat(cache.getUserByName("DMK")).isEmpty(); - assertThat(cache.getUserByName("DMKNew")).contains(newUser); - } - - @Test - void invalidateUser_shouldRemoveFromBothIndexes() { - var u = user(UUID.randomUUID(), "Player1"); - cache.cacheUser(u); - - cache.invalidateUser(u); - - assertThat(cache.getUserByUuid(u.getUuid())).isEmpty(); - assertThat(cache.getUserByName("Player1")).isEmpty(); - } - - @Test - void invalidateByUuid_shouldRemoveCorrectEntries() { - var u = user(UUID.randomUUID(), "Nick"); - - cache.cacheUser(u); - cache.invalidateByUuid(u.getUuid()); - - assertThat(cache.getUserByUuid(u.getUuid())).isEmpty(); - assertThat(cache.getUserByName("Nick")).isEmpty(); - } - - @Test - void invalidateByName_shouldRemoveCorrectEntries() { - var u = user(UUID.randomUUID(), "Tester"); - - cache.cacheUser(u); - cache.invalidateByName("Tester"); - - assertThat(cache.getUserByUuid(u.getUuid())).isEmpty(); - assertThat(cache.getUserByName("Tester")).isEmpty(); - } - - @Test - void getUserByUuid_shouldReturnEmptyWhenNotFound() { - assertThat(cache.getUserByUuid(UUID.randomUUID())).isEmpty(); - } - - @Test - void getUserByName_shouldReturnEmptyWhenNotFound() { - assertThat(cache.getUserByName("Unknown")).isEmpty(); - } - - @Test - void updateUserNameMapping_shouldUpdateNameIndexProperly() { - var uuid = UUID.randomUUID(); - var oldUser = user(uuid, "A"); - var newUser = user(uuid, "B"); - - cache.cacheUser(oldUser); - cache.updateUserNameMapping(newUser, "A"); - - assertThat(cache.getUserByName("A")).isEmpty(); - assertThat(cache.getUserByName("B")).contains(newUser); - } - - @Test - void forEachUser_shouldIterateAllUsers() { - var u1 = user(UUID.randomUUID(), "One"); - var u2 = user(UUID.randomUUID(), "Two"); - - cache.cacheUser(u1); - cache.cacheUser(u2); - - AtomicInteger counter = new AtomicInteger(); - - cache.forEachUser(u -> counter.incrementAndGet()); - - assertThat(counter.get()).isEqualTo(2); - } - - @Test - void getCache_shouldReturnUnmodifiableSnapshot() { - var u1 = user(UUID.randomUUID(), "One"); - cache.cacheUser(u1); - - var snapshot = cache.getCache(); - - assertThat(snapshot).contains(u1); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(snapshot::clear); - } - - @Test - void invalidateAll_shouldClearEverything() { - var u1 = user(UUID.randomUUID(), "A"); - var u2 = user(UUID.randomUUID(), "B"); - - cache.cacheUser(u1); - cache.cacheUser(u2); - - cache.invalidateAll(); - - assertThat(cache.getCache()).isEmpty(); - assertThat(cache.getUserByName("A")).isEmpty(); - assertThat(cache.getUserByName("B")).isEmpty(); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java deleted file mode 100644 index d3c701b..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimePlayTimeUserEntityMapperTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.imdmk.playtime.user.repository; - -import com.github.imdmk.playtime.PlayTime; -import com.github.imdmk.playtime.feature.playtime.PlayTimeUserEntity; -import com.github.imdmk.playtime.feature.playtime.repository.UserEntityMapper; -import com.github.imdmk.playtime.user.User; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class PlayTimePlayTimeUserEntityMapperTest { - - private final UserEntityMapper mapper = new UserEntityMapper(); - - @Test - void toEntityShouldMapFieldsCorrectly() { - var uuid = UUID.randomUUID(); - var user = new User(uuid, "DMK", PlayTime.ofMillis(3000)); - - var entity = mapper.toEntity(user); - - assertThat(entity.getUuid()).isEqualTo(uuid); - assertThat(entity.getName()).isEqualTo("DMK"); - assertThat(entity.getPlayTimeMillis()).isEqualTo(3000); - } - - @Test - void toDomainShouldMapFieldsCorrectly() { - var uuid = UUID.randomUUID(); - var entity = new PlayTimeUserEntity(uuid, "XYZ", 5000); - - var user = mapper.toDomain(entity); - - assertThat(user.getUuid()).isEqualTo(uuid); - assertThat(user.getName()).isEqualTo("XYZ"); - assertThat(user.getPlayTime().millis()).isEqualTo(5000); - } - - @Test - void toEntityShouldRejectNull() { - assertThatThrownBy(() -> mapper.toEntity(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - void toDomainShouldRejectNull() { - assertThatThrownBy(() -> mapper.toDomain(null)) - .isInstanceOf(NullPointerException.class); - } -} - diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimeUserEntityTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimeUserEntityTest.java deleted file mode 100644 index 343d612..0000000 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/user/repository/PlayTimeUserEntityTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.imdmk.playtime.user.repository; - -import com.github.imdmk.playtime.feature.playtime.PlayTimeUserEntity; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -class PlayTimeUserEntityTest { - - @Test - void constructorShouldSetFields() { - var uuid = UUID.randomUUID(); - var entity = new PlayTimeUserEntity(uuid, "DMK", 12345); - - assertThat(entity.getUuid()).isEqualTo(uuid); - assertThat(entity.getName()).isEqualTo("DMK"); - assertThat(entity.getPlayTimeMillis()).isEqualTo(12345); - } - - @Test - void settersShouldUpdateFields() { - var entity = new PlayTimeUserEntity(); - - var uuid = UUID.randomUUID(); - entity.setUuid(uuid); - entity.setName("DMK"); - entity.setPlayTimeMillis(500); - - assertThat(entity.getUuid()).isEqualTo(uuid); - assertThat(entity.getName()).isEqualTo("DMK"); - assertThat(entity.getPlayTimeMillis()).isEqualTo(500); - } - - @Test - void equalsShouldCompareByUuidOnly() { - var uuid = UUID.randomUUID(); - - var a = new PlayTimeUserEntity(uuid, "A", 1); - var b = new PlayTimeUserEntity(uuid, "B", 9999); - - assertThat(a).isEqualTo(b); - assertThat(a.hashCode()).isEqualTo(b.hashCode()); - } - - @Test - void equalsShouldReturnFalseForDifferentUuid() { - var a = new PlayTimeUserEntity(UUID.randomUUID(), "A", 1); - var b = new PlayTimeUserEntity(UUID.randomUUID(), "A", 1); - - assertThat(a).isNotEqualTo(b); - } - - @Test - void toStringShouldContainFields() { - var uuid = UUID.randomUUID(); - var e = new PlayTimeUserEntity(uuid, "X", 123); - - var s = e.toString(); - - assertThat(s).contains("uuid=" + uuid); - assertThat(s).contains("name='X'"); - assertThat(s).contains("spentMillis=123"); - } -} - From 9e9b220ae8fdcb003e557f498fd1b1df1837a843 Mon Sep 17 00:00:00 2001 From: imDMK Date: Tue, 13 Jan 2026 18:19:40 +0100 Subject: [PATCH 14/17] Fix ComponentManager. --- .../playtime/injector/ComponentManager.java | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java index 63bd7fb..3bbe576 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -9,7 +9,10 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; public final class ComponentManager { @@ -17,10 +20,13 @@ public final class ComponentManager { private final ComponentScanner scanner; private final ComponentSorter sorter; - private final List> processors = new ArrayList<>(); + // annotation -> processor container + private final Map, ProcessorContainer> processors = new HashMap<>(); private final List postProcessors = new ArrayList<>(); private final List> components = new ArrayList<>(); + private boolean scanned = false; + public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) { this.injector = injector; this.scanner = new ComponentScanner(basePackage); @@ -28,11 +34,22 @@ public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) } public ComponentManager addProcessor(@NotNull ProcessorContainer container) { - processors.add(container); + if (scanned) { + throw new IllegalStateException("Cannot add processors after scanAll()"); + } + + final Class type = container.annotationType(); + if (processors.containsKey(type)) { + throw new IllegalStateException( + "Processor already registered for annotation: " + type.getName() + ); + } + + processors.put(type, container); return this; } - public ComponentManager addProcessors(@NotNull List> containers) { + public ComponentManager addProcessors(@NotNull Collection> containers) { containers.forEach(this::addProcessor); return this; } @@ -42,18 +59,23 @@ public ComponentManager addPostProcessor(@NotNull ComponentPostProcessor postPro return this; } - public ComponentManager addPostProcessors(@NotNull List postProcessors) { - postProcessors.forEach(this::addPostProcessor); - return this; - } - public void scanAll() { - for (final ProcessorContainer container : processors) { + if (scanned) { + throw new IllegalStateException("scanAll() already called"); + } + + for (final ProcessorContainer container : processors.values()) { components.addAll(scanner.scan(container.annotationType())); } + + scanned = true; } public void processAll() { + if (!scanned) { + throw new IllegalStateException("scanAll() must be called before processAll()"); + } + final ComponentProcessorContext context = new ComponentProcessorContext(injector); sorter.sort(components); @@ -70,12 +92,10 @@ private
void processComponent( ) { component.createInstance(injector); - for (final ProcessorContainer container : processors) { - if (container.annotationType() != component.annotation().annotationType()) { - continue; - } - - final ComponentProcessor processor = (ComponentProcessor) container.processor(); + final ProcessorContainer raw = processors.get(component.annotation().annotationType()); + if (raw != null) { + final ProcessorContainer container = (ProcessorContainer) raw; + final ComponentProcessor processor = container.processor(); processor.process( component.instance(), @@ -89,5 +109,3 @@ private void processComponent( } } } - - From 31f0d9f07db50f0b15b1e3c68f08cb181480574d Mon Sep 17 00:00:00 2001 From: imDMK Date: Tue, 13 Jan 2026 18:20:43 +0100 Subject: [PATCH 15/17] Add NotNull annotation. --- .../main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java index e93a32d..66478e8 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java @@ -10,6 +10,7 @@ private PlayTimeApiProvider() { throw new UnsupportedOperationException("This class cannot be instantiated."); } + @NotNull public static PlayTimeApi get() { final PlayTimeApi api = API; if (api == null) { From 82061312b7ac179a025f16a4daf0889bdc273fca Mon Sep 17 00:00:00 2001 From: imDMK Date: Tue, 13 Jan 2026 18:23:05 +0100 Subject: [PATCH 16/17] Improve AdventureFormatter. --- .../adventure/AdventureFormatter.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java index 024a8d5..3f4c0f2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java @@ -1,10 +1,13 @@ package com.github.imdmk.playtime.platform.adventure; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.minimessage.MiniMessage; import org.jetbrains.annotations.NotNull; +import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public final class AdventureFormatter { @@ -28,14 +31,25 @@ public static Component format(@NotNull Component input, @NotNull AdventurePlace return input; } - final MiniMessage miniMessage = AdventureComponents.miniMessage(); + // Sort keys by descending length to avoid substring overlap + var ordered = placeholders.asMap().entrySet().stream() + .sorted(Comparator.>comparingInt(e -> e.getKey().length()).reversed()) + .toList(); - String raw = miniMessage.serialize(input); - for (final var entry : placeholders.asMap().entrySet()) { - raw = raw.replace(entry.getKey(), miniMessage.serialize(entry.getValue())); + Component out = input; + for (final var entry : ordered) { + final String key = entry.getKey(); + final Component replacement = entry.getValue(); + + final TextReplacementConfig config = TextReplacementConfig.builder() + .matchLiteral(key) + .replacement(replacement) + .build(); + + out = out.replaceText(config); } - return miniMessage.deserialize(raw); + return out; } } From 29475c628d57667fa6db154d3acb55ca8bba32d2 Mon Sep 17 00:00:00 2001 From: imDMK Date: Tue, 13 Jan 2026 18:23:21 +0100 Subject: [PATCH 17/17] Add final --- .../imdmk/playtime/platform/adventure/AdventureFormatter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java index 3f4c0f2..e62ac1c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java @@ -32,7 +32,7 @@ public static Component format(@NotNull Component input, @NotNull AdventurePlace } // Sort keys by descending length to avoid substring overlap - var ordered = placeholders.asMap().entrySet().stream() + final var ordered = placeholders.asMap().entrySet().stream() .sorted(Comparator.>comparingInt(e -> e.getKey().length()).reversed()) .toList();