From b365dcb2b2afd8bb4a7ff5832492480722371184 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sat, 6 Dec 2025 16:00:55 +0100 Subject: [PATCH 01/12] refactor: complete system overhaul (architecture, performance, messaging, scheduling) --- automessage-core/build.gradle.kts | 23 ++ .../imdmk/automessage/AutoMessagePlugin.java | 118 +++++++ .../imdmk/automessage/PluginSettings.java | 11 + .../command/dispatcher/DisableCommand.java | 33 ++ .../command/dispatcher/EnableCommand.java | 33 ++ .../messages/DispatcherMessages.java | 11 + .../messages/DispatcherMessagesImpl.java | 28 ++ .../command/reload/ReloadCommand.java | 44 +++ .../reload/messages/ReloadMessages.java | 11 + .../reload/messages/ReloadMessagesImpl.java | 29 ++ .../config/ConfigLoadException.java | 41 +++ .../automessage/config/ConfigManager.java | 136 ++++++++ .../automessage/config/ConfigRepresenter.java | 6 +- .../automessage/config/ConfigSection.java | 37 ++ .../handler/InvalidUsageHandlerImpl.java | 48 +++ .../MissingPermissionsHandlerImpl.java | 17 +- .../handler/NoticeResultHandlerImpl.java | 24 ++ .../messages/LiteCommandsMessages.java | 15 + .../messages/LiteCommandsMessagesImpl.java | 75 ++++ .../platform/logger/BukkitPluginLogger.java | 114 +++++++ .../platform/logger/PluginLogger.java | 117 +++++++ .../scheduler/BukkitTaskScheduler.java | 148 ++++++++ .../platform/scheduler/PluginTask.java | 62 ++++ .../platform/scheduler/TaskScheduler.java | 152 +++++++++ .../scheduled/ScheduledMessage.java | 28 ++ .../scheduled/ScheduledMessageBuilder.java | 65 ++++ .../scheduled/ScheduledMessageConfig.java | 106 ++++++ .../scheduled/ScheduledMessageSerializer.java | 39 +++ .../audience/filter/AudienceFilter.java | 14 + .../audience/filter/AudienceFilterImpl.java | 19 ++ .../audience/rule/AudienceGroupRule.java | 26 ++ .../audience/rule/AudiencePermissionRule.java | 22 ++ .../scheduled/audience/rule/AudienceRule.java | 27 ++ .../audience/rule/AudienceRuleSerializer.java | 35 ++ .../dispatcher/DispatchFilteredTarget.java | 26 ++ .../dispatcher/DispatchPlayersTarget.java | 21 ++ .../scheduled/dispatcher/DispatchTarget.java | 28 ++ .../dispatcher/MessageDispatcher.java | 80 +++++ .../dispatcher/MessageDispatcherConfig.java | 41 +++ .../dispatcher/MessageDispatcherTask.java | 50 +++ .../scheduled/selector/MessageSelector.java | 16 + .../selector/MessageSelectorFactory.java | 18 + .../selector/MessageSelectorType.java | 6 + .../selector/RandomMessageSelector.java | 21 ++ .../selector/SequentialMessageSelector.java | 43 +++ .../AdventureComponentSerializer.java | 26 ++ .../shared/adventure/AdventureComponents.java | 182 ++++++++++ .../shared/adventure/AdventureFormatter.java | 84 +++++ .../adventure/AdventurePlaceholders.java | 158 +++++++++ .../shared/message/MessageConfig.java | 49 +++ .../shared/message/MessageService.java | 86 +++++ .../shared/validate/Validator.java | 65 ++++ .../scheduled/ScheduledMessageTest.java | 107 ++++++ .../selector/MessageSelectorFactoryTest.java | 38 +++ .../selector/RandomMessageSelectorTest.java | 56 +++ .../SequentialMessageSelectorTest.java | 78 +++++ .../adventure/AdventureComponentsTest.java | 61 ++++ .../adventure/AdventureFormatterTest.java | 73 ++++ .../adventure/AdventurePlaceholdersTest.java | 81 +++++ automessage-plugin/build.gradle.kts | 54 +++ .../automessage/AutoMessagePluginLoader.java | 25 ++ .../automessage/DefaultPluginSettings.java | 20 ++ build.gradle | 95 ------ build.gradle.kts | 40 +++ checkstyle.xml | 206 ----------- gradle/wrapper/gradle-wrapper.properties | 15 +- gradlew | 323 +++++------------- gradlew.bat | 2 +- settings.gradle | 1 - settings.gradle.kts | 3 + .../github/imdmk/automessage/AutoMessage.java | 138 -------- .../imdmk/automessage/AutoMessagePlugin.java | 21 -- .../configuration/ConfigSection.java | 12 - .../ConfigurationLoadException.java | 16 - .../configuration/ConfigurationManager.java | 107 ------ .../configuration/PluginConfig.java | 29 -- .../builder/configuration/Command.java | 81 ----- .../builder/configuration/CommandConfig.java | 68 ---- .../builder/configuration/CommandEditor.java | 63 ---- .../builder/configuration/SubCommand.java | 42 --- .../command/builder/handler/UsageHandler.java | 49 --- .../builder/player/PlayerArgument.java | 41 --- .../builder/player/PlayerContextual.java | 20 -- .../command/implementation/DelayCommand.java | 51 --- .../implementation/DisableCommand.java | 38 --- .../implementation/DispatchCommand.java | 77 ----- .../command/implementation/EnableCommand.java | 38 --- .../command/implementation/ReloadCommand.java | 44 --- .../feature/message/MessageConfig.java | 93 ----- .../feature/message/MessageResultHandler.java | 27 -- .../feature/message/MessageService.java | 70 ---- .../message/auto/AutoMessageNotice.java | 174 ---------- .../auto/AutoMessageNoticeArgument.java | 38 --- .../message/auto/AutoMessageNoticeConfig.java | 114 ------- .../auto/AutoMessageNoticeSerializer.java | 56 --- .../dispatcher/AutoMessageDispatchTask.java | 28 -- .../dispatcher/AutoMessageDispatcher.java | 245 ------------- .../AutoMessageEligibilityEvaluator.java | 21 -- .../DefaultEligibilityEvaluator.java | 24 -- .../auto/selector/AutoMessageSelector.java | 28 -- .../selector/AutoMessageSelectorFactory.java | 31 -- .../selector/AutoMessageSelectorMode.java | 22 -- .../selector/RandomAutoMessageSelector.java | 38 --- .../SequentialAutoMessageSelector.java | 48 --- .../message/auto/sound/AutoMessageSound.java | 26 -- .../sound/AutoMessageSoundSerializer.java | 32 -- .../message/auto/sound/SoundSerializer.java | 26 -- .../feature/update/UpdateController.java | 95 ------ .../feature/update/UpdateService.java | 44 --- .../scheduler/BukkitTaskScheduler.java | 54 --- .../automessage/scheduler/TaskScheduler.java | 77 ----- .../imdmk/automessage/util/DurationUtil.java | 77 ----- 112 files changed, 3440 insertions(+), 2975 deletions(-) create mode 100644 automessage-core/build.gradle.kts create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/PluginSettings.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessages.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java rename src/main/java/com/github/imdmk/automessage/configuration/CustomRepresenter.java => automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java (93%) create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigSection.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/InvalidUsageHandlerImpl.java rename src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/MissingPermissionHandler.java => automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/MissingPermissionsHandlerImpl.java (54%) create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/NoticeResultHandlerImpl.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessages.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/PluginLogger.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/BukkitTaskScheduler.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/PluginTask.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/TaskScheduler.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessage.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageBuilder.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageSerializer.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilterImpl.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceGroupRule.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudiencePermissionRule.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRuleSerializer.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelector.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactory.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorType.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelector.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelector.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentSerializer.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponents.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatter.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholders.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageService.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/shared/validate/Validator.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/ScheduledMessageTest.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactoryTest.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelectorTest.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelectorTest.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentsTest.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatterTest.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholdersTest.java create mode 100644 automessage-plugin/build.gradle.kts create mode 100644 automessage-plugin/src/main/java/com/github/imdmk/automessage/AutoMessagePluginLoader.java create mode 100644 automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 checkstyle.xml delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts delete mode 100644 src/main/java/com/github/imdmk/automessage/AutoMessage.java delete mode 100644 src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java delete mode 100644 src/main/java/com/github/imdmk/automessage/configuration/ConfigSection.java delete mode 100644 src/main/java/com/github/imdmk/automessage/configuration/ConfigurationLoadException.java delete mode 100644 src/main/java/com/github/imdmk/automessage/configuration/ConfigurationManager.java delete mode 100644 src/main/java/com/github/imdmk/automessage/configuration/PluginConfig.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/Command.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandConfig.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandEditor.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/SubCommand.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/UsageHandler.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerArgument.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerContextual.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/implementation/DelayCommand.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/implementation/DisableCommand.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/implementation/DispatchCommand.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/implementation/EnableCommand.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/command/implementation/ReloadCommand.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/MessageConfig.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/MessageResultHandler.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/MessageService.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNotice.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeArgument.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeConfig.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeSerializer.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatchTask.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatcher.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/AutoMessageEligibilityEvaluator.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/DefaultEligibilityEvaluator.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelector.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorFactory.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorMode.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/RandomAutoMessageSelector.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/SequentialAutoMessageSelector.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSound.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSoundSerializer.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/SoundSerializer.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/update/UpdateController.java delete mode 100644 src/main/java/com/github/imdmk/automessage/feature/update/UpdateService.java delete mode 100644 src/main/java/com/github/imdmk/automessage/scheduler/BukkitTaskScheduler.java delete mode 100644 src/main/java/com/github/imdmk/automessage/scheduler/TaskScheduler.java delete mode 100644 src/main/java/com/github/imdmk/automessage/util/DurationUtil.java diff --git a/automessage-core/build.gradle.kts b/automessage-core/build.gradle.kts new file mode 100644 index 0000000..e542317 --- /dev/null +++ b/automessage-core/build.gradle.kts @@ -0,0 +1,23 @@ +dependencies { + compileOnlyApi("org.spigotmc:spigot-api:1.21.10-R0.1-SNAPSHOT") + + implementation("net.kyori:adventure-platform-bukkit:4.4.1") + implementation("net.kyori:adventure-text-minimessage:4.21.0") + + implementation("com.eternalcode:multification-bukkit:1.2.2") + implementation("com.eternalcode:multification-okaeri:1.2.2") + + api("eu.okaeri:okaeri-configs-yaml-snakeyaml:5.0.9") + implementation("eu.okaeri:okaeri-configs-serdes-commons:5.0.5") + + implementation("org.bstats:bstats-bukkit:3.1.0") + implementation("dev.rollczi:litecommands-bukkit:3.10.6") + implementation("dev.rollczi:litecommands-annotations:3.10.6") + + testImplementation("org.junit.jupiter:junit-jupiter:6.0.1") + testImplementation("org.assertj:assertj-core:3.25.2") + testImplementation("org.mockito:mockito-core:5.8.0") + testImplementation("org.mockito:mockito-junit-jupiter:5.8.0") + testImplementation("org.mockito:mockito-inline:5.2.0") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} \ No newline at end of file diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java new file mode 100644 index 0000000..c7338c5 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java @@ -0,0 +1,118 @@ +package com.github.imdmk.automessage; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.command.dispatcher.DisableCommand; +import com.github.imdmk.automessage.command.dispatcher.EnableCommand; +import com.github.imdmk.automessage.command.reload.ReloadCommand; +import com.github.imdmk.automessage.config.ConfigManager; +import com.github.imdmk.automessage.platform.litecommands.handler.InvalidUsageHandlerImpl; +import com.github.imdmk.automessage.platform.litecommands.handler.MissingPermissionsHandlerImpl; +import com.github.imdmk.automessage.platform.litecommands.handler.NoticeResultHandlerImpl; +import com.github.imdmk.automessage.platform.logger.BukkitPluginLogger; +import com.github.imdmk.automessage.platform.logger.PluginLogger; +import com.github.imdmk.automessage.platform.scheduler.BukkitTaskScheduler; +import com.github.imdmk.automessage.platform.scheduler.TaskScheduler; +import com.github.imdmk.automessage.scheduled.ScheduledMessageConfig; +import com.github.imdmk.automessage.scheduled.audience.filter.AudienceFilter; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcher; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherTask; +import com.github.imdmk.automessage.scheduled.selector.MessageSelector; +import com.github.imdmk.automessage.scheduled.selector.MessageSelectorFactory; +import com.github.imdmk.automessage.shared.message.MessageConfig; +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; +import dev.rollczi.litecommands.LiteCommands; +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.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +final class AutoMessagePlugin { + + private static final String PLUGIN_PREFIX = "AutoMessage"; + private static final int PLUGIN_METRICS_ID = 19487; + + private final Plugin plugin; + private final Server server; + private final PluginLogger logger; + + private ConfigManager configManager; + + private BukkitAudiences bukkitAudiences; + private MessageService messageService; + private TaskScheduler taskScheduler; + + private LiteCommands liteCommands; + private Metrics metrics; + + public AutoMessagePlugin( + @NotNull final Plugin plugin, + @NotNull final Server server, + @NotNull final PluginLogger logger + ) { + this.plugin = Validator.notNull(plugin, "plugin"); + this.server = Validator.notNull(server, "server"); + this.logger = Validator.notNull(logger, "logger"); + } + + AutoMessagePlugin(@NotNull final Plugin plugin) { + this(plugin, plugin.getServer(), new BukkitPluginLogger(plugin)); + } + + void enable(@NotNull PluginSettings settings) { + Validator.notNull(settings, "settings"); + + configManager = new ConfigManager(logger, plugin.getDataFolder()); + configManager.createAll(settings.configs()); + + bukkitAudiences = BukkitAudiences.create(plugin); + messageService = new MessageService(configManager.require(MessageConfig.class), bukkitAudiences); + taskScheduler = new BukkitTaskScheduler(plugin, server.getScheduler()); + + final MessageDispatcherConfig messageDispatcherConfig = configManager.require(MessageDispatcherConfig.class); + final ScheduledMessageConfig scheduledMessageConfig = configManager.require(ScheduledMessageConfig.class); + + final AudienceFilter audienceFilter = AudienceFilter.createDefault(); + final MessageSelector messageSelector = MessageSelectorFactory.create(messageDispatcherConfig.selector); + + final MessageDispatcher messageDispatcher = new MessageDispatcher( + messageService, + messageSelector, + audienceFilter, + () -> scheduledMessageConfig.messages + ); + + MessageDispatcherTask messageDispatcherTask = new MessageDispatcherTask(server, messageDispatcherConfig, messageDispatcher); + taskScheduler.runTimerAsync(messageDispatcherTask); + + liteCommands = LiteBukkitFactory.builder(PLUGIN_PREFIX, plugin, server) + .invalidUsage(new InvalidUsageHandlerImpl(messageService)) + .missingPermission(new MissingPermissionsHandlerImpl(messageService)) + .result(Notice.class, new NoticeResultHandlerImpl(messageService)) + + .commands( + new DisableCommand(messageDispatcherConfig, messageService), + new EnableCommand(messageDispatcherConfig, messageService), + new ReloadCommand(logger, configManager, messageService) + ) + + .build(); + + metrics = new Metrics(plugin, PLUGIN_METRICS_ID); + + logger.info("%s plugin enabled.", PLUGIN_PREFIX); + } + + void disable() { + Validator.ifNotNull(configManager, ConfigManager::saveAll); + Validator.ifNotNull(bukkitAudiences, BukkitAudiences::close); + Validator.ifNotNull(taskScheduler, TaskScheduler::shutdown); + Validator.ifNotNull(liteCommands, LiteCommands::unregister); + Validator.ifNotNull(metrics, Metrics::shutdown); + + logger.info("%s plugin disabled successfully.", PLUGIN_PREFIX); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/PluginSettings.java b/automessage-core/src/main/java/com/github/imdmk/automessage/PluginSettings.java new file mode 100644 index 0000000..16c8374 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/PluginSettings.java @@ -0,0 +1,11 @@ +package com.github.imdmk.automessage; + +import com.github.imdmk.automessage.config.ConfigSection; + +import java.util.List; + +public interface PluginSettings { + + List> configs(); + +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java new file mode 100644 index 0000000..69431cc --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java @@ -0,0 +1,33 @@ +package com.github.imdmk.automessage.command.dispatcher; + +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; +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; + +@Command(name = "automessage disable") +@Permission("command.automessage.disable") +public final class DisableCommand { + + private final MessageDispatcherConfig dispatcherConfig; + private final MessageService messageService; + + public DisableCommand( + @NotNull MessageDispatcherConfig dispatcherConfig, + @NotNull MessageService messageService + ) { + this.dispatcherConfig = Validator.notNull(dispatcherConfig, "dispatcherConfig"); + this.messageService = Validator.notNull(messageService, "messageService"); + } + + @Execute + void disable(@Context CommandSender sender) { + dispatcherConfig.setEnabled(false); + messageService.send(sender, n -> n.dispatcherMessages.dispatcherDisabled()); + } +} \ No newline at end of file diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java new file mode 100644 index 0000000..aebcf55 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java @@ -0,0 +1,33 @@ +package com.github.imdmk.automessage.command.dispatcher; + +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; +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; + +@Command(name = "automessage enable") +@Permission("command.automessage.enable") +public final class EnableCommand { + + private final MessageDispatcherConfig dispatcherConfig; + private final MessageService messageService; + + public EnableCommand( + @NotNull MessageDispatcherConfig dispatcherConfig, + @NotNull MessageService messageService + ) { + this.dispatcherConfig = Validator.notNull(dispatcherConfig, "dispatcherConfig"); + this.messageService = Validator.notNull(messageService, "messageService"); + } + + @Execute + void enable(@Context CommandSender sender) { + dispatcherConfig.setEnabled(true); + messageService.send(sender, n -> n.dispatcherMessages.dispatcherEnabled()); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java new file mode 100644 index 0000000..8e5be29 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java @@ -0,0 +1,11 @@ +package com.github.imdmk.automessage.command.dispatcher.messages; + +import com.eternalcode.multification.notice.Notice; + +public interface DispatcherMessages { + + Notice dispatcherEnabled(); + + Notice dispatcherDisabled(); + +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java new file mode 100644 index 0000000..39e35ef --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java @@ -0,0 +1,28 @@ +package com.github.imdmk.automessage.command.dispatcher.messages; + +import com.eternalcode.multification.notice.Notice; +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; + +public final class DispatcherMessagesImpl extends OkaeriConfig implements DispatcherMessages { + + @Comment({"#", "# Message shown when automatic message dispatching gets enabled.", "#"}) + Notice dispatcherEnabled = Notice.chat( + "Automatic messages have been enabled." + ); + + @Comment({"#", "# Message shown when automatic message dispatching gets disabled.", "#"}) + Notice dispatcherDisabled = Notice.chat( + "Automatic messages have been disabled." + ); + + @Override + public Notice dispatcherEnabled() { + return dispatcherEnabled; + } + + @Override + public Notice dispatcherDisabled() { + return dispatcherDisabled; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java new file mode 100644 index 0000000..7adbfa3 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java @@ -0,0 +1,44 @@ +package com.github.imdmk.automessage.command.reload; + +import com.github.imdmk.automessage.config.ConfigManager; +import com.github.imdmk.automessage.platform.logger.PluginLogger; +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; +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 java.util.concurrent.CompletableFuture; + +@Command(name = "automessage reload") +@Permission("command.automessage.reload") +public final class ReloadCommand { + + private final PluginLogger logger; + private final ConfigManager configManager; + private final MessageService messageService; + + public ReloadCommand( + @NotNull PluginLogger logger, + @NotNull ConfigManager configManager, + @NotNull MessageService messageService + ) { + this.logger = Validator.notNull(logger, "logger"); + this.configManager = Validator.notNull(configManager, "configManager"); + this.messageService = Validator.notNull(messageService, "messageService"); + } + + @Execute + void reload(@Context CommandSender sender) { + CompletableFuture.runAsync(configManager::loadAll) + .thenAccept(v -> messageService.send(sender, n -> n.reloadMessages.configReloadedSuccess())) + .exceptionally(e -> { + logger.error(e, "Failed to reload plugin config"); + messageService.send(sender, n -> n.reloadMessages.configReloadFailed()); + return null; + }); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessages.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessages.java new file mode 100644 index 0000000..9883372 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessages.java @@ -0,0 +1,11 @@ +package com.github.imdmk.automessage.command.reload.messages; + +import com.eternalcode.multification.notice.Notice; + +public interface ReloadMessages { + + Notice configReloadedSuccess(); + + Notice configReloadFailed(); + +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java new file mode 100644 index 0000000..68df57a --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java @@ -0,0 +1,29 @@ +package com.github.imdmk.automessage.command.reload.messages; + +import com.eternalcode.multification.notice.Notice; +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; + +public final class ReloadMessagesImpl extends OkaeriConfig implements ReloadMessages { + + @Comment({"#", "# Message shown when all configuration files are successfully reloaded.", "#"}) + Notice configReloadedSuccess = Notice.chat( + "AutoMessage configuration has been reloaded successfully." + ); + + @Comment({"#", "# Message shown when reloading the configuration fails due to invalid or corrupted files.", "#"}) + Notice configReloadFailed = Notice.chat( + "Failed to reload AutoMessage configuration files. " + + "Please disable the plugin and verify your configuration." + ); + + @Override + public Notice configReloadedSuccess() { + return configReloadedSuccess; + } + + @Override + public Notice configReloadFailed() { + return configReloadFailed; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java new file mode 100644 index 0000000..e577075 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java @@ -0,0 +1,41 @@ +package com.github.imdmk.automessage.config; + +import org.jetbrains.annotations.NotNull; + +/** + * Thrown to indicate that a configuration file failed to load or save. + *

+ * Wraps exceptions raised by the OkaeriConfig framework or underlying I/O operations. + * Used to propagate configuration-related errors as unchecked exceptions. + *

+ */ +public final class ConfigLoadException extends RuntimeException { + + /** + * Creates a new {@link ConfigLoadException} with a default message and cause. + * + * @param cause the underlying exception that caused this failure + */ + public ConfigLoadException(@NotNull Throwable cause) { + super("Failed to load configuration", cause); + } + + /** + * Creates a new {@link ConfigLoadException} with a custom message. + * + * @param message the detail message describing the failure + */ + public ConfigLoadException(@NotNull String message) { + super(message); + } + + /** + * Creates a new {@link ConfigLoadException} with a custom message and cause. + * + * @param message the detail message describing the failure + * @param cause the underlying exception that caused this failure + */ + public ConfigLoadException(@NotNull String message, @NotNull Throwable cause) { + super(message, cause); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java new file mode 100644 index 0000000..ef36ef8 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java @@ -0,0 +1,136 @@ +package com.github.imdmk.automessage.config; + +import com.github.imdmk.automessage.platform.logger.PluginLogger; +import com.github.imdmk.automessage.shared.validate.Validator; +import eu.okaeri.configs.exception.OkaeriException; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import eu.okaeri.configs.serdes.commons.SerdesCommons; +import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; +import org.yaml.snakeyaml.resolver.Resolver; + +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 { + + private final Set configs = ConcurrentHashMap.newKeySet(); + private final Map, ConfigSection> byType = new ConcurrentHashMap<>(); + + private final PluginLogger logger; + private final File dataFolder; + + public ConfigManager(@NotNull PluginLogger logger, @NotNull File dataFolder) { + this.logger = Validator.notNull(logger, "logger"); + this.dataFolder = Validator.notNull(dataFolder, "dataFolder"); + } + + public @NotNull T create(@NotNull Class configClass) { + final T config = eu.okaeri.configs.ConfigManager.create(configClass); + + final String fileName = config.getFileName(); + if (fileName.isBlank()) { + throw new IllegalStateException( + "Missing config file name for " + configClass.getName() + + " – override getFileName() to return a non-empty path, e.g. 'config.yml'." + ); + } + + final OkaeriSerdesPack serdes = Validator.notNull(config.getSerdesPack(), "config serdes pack"); + + final File file = new File(dataFolder, fileName); + final YamlSnakeYamlConfigurer configurer = createYamlSnakeYamlConfigurer(); + + config.withConfigurer(configurer, serdes); + config.withSerdesPack(new SerdesCommons()); + config.withBindFile(file); + config.withRemoveOrphans(true); + config.saveDefaults(); + config.load(true); + + configs.add(config); + byType.put(configClass, config); + return config; + } + + public void createAll(@NotNull List> configClasses) { + configClasses.forEach(this::create); + } + + @SuppressWarnings("unchecked") + public T get(@NotNull Class type) { + return (T) byType.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()); + } + return config; + } + + private @NotNull YamlSnakeYamlConfigurer createYamlSnakeYamlConfigurer() { + final LoaderOptions loader = new LoaderOptions(); + loader.setAllowRecursiveKeys(false); + loader.setMaxAliasesForCollections(50); + + final Constructor constructor = new Constructor(loader); + + final DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setIndent(2); + options.setSplitLines(false); + + final Representer representer = new ConfigRepresenter(options); + final Resolver resolver = new Resolver(); + + final Yaml yaml = new Yaml(constructor, representer, options, loader, resolver); + return new YamlSnakeYamlConfigurer(yaml); + } + + public void loadAll() { + configs.forEach(this::load); + } + + private 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 ConfigLoadException(e); + } + } + + public void saveAll() { + configs.forEach(this::save); + } + + private void save(@NotNull ConfigSection config) { + try { + config.save(); + } catch (OkaeriException e) { + logger.error(e, "Failed to save config: %s", config.getClass().getSimpleName()); + throw new ConfigLoadException(e); + } + } + + public @NotNull @Unmodifiable Set getConfigs() { + return Collections.unmodifiableSet(configs); + } + + public void clearAll() { + configs.clear(); + byType.clear(); + } +} diff --git a/src/main/java/com/github/imdmk/automessage/configuration/CustomRepresenter.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java similarity index 93% rename from src/main/java/com/github/imdmk/automessage/configuration/CustomRepresenter.java rename to automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java index b62e266..6f12146 100644 --- a/src/main/java/com/github/imdmk/automessage/configuration/CustomRepresenter.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java @@ -1,4 +1,4 @@ -package com.github.imdmk.automessage.configuration; +package com.github.imdmk.automessage.config; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.nodes.Node; @@ -11,9 +11,9 @@ import java.util.LinkedHashMap; import java.util.Map; -public class CustomRepresenter extends Representer { +public final class ConfigRepresenter extends Representer { - public CustomRepresenter(DumperOptions options) { + public ConfigRepresenter(DumperOptions options) { super(options); this.representers.put(String.class, new RepresentString()); this.representers.put(Boolean.class, new RepresentBoolean()); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigSection.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigSection.java new file mode 100644 index 0000000..fc2303f --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigSection.java @@ -0,0 +1,37 @@ +package com.github.imdmk.automessage.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/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/InvalidUsageHandlerImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/InvalidUsageHandlerImpl.java new file mode 100644 index 0000000..b08ffac --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/InvalidUsageHandlerImpl.java @@ -0,0 +1,48 @@ +package com.github.imdmk.automessage.platform.litecommands.handler; + +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; +import dev.rollczi.litecommands.handler.result.ResultHandlerChain; +import dev.rollczi.litecommands.invalidusage.InvalidUsage; +import dev.rollczi.litecommands.invalidusage.InvalidUsageHandler; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.schematic.Schematic; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public final class InvalidUsageHandlerImpl implements InvalidUsageHandler { + + private final MessageService messageService; + + public InvalidUsageHandlerImpl(@NotNull MessageService messageService) { + this.messageService = Validator.notNull(messageService, "messageService cannot be null"); + } + + @Override + public void handle(Invocation invocation, InvalidUsage result, ResultHandlerChain chain) { + final CommandSender sender = invocation.sender(); + final Schematic schematic = result.getSchematic(); + + if (schematic.isOnlyFirst()) { + messageService.create() + .viewer(sender) + .notice(notice -> notice.liteCommandsMessages.commandUsageInvalid()) + .placeholder("{USAGE}", schematic.first()) + .send(); + return; + } + + messageService.create() + .viewer(sender) + .notice(notice -> notice.liteCommandsMessages.commandUsageHeader()) + .send(); + + for (final String sch : schematic.all()) { + messageService.create() + .viewer(sender) + .notice(notice -> notice.liteCommandsMessages.commandUsageEntry()) + .placeholder("{USAGE}", sch) + .send(); + } + } +} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/MissingPermissionHandler.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/MissingPermissionsHandlerImpl.java similarity index 54% rename from src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/MissingPermissionHandler.java rename to automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/MissingPermissionsHandlerImpl.java index f459ce3..5e261f7 100644 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/MissingPermissionHandler.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/MissingPermissionsHandlerImpl.java @@ -1,6 +1,7 @@ -package com.github.imdmk.automessage.feature.command.builder.handler; +package com.github.imdmk.automessage.platform.litecommands.handler; -import com.github.imdmk.automessage.feature.message.MessageService; +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.permission.MissingPermissions; @@ -8,21 +9,19 @@ import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; -import java.util.Objects; - -public class MissingPermissionHandler implements MissingPermissionsHandler { +public final class MissingPermissionsHandlerImpl implements MissingPermissionsHandler { private final MessageService messageService; - public MissingPermissionHandler(@NotNull MessageService messageService) { - this.messageService = Objects.requireNonNull(messageService, "message service cannot be null"); + public MissingPermissionsHandlerImpl(@NotNull MessageService messageService) { + this.messageService = Validator.notNull(messageService, "messageService cannot be null"); } @Override public void handle(Invocation invocation, MissingPermissions permissions, ResultHandlerChain chain) { - this.messageService.create() + messageService.create() .viewer(invocation.sender()) - .notice(notice -> notice.missingPermissions) + .notice(n -> n.liteCommandsMessages.commandPermissionMissing()) .placeholder("{PERMISSIONS}", String.join(", ", permissions.getPermissions())) .send(); } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/NoticeResultHandlerImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/NoticeResultHandlerImpl.java new file mode 100644 index 0000000..ad8ac28 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/handler/NoticeResultHandlerImpl.java @@ -0,0 +1,24 @@ +package com.github.imdmk.automessage.platform.litecommands.handler; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; +import dev.rollczi.litecommands.handler.result.ResultHandler; +import dev.rollczi.litecommands.handler.result.ResultHandlerChain; +import dev.rollczi.litecommands.invocation.Invocation; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public final class NoticeResultHandlerImpl implements ResultHandler { + + private final MessageService messageService; + + public NoticeResultHandlerImpl(@NotNull MessageService messageService) { + this.messageService = Validator.notNull(messageService, "messageService cannot be null"); + } + + @Override + public void handle(Invocation invocation, Notice notice, ResultHandlerChain chain) { + messageService.send(invocation.sender(), n -> notice); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessages.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessages.java new file mode 100644 index 0000000..06a547a --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessages.java @@ -0,0 +1,15 @@ +package com.github.imdmk.automessage.platform.litecommands.messages; + +import com.eternalcode.multification.notice.Notice; + +public interface LiteCommandsMessages { + + Notice commandPermissionMissing(); + + Notice commandUsageInvalid(); + + Notice commandUsageHeader(); + + Notice commandUsageEntry(); + +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java new file mode 100644 index 0000000..e859a5f --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java @@ -0,0 +1,75 @@ +package com.github.imdmk.automessage.platform.litecommands.messages; + +import com.eternalcode.multification.notice.Notice; +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; + +public final class LiteCommandsMessagesImpl extends OkaeriConfig implements LiteCommandsMessages { + + @Comment({ + "#", + "# Sent when a player attempts to execute a command without having all required permissions.", + "#", + "# Placeholders:", + "# {PERMISSIONS} - comma-separated list of missing permission nodes required to execute the command.", + "#" + }) + Notice commandPermissionMissing = Notice.chat( + "You are missing required permissions {PERMISSIONS} to execute this command." + ); + + @Comment({ + "#", + "# Sent when a player uses a command with an invalid or incomplete syntax.", + "# ", + "# Placeholders:", + "# {USAGE} - correct usage format of the command (e.g. /playtime ).", + "#" + }) + Notice commandUsageInvalid = Notice.chat( + "Invalid command usage! Correct syntax: {USAGE}." + ); + + @Comment({ + "#", + "# Header shown before listing available correct usages for a command.", + "# Typically used together with 'commandUsageEntry' when there are multiple valid variants.", + "#" + }) + Notice commandUsageHeader = Notice.chat( + "Correct usage variants:" + ); + + @Comment({ + "#", + "# Single entry in the list of valid command usages.", + "# Displayed under 'commandUsageHeader' for each available usage.", + "# ", + "# Placeholders:", + "# {USAGE} - a single valid usage variant of the command.", + "#" + }) + Notice commandUsageEntry = Notice.chat( + "{USAGE}" + ); + + @Override + public Notice commandPermissionMissing() { + return commandPermissionMissing; + } + + @Override + public Notice commandUsageInvalid() { + return commandUsageInvalid; + } + + @Override + public Notice commandUsageHeader() { + return commandUsageHeader; + } + + @Override + public Notice commandUsageEntry() { + return commandUsageEntry; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java new file mode 100644 index 0000000..0553682 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java @@ -0,0 +1,114 @@ +package com.github.imdmk.automessage.platform.logger; + +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +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"); + } + + /** + * 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 + */ + public BukkitPluginLogger(@NotNull Plugin plugin) { + this(plugin.getLogger()); + } + + @Override + public void info(@NotNull String message) { + logger.info(message); + } + + @Override + public void info(@NotNull String message, @NotNull Object... args) { + logger.log(Level.INFO, format(message, args)); + } + + @Override + public void warn(@NotNull String message) { + logger.warning(message); + } + + @Override + public void warn(@NotNull Throwable throwable) { + logger.warning(throwable.getMessage()); + } + + @Override + public void warn(@NotNull Throwable throwable, @NotNull String message, @NotNull Object... args) { + logger.log(Level.WARNING, format(message, args), throwable); + } + + @Override + public void warn(@NotNull String message, @NotNull Object... args) { + logger.log(Level.WARNING, format(message, args)); + } + + @Override + public void error(@NotNull Throwable throwable) { + logger.severe(throwable.getMessage()); + } + + @Override + public void error(@NotNull String message) { + logger.log(Level.SEVERE, message); + } + + @Override + public void error(@NotNull String message, @NotNull Object... args) { + logger.log(Level.SEVERE, format(message, args)); + } + + @Override + public void error(@NotNull Throwable throwable, @NotNull String message) { + logger.log(Level.SEVERE, message, throwable); + } + + @Override + public void error(@NotNull Throwable throwable, @NotNull String message, @NotNull Object... args) { + logger.log(Level.SEVERE, format(message, args), throwable); + } + + private String format(String message, Object... args) { + return String.format(Locale.ROOT, message, args); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/PluginLogger.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/PluginLogger.java new file mode 100644 index 0000000..c7c33d4 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/PluginLogger.java @@ -0,0 +1,117 @@ +package com.github.imdmk.automessage.platform.logger; + +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); + + /** + * 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, @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/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/BukkitTaskScheduler.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/BukkitTaskScheduler.java new file mode 100644 index 0000000..d2cae1d --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/BukkitTaskScheduler.java @@ -0,0 +1,148 @@ +package com.github.imdmk.automessage.platform.scheduler; + +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +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}.

+ */ +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; + + public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler scheduler) { + this.plugin = Validator.notNull(plugin, "plugin"); + this.scheduler = Validator.notNull(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"); + 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"); + 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() { + 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"); + + long ticks = duration.toMillis() / MILLIS_PER_TICK; + return ticks <= 0 ? 0 : (int) ticks; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/PluginTask.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/PluginTask.java new file mode 100644 index 0000000..73f8a28 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/PluginTask.java @@ -0,0 +1,62 @@ +package com.github.imdmk.automessage.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/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/TaskScheduler.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/TaskScheduler.java new file mode 100644 index 0000000..da8b310 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/scheduler/TaskScheduler.java @@ -0,0 +1,152 @@ +package com.github.imdmk.automessage.platform.scheduler; + +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +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(); +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessage.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessage.java new file mode 100644 index 0000000..341a39f --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessage.java @@ -0,0 +1,28 @@ +package com.github.imdmk.automessage.scheduled; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRule; +import com.github.imdmk.automessage.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.List; + +public record ScheduledMessage( + @NotNull String name, + @NotNull @Unmodifiable List notices, + @NotNull @Unmodifiable List rules) { + + public ScheduledMessage { + Validator.notNull(name, "name cannot be null"); + Validator.notNull(notices, "notices cannot be null"); + Validator.notNull(rules, "rules cannot be null"); + + if (notices.isEmpty()) { + throw new IllegalArgumentException("ScheduledMessage must contain at least one Notice"); + } + + notices = List.copyOf(notices); + rules = List.copyOf(rules); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageBuilder.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageBuilder.java new file mode 100644 index 0000000..fc6fcfc --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageBuilder.java @@ -0,0 +1,65 @@ +package com.github.imdmk.automessage.scheduled; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRule; +import com.github.imdmk.automessage.shared.validate.Validator; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public final class ScheduledMessageBuilder { + + private String name; + private final List notices = new ArrayList<>(); + private final List rules = new ArrayList<>(); + + public static ScheduledMessageBuilder create() { + return new ScheduledMessageBuilder(); + } + + public @NotNull ScheduledMessageBuilder name(@NotNull String name) { + this.name = Validator.notNull(name, "name"); + return this; + } + + public @NotNull ScheduledMessageBuilder addNotice(@NotNull Notice notice) { + this.notices.add(Validator.notNull(notice, "notice")); + return this; + } + + public @NotNull ScheduledMessageBuilder addNotices(@NotNull List notices) { + Validator.notNull(notices, "notices"); + notices.forEach(n -> this.notices.add(Validator.notNull(n, "notice element"))); + return this; + } + + public @NotNull ScheduledMessageBuilder addNotices(@NotNull Notice... notices) { + return addNotices(List.of(notices)); + } + + public @NotNull ScheduledMessageBuilder addRule(@NotNull AudienceRule rule) { + this.rules.add(Validator.notNull(rule, "rule")); + return this; + } + + public @NotNull ScheduledMessageBuilder addRules(@NotNull List rules) { + Validator.notNull(rules, "rules"); + rules.forEach(r -> this.rules.add(Validator.notNull(r, "rule element"))); + return this; + } + + public @NotNull ScheduledMessageBuilder addRules(@NotNull AudienceRule... rules) { + return addRules(List.of(rules)); + } + + public @NotNull ScheduledMessage build() { + Validator.notNull(this.name, "name is required"); + + return new ScheduledMessage( + this.name, + List.copyOf(this.notices), + List.copyOf(this.rules) + ); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java new file mode 100644 index 0000000..5fec508 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java @@ -0,0 +1,106 @@ +package com.github.imdmk.automessage.scheduled; + +import com.eternalcode.multification.notice.Notice; +import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults; +import com.eternalcode.multification.okaeri.MultificationSerdesPack; +import com.github.imdmk.automessage.config.ConfigSection; +import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRule; +import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRuleSerializer; +import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import net.kyori.adventure.bossbar.BossBar; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; + +public final class ScheduledMessageConfig extends ConfigSection { + + @Comment({ + "#", + "# List of all scheduled messages dispatched automatically by the plugin.", + "# Each entry contains:", + "# - name: unique identifier", + "# - notices: one or more message types (chat, title, actionbar, bossbar, sound)", + "# - rules: audience restrictions (permissions, groups)", + "#", + "# You may freely remove, modify, or add new message entries.", + "# Message order is preserved and processed sequentially by the dispatcher.", + "#" + }) + public List messages = Arrays.asList( + ScheduledMessageBuilder.create() + .name("first-message") + .addNotices( + Notice.chat("[!] This is the first announcement of AutoMessage!"), + Notice.sound(Key.key("entity.experience_orb.pickup"), Sound.Source.MASTER, 1.0F, 1.0F) + ) + .build(), + + ScheduledMessageBuilder.create() + .name("second-message-actionbar") + .addNotices(Notice.actionbar( + "[!] This is the second announcement of AutoMessage!" + )) + .build(), + + ScheduledMessageBuilder.create() + .name("third-message-title") + .addNotices(Notice.title( + "[!]", + "This is the third announcement!" + )) + .build(), + + ScheduledMessageBuilder.create() + .name("fourth-message-bossbar") + .addNotices(Notice.bossBar( + BossBar.Color.RED, + BossBar.Overlay.PROGRESS, + Duration.ofSeconds(5), + "[!] This is the fourth announcement!" + )) + .build(), + + ScheduledMessageBuilder.create() + .name("multiple-chat-actionbar") + .addNotices( + Notice.chat("[!] This is a multi-channel announcement!"), + Notice.actionbar("[!] This is a multi-channel announcement!") + ) + .build(), + + ScheduledMessageBuilder.create() + .name("only-vip-permission") + .addNotice( + Notice.chat("[!] This is a message to only players with `permission.vip`!") + ) + .addRule(AudienceRule.permission("permission.vip")) + .build(), + + ScheduledMessageBuilder.create() + .name("only-vip-group") + .addNotice( + Notice.chat("[!] This is a message to only players with VIP group!") + ) + .addRule(AudienceRule.group("vip")) + .build() + ); + + @Override + public @NotNull OkaeriSerdesPack getSerdesPack() { + return registry -> { + registry.register(new ScheduledMessageSerializer()); + registry.register(new AudienceRuleSerializer()); + registry.register(new MultificationSerdesPack(NoticeResolverDefaults.createRegistry())); + }; + } + + @Override + public @NotNull String getFileName() { + return "scheduledMessages.yml"; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageSerializer.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageSerializer.java new file mode 100644 index 0000000..02fff8c --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageSerializer.java @@ -0,0 +1,39 @@ +package com.github.imdmk.automessage.scheduled; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRule; +import eu.okaeri.configs.schema.GenericsDeclaration; +import eu.okaeri.configs.serdes.DeserializationData; +import eu.okaeri.configs.serdes.ObjectSerializer; +import eu.okaeri.configs.serdes.SerializationData; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public final class ScheduledMessageSerializer implements ObjectSerializer { + + @Override + public boolean supports(@NotNull Class type) { + return ScheduledMessage.class.isAssignableFrom(type); + } + + @Override + public void serialize(@NotNull ScheduledMessage notice, @NotNull SerializationData data, @NotNull GenericsDeclaration generics) { + data.add("name", notice.name(), String.class); + data.addCollection("notices", notice.notices(), Notice.class); + data.addCollection("rules", notice.rules(), AudienceRule.class); + } + + @Override + public ScheduledMessage deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { + String name = data.get("name", String.class); + List notices = data.getAsList("notices", Notice.class); + List rules = data.getAsList("rules", AudienceRule.class); + + return new ScheduledMessage( + name, + notices, + rules + ); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java new file mode 100644 index 0000000..3ce55c2 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java @@ -0,0 +1,14 @@ +package com.github.imdmk.automessage.scheduled.audience.filter; + +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public interface AudienceFilter { + + boolean allows(@NotNull Player player, @NotNull ScheduledMessage message); + + static AudienceFilter createDefault() { + return new AudienceFilterImpl(); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilterImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilterImpl.java new file mode 100644 index 0000000..845729e --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilterImpl.java @@ -0,0 +1,19 @@ +package com.github.imdmk.automessage.scheduled.audience.filter; + +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +final class AudienceFilterImpl implements AudienceFilter { + + @Override + public boolean allows(@NotNull Player player, @NotNull ScheduledMessage message) { + Validator.notNull(player, "player"); + Validator.notNull(message, "message"); + + return message.rules().stream() + .allMatch(rule -> rule.test(player)); + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceGroupRule.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceGroupRule.java new file mode 100644 index 0000000..dde58f8 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceGroupRule.java @@ -0,0 +1,26 @@ +package com.github.imdmk.automessage.scheduled.audience.rule; + +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public record AudienceGroupRule(@NotNull String group) implements AudienceRule { + + private static final String GROUP_PREFIX = "group."; + + public AudienceGroupRule { + Validator.notNull(group, "group"); + } + + @Override + public boolean test(@NotNull Player player) { + String permission = GROUP_PREFIX + group; + return player.hasPermission(permission); + } + + @Override + public @NotNull Type type() { + return Type.GROUP; + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudiencePermissionRule.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudiencePermissionRule.java new file mode 100644 index 0000000..e0ca307 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudiencePermissionRule.java @@ -0,0 +1,22 @@ +package com.github.imdmk.automessage.scheduled.audience.rule; + +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public record AudiencePermissionRule(@NotNull String permission) implements AudienceRule { + + public AudiencePermissionRule { + Validator.notNull(permission, "permission"); + } + + @Override + public boolean test(@NotNull Player player) { + return player.hasPermission(permission); + } + + @Override + public @NotNull Type type() { + return Type.PERMISSION; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java new file mode 100644 index 0000000..b906cfa --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java @@ -0,0 +1,27 @@ +package com.github.imdmk.automessage.scheduled.audience.rule; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public sealed interface AudienceRule + permits AudiencePermissionRule, AudienceGroupRule { + + static AudienceGroupRule group(String group) { + return new AudienceGroupRule(group); + } + + static AudiencePermissionRule permission(String permission) { + return new AudiencePermissionRule(permission); + } + + boolean test(@NotNull Player player); + + enum Type { + PERMISSION, + GROUP + } + + @NotNull + Type type(); +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRuleSerializer.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRuleSerializer.java new file mode 100644 index 0000000..8bdbb58 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRuleSerializer.java @@ -0,0 +1,35 @@ +package com.github.imdmk.automessage.scheduled.audience.rule; + +import eu.okaeri.configs.schema.GenericsDeclaration; +import eu.okaeri.configs.serdes.DeserializationData; +import eu.okaeri.configs.serdes.ObjectSerializer; +import eu.okaeri.configs.serdes.SerializationData; +import org.jetbrains.annotations.NotNull; + +public final class AudienceRuleSerializer implements ObjectSerializer { + + @Override + public boolean supports(@NotNull Class type) { + return AudienceRule.class.isAssignableFrom(type); + } + + @Override + public void serialize(@NotNull AudienceRule rule, @NotNull SerializationData data, @NotNull GenericsDeclaration generics) { + data.add("type", rule.type(), AudienceRule.Type.class); + + switch (rule) { + case AudienceGroupRule audienceGroupRule -> data.add("group", audienceGroupRule.group(), String.class); + case AudiencePermissionRule audiencePermissionRule -> data.add("permission", audiencePermissionRule.permission(), String.class); + } + } + + @Override + public AudienceRule deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { + final AudienceRule.Type type = data.get("type", AudienceRule.Type.class); + + return switch (type) { + case GROUP -> new AudienceGroupRule(data.get("group", String.class)); + case PERMISSION -> new AudiencePermissionRule(data.get("permission", String.class)); + }; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java new file mode 100644 index 0000000..0bebd8e --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java @@ -0,0 +1,26 @@ +package com.github.imdmk.automessage.scheduled.dispatcher; + +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.function.Predicate; + +public record DispatchFilteredTarget( + @NotNull Collection targets, + @NotNull Predicate filter +) implements DispatchTarget { + + public DispatchFilteredTarget { + Validator.notNull(targets, "targets cannot be null"); + Validator.notNull(filter, "filter cannot be null"); + } + + @Override + public @NotNull Collection recipients() { + return targets.stream() + .filter(filter) + .toList(); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java new file mode 100644 index 0000000..a9d3b33 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java @@ -0,0 +1,21 @@ +package com.github.imdmk.automessage.scheduled.dispatcher; + +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +public record DispatchPlayersTarget( + @NotNull Collection targets +) implements DispatchTarget { + + public DispatchPlayersTarget { + Validator.notNull(targets, "targets cannot be null"); + } + + @Override + public @NotNull Collection recipients() { + return targets; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java new file mode 100644 index 0000000..8532b20 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java @@ -0,0 +1,28 @@ +package com.github.imdmk.automessage.scheduled.dispatcher; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +public interface DispatchTarget { + + @NotNull Collection recipients(); + + static @NotNull DispatchTarget player(@NotNull Player player) { + return new DispatchPlayersTarget(List.of(player)); + } + + static @NotNull DispatchTarget players(@NotNull Collection players) { + return new DispatchPlayersTarget(players); + } + + static @NotNull DispatchTarget filtered( + @NotNull Collection players, + @NotNull Predicate filter + ) { + return new DispatchFilteredTarget(players, filter); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java new file mode 100644 index 0000000..797aecb --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java @@ -0,0 +1,80 @@ +package com.github.imdmk.automessage.scheduled.dispatcher; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import com.github.imdmk.automessage.scheduled.audience.filter.AudienceFilter; +import com.github.imdmk.automessage.scheduled.selector.MessageSelector; +import com.github.imdmk.automessage.shared.message.MessageService; +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +public final class MessageDispatcher { + + private final MessageService messageService; + private final MessageSelector selector; + private final AudienceFilter filter; + private final Supplier> messagesSupplier; + + public MessageDispatcher( + @NotNull MessageService messageService, + @NotNull MessageSelector selector, + @NotNull AudienceFilter filter, + @NotNull Supplier> messagesSupplier + ) { + this.messageService = Validator.notNull(messageService, "messageService"); + this.selector = Validator.notNull(selector, "selector"); + this.filter = Validator.notNull(filter, "filter"); + this.messagesSupplier = Validator.notNull(messagesSupplier, "messages"); + } + + public void dispatchNext(@NotNull DispatchTarget target) { + dispatchNext(target, true); + } + + public void dispatchNext( + @NotNull DispatchTarget target, + boolean advanceSelectorIndex + ) { + final List messages = messagesSupplier.get(); + if (messages.isEmpty()) { + return; + } + + final Optional next = selector.selectNext(messages, advanceSelectorIndex); + if (next.isEmpty()) { + return; + } + + dispatch(next.get(), target); + } + + public void dispatch( + @NotNull ScheduledMessage message, + @NotNull DispatchTarget target + ) { + for (final Player recipient : target.recipients()) { + if (!filter.allows(recipient, message)) { + continue; + } + + sendToPlayer(recipient, message); + } + } + + private void sendToPlayer( + @NotNull Player recipient, + @NotNull ScheduledMessage message + ) { + for (final Notice notice : message.notices()) { + messageService.create() + .viewer(recipient) + .notice(notice) + .sendAsync(); + } + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java new file mode 100644 index 0000000..8f376aa --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java @@ -0,0 +1,41 @@ +package com.github.imdmk.automessage.scheduled.dispatcher; + +import com.github.imdmk.automessage.config.ConfigSection; +import com.github.imdmk.automessage.scheduled.selector.MessageSelectorType; +import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import eu.okaeri.configs.serdes.commons.SerdesCommons; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +public final class MessageDispatcherConfig extends ConfigSection { + + @Comment("# Should the automatic dispatcher enabled?") + public boolean enabled = true; + + @Comment("# Delay between messages (time between dispatch executions).") + public Duration period = Duration.ofSeconds(10); + + @Comment("# How long to wait before the first automatic dispatch.") + public Duration initialDelay = Duration.ZERO; + + @Comment("# Strategy used to select the next message.") + public MessageSelectorType selector = MessageSelectorType.SEQUENTIAL; + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public @NotNull OkaeriSerdesPack getSerdesPack() { + return registry -> { + registry.register(new SerdesCommons()); + }; + } + + @Override + public @NotNull String getFileName() { + return "messageDispatcher.yml"; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java new file mode 100644 index 0000000..70ec4d6 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java @@ -0,0 +1,50 @@ +package com.github.imdmk.automessage.scheduled.dispatcher; + +import com.github.imdmk.automessage.platform.scheduler.PluginTask; +import com.github.imdmk.automessage.shared.validate.Validator; +import org.bukkit.Server; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +public final class MessageDispatcherTask implements PluginTask { + + private final Server server; + private final MessageDispatcherConfig dispatcherConfig; + private final MessageDispatcher messageDispatcher; + + public MessageDispatcherTask( + @NotNull Server server, + @NotNull MessageDispatcherConfig dispatcherConfig, + @NotNull MessageDispatcher messageDispatcher + ) { + this.server = Validator.notNull(server, "server"); + this.dispatcherConfig = Validator.notNull(dispatcherConfig, "dispatcherConfig"); + this.messageDispatcher = Validator.notNull(messageDispatcher, "messageDispatcher"); + } + + @Override + public void run() { + if (!dispatcherConfig.enabled) { + return; + } + + var onlinePlayers = server.getOnlinePlayers(); + if (onlinePlayers.isEmpty()) { + return; + } + + var target = DispatchTarget.players(onlinePlayers); + messageDispatcher.dispatchNext(target); + } + + @Override + public Duration delay() { + return dispatcherConfig.initialDelay; + } + + @Override + public Duration period() { + return dispatcherConfig.period; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelector.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelector.java new file mode 100644 index 0000000..ebdd057 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelector.java @@ -0,0 +1,16 @@ +package com.github.imdmk.automessage.scheduled.selector; + +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; + +public interface MessageSelector { + + Optional selectNext(@NotNull List messages, boolean advanceIndex); + + default Optional selectNext(@NotNull List messages) { + return selectNext(messages, true); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactory.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactory.java new file mode 100644 index 0000000..5a2c862 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactory.java @@ -0,0 +1,18 @@ +package com.github.imdmk.automessage.scheduled.selector; + +import org.jetbrains.annotations.NotNull; + +public final class MessageSelectorFactory { + + private MessageSelectorFactory() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } + + public static @NotNull MessageSelector create(@NotNull MessageSelectorType type) { + return switch (type) { + case RANDOM -> new RandomMessageSelector(); + case SEQUENTIAL -> new SequentialMessageSelector(); + }; + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorType.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorType.java new file mode 100644 index 0000000..71dcdfb --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorType.java @@ -0,0 +1,6 @@ +package com.github.imdmk.automessage.scheduled.selector; + +public enum MessageSelectorType { + RANDOM, + SEQUENTIAL, +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelector.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelector.java new file mode 100644 index 0000000..9ffe6a8 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelector.java @@ -0,0 +1,21 @@ +package com.github.imdmk.automessage.scheduled.selector; + +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; + +final class RandomMessageSelector implements MessageSelector { + + @Override + public Optional selectNext(@NotNull List messages, boolean advanceIndex) { + if (messages.isEmpty()) { + return Optional.empty(); + } + + int index = ThreadLocalRandom.current().nextInt(messages.size()); + return Optional.of(messages.get(index)); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelector.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelector.java new file mode 100644 index 0000000..4a9d543 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelector.java @@ -0,0 +1,43 @@ +package com.github.imdmk.automessage.scheduled.selector; + +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +final class SequentialMessageSelector implements MessageSelector { + + private static final int RESET_THRESHOLD = 1_000_000_000; + private final AtomicInteger index = new AtomicInteger(0); + + @Override + public Optional selectNext( + @NotNull List messages, + boolean advanceIndex + ) { + final int size = messages.size(); + if (size == 0) { + return Optional.empty(); + } + + final int current = index.get(); + final int position = Math.floorMod(current, size); + final ScheduledMessage selected = messages.get(position); + + // increment AFTER selecting, if required + if (advanceIndex) { + int next = current + 1; + + if (next >= RESET_THRESHOLD) { + next = 0; // prevent overflow + } + + index.set(next); + } + + return Optional.of(selected); + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentSerializer.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentSerializer.java new file mode 100644 index 0000000..e7e08d9 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentSerializer.java @@ -0,0 +1,26 @@ +package com.github.imdmk.automessage.shared.adventure; + +import eu.okaeri.configs.schema.GenericsDeclaration; +import eu.okaeri.configs.serdes.DeserializationData; +import eu.okaeri.configs.serdes.ObjectSerializer; +import eu.okaeri.configs.serdes.SerializationData; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +public final class AdventureComponentSerializer implements ObjectSerializer { + + @Override + public boolean supports(@NotNull Class type) { + return Component.class.isAssignableFrom(type); + } + + @Override + public void serialize(@NotNull Component component, @NotNull SerializationData data, @NotNull GenericsDeclaration generics) { + data.setValue(AdventureComponents.serialize(component)); + } + + @Override + public Component deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { + return AdventureComponents.text(data.getValue(String.class)); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponents.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponents.java new file mode 100644 index 0000000..f712d2e --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureComponents.java @@ -0,0 +1,182 @@ +package com.github.imdmk.automessage.shared.adventure; + +import com.github.imdmk.automessage.shared.validate.Validator; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +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(); + + 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"); + 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"); + + final List out = new ArrayList<>(texts.length); + for (CharSequence text : texts) { + out.add(MINI_MESSAGE.deserialize(text.toString())); + } + + 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"); + + 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"); + 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) { + 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"); + 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"); + + 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"); + 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"); + + 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"); + Validator.notNull(delimiter, "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); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatter.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatter.java new file mode 100644 index 0000000..6f782c1 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatter.java @@ -0,0 +1,84 @@ +package com.github.imdmk.automessage.shared.adventure; + +import com.github.imdmk.automessage.shared.validate.Validator; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.List; +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 cannot be null"); + 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 cannot be null"); + 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 cannot be null"); + Validator.notNull(placeholders, "placeholders cannot be null"); + + if (placeholders.isEmpty()) { + return input; + } + + // Sort keys by descending length to avoid substring overlap + 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); + } + + return out; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholders.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholders.java new file mode 100644 index 0000000..70a085a --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholders.java @@ -0,0 +1,158 @@ +package com.github.imdmk.automessage.shared.adventure; + +import com.github.imdmk.automessage.shared.validate.Validator; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collections; +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()); + + private final Map map; + + private AdventurePlaceholders(@NotNull Map map) { + Validator.notNull(map, "map cannot be null"); + 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() { + return EMPTY; + } + + /** + * Creates a new builder for {@link AdventurePlaceholders}. + * + * @return new builder instance + */ + public static @NotNull 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 cannot be null"); + Validator.notNull(value, "value cannot be null"); + 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"); + 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"); + 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"); + 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() { + if (this.entries.isEmpty()) { + return EMPTY; + } + + return new AdventurePlaceholders(new LinkedHashMap<>(this.entries)); + } + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java new file mode 100644 index 0000000..9b5c9eb --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java @@ -0,0 +1,49 @@ +package com.github.imdmk.automessage.shared.message; + +import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults; +import com.eternalcode.multification.okaeri.MultificationSerdesPack; +import com.github.imdmk.automessage.command.dispatcher.messages.DispatcherMessagesImpl; +import com.github.imdmk.automessage.command.reload.messages.ReloadMessagesImpl; +import com.github.imdmk.automessage.config.ConfigSection; +import com.github.imdmk.automessage.platform.litecommands.messages.LiteCommandsMessagesImpl; +import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import org.jetbrains.annotations.NotNull; + +public final class MessageConfig extends ConfigSection { + + @Comment({ + "#", + "# Messages used by the LiteCommands subsystem.", + "# Contains permission errors, usage hints, and syntax messages.", + "#" + }) + public LiteCommandsMessagesImpl liteCommandsMessages = new LiteCommandsMessagesImpl(); + + @Comment({ + "#", + "# Messages used by dispatcher-related commands.", + "#" + }) + public DispatcherMessagesImpl dispatcherMessages = new DispatcherMessagesImpl(); + + @Comment({ + "#", + "# Messages used during configuration reload commands.", + "# Includes success and failure notifications.", + "#" + }) + public ReloadMessagesImpl reloadMessages = new ReloadMessagesImpl(); + + @Override + public @NotNull OkaeriSerdesPack getSerdesPack() { + return registry -> { + registry.register(new MultificationSerdesPack(NoticeResolverDefaults.createRegistry())); + }; + } + + @Override + public @NotNull String getFileName() { + return "messages.yml"; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageService.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageService.java new file mode 100644 index 0000000..c2783cc --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageService.java @@ -0,0 +1,86 @@ +package com.github.imdmk.automessage.shared.message; + +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.automessage.shared.validate.Validator; +import net.kyori.adventure.platform.AudienceProvider; +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.jetbrains.annotations.NotNull; + +public final class MessageService extends BukkitMultification { + + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + private final MessageConfig messageConfig; + private final AudienceProvider audienceProvider; + + public MessageService( + @NotNull MessageConfig messageConfig, + @NotNull AudienceProvider audienceProvider + ) { + this.messageConfig = Validator.notNull(messageConfig, "messageConfig cannot be null"); + this.audienceProvider = Validator.notNull(audienceProvider, "audienceProvider cannot be null"); + } + + /** + * 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 MINI_MESSAGE; + } + + /** + * 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 -> { + if (sender instanceof Player player) { + return audienceProvider.player(player.getUniqueId()); + } + return audienceProvider.console(); + }; + } + + /** + * 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 cannot be null"); + Validator.notNull(notice, "notice cannot be null"); + create().viewer(sender).notice(notice).send(); + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/validate/Validator.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/validate/Validator.java new file mode 100644 index 0000000..2e20606 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/validate/Validator.java @@ -0,0 +1,65 @@ +package com.github.imdmk.automessage.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 exception context 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) { + 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"); + if (obj != null) { + consumer.accept(obj); + } + } + +} \ No newline at end of file diff --git a/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/ScheduledMessageTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/ScheduledMessageTest.java new file mode 100644 index 0000000..aaeca09 --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/ScheduledMessageTest.java @@ -0,0 +1,107 @@ +package com.github.imdmk.automessage.scheduled; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRule; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ScheduledMessageTest { + + @Test + @DisplayName("Should create ScheduledMessage when valid arguments are provided") + void shouldCreateScheduledMessage() { + // given + var notice = Notice.chat("hello"); + var rule = AudienceRule.permission("test.permission"); + + // when + ScheduledMessage message = new ScheduledMessage( + "example", + List.of(notice), + List.of(rule) + ); + + // then + assertEquals("example", message.name()); + assertEquals(List.of(notice), message.notices()); + assertEquals(List.of(rule), message.rules()); + } + + @Test + @DisplayName("Should throw when name is null") + void shouldFailWhenNameNull() { + var notice = Notice.chat("test"); + + assertThrows(NullPointerException.class, () -> + new ScheduledMessage( + null, + List.of(notice), + List.of() + ) + ); + } + + @Test + @DisplayName("Should throw when notices list is null") + void shouldFailWhenNoticesNull() { + assertThrows(NullPointerException.class, () -> + new ScheduledMessage( + "msg", + null, + List.of() + ) + ); + } + + @Test + @DisplayName("Should throw when notices list is empty") + void shouldFailWhenNoticesEmpty() { + assertThrows(IllegalArgumentException.class, () -> + new ScheduledMessage( + "msg", + List.of(), + List.of() + ) + ); + } + + @Test + @DisplayName("Should produce unmodifiable notice and rule lists") + void shouldMakeCollectionsUnmodifiable() { + var notice = Notice.chat("test"); + var rule = AudienceRule.permission("perm"); + + ScheduledMessage message = new ScheduledMessage( + "msg", + List.of(notice), + List.of(rule) + ); + + assertThrows(UnsupportedOperationException.class, () -> + message.notices().add(Notice.chat("other")) + ); + + assertThrows(UnsupportedOperationException.class, () -> + message.rules().add(AudienceRule.group("vip")) + ); + } + + @Test + @DisplayName("Record equality should compare field values") + void shouldRespectRecordEquality() { + var n1 = Notice.chat("x"); + var r1 = AudienceRule.permission("p"); + + ScheduledMessage a = new ScheduledMessage("name", List.of(n1), List.of(r1)); + ScheduledMessage b = new ScheduledMessage("name", List.of(n1), List.of(r1)); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } +} + diff --git a/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactoryTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactoryTest.java new file mode 100644 index 0000000..f80107a --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/MessageSelectorFactoryTest.java @@ -0,0 +1,38 @@ +package com.github.imdmk.automessage.scheduled.selector; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MessageSelectorFactoryTest { + + @Test + @DisplayName("Factory should create a RandomMessageSelector when RANDOM is chosen") + void shouldCreateRandomSelector() { + MessageSelector selector = MessageSelectorFactory.create(MessageSelectorType.RANDOM); + + assertNotNull(selector); + assertEquals(RandomMessageSelector.class, selector.getClass()); + } + + @Test + @DisplayName("Factory should create a SequentialMessageSelector when SEQUENTIAL is chosen") + void shouldCreateSequentialSelector() { + MessageSelector selector = MessageSelectorFactory.create(MessageSelectorType.SEQUENTIAL); + + assertNotNull(selector); + assertEquals(SequentialMessageSelector.class, selector.getClass()); + } + + @Test + @DisplayName("Factory should reject null type") + void shouldRejectNullType() { + assertThrows(NullPointerException.class, () -> + MessageSelectorFactory.create(null) + ); + } +} + diff --git a/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelectorTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelectorTest.java new file mode 100644 index 0000000..be0ebda --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/RandomMessageSelectorTest.java @@ -0,0 +1,56 @@ +package com.github.imdmk.automessage.scheduled.selector; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RandomMessageSelectorTest { + + private static ScheduledMessage msg(String name) { + return new ScheduledMessage(name, List.of(Notice.chat("x")), List.of()); + } + + @Test + @DisplayName("Random selector should return empty if list is empty") + void shouldReturnEmptyOnEmptyList() { + RandomMessageSelector selector = new RandomMessageSelector(); + + assertTrue(selector.selectNext(List.of()).isEmpty()); + } + + @Test + @DisplayName("Random selector should always return an element from the list") + void shouldReturnElementFromList() { + RandomMessageSelector selector = new RandomMessageSelector(); + List messages = List.of(msg("a"), msg("b"), msg("c")); + + ScheduledMessage result = selector.selectNext(messages).orElseThrow(); + + assertTrue(messages.contains(result)); + } + + @Test + @DisplayName("Random selector should return varying results across multiple calls") + void shouldReturnDifferentElements() { + RandomMessageSelector selector = new RandomMessageSelector(); + List messages = List.of(msg("a"), msg("b"), msg("c")); + + boolean variationFound = false; + + ScheduledMessage first = selector.selectNext(messages).orElseThrow(); + + for (int i = 0; i < 50; i++) { + if (!selector.selectNext(messages).orElseThrow().equals(first)) { + variationFound = true; + break; + } + } + + assertTrue(variationFound, "Random selector returned the same element in all iterations — suspicious randomness"); + } +} diff --git a/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelectorTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelectorTest.java new file mode 100644 index 0000000..3c85203 --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/scheduled/selector/SequentialMessageSelectorTest.java @@ -0,0 +1,78 @@ +package com.github.imdmk.automessage.scheduled.selector; + +import com.eternalcode.multification.notice.Notice; +import com.github.imdmk.automessage.scheduled.ScheduledMessage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SequentialMessageSelectorTest { + + private static ScheduledMessage msg(String name) { + return new ScheduledMessage(name, List.of(Notice.chat("x")), List.of()); + } + + @Test + @DisplayName("Sequential selector should return empty if list is empty") + void shouldReturnEmptyOnEmptyList() { + SequentialMessageSelector selector = new SequentialMessageSelector(); + + assertTrue(selector.selectNext(List.of(), true).isEmpty()); + } + + @Test + @DisplayName("Sequential selector should iterate in order") + void shouldIterateSequentially() { + SequentialMessageSelector selector = new SequentialMessageSelector(); + List messages = List.of(msg("a"), msg("b"), msg("c")); + + assertEquals("a", selector.selectNext(messages).get().name()); + assertEquals("b", selector.selectNext(messages).get().name()); + assertEquals("c", selector.selectNext(messages).get().name()); + assertEquals("a", selector.selectNext(messages).get().name()); // wraps around + } + + @Test + @DisplayName("Sequential selector should not advance index when advanceIndex=false") + void shouldNotAdvanceIndexWhenFlagFalse() { + SequentialMessageSelector selector = new SequentialMessageSelector(); + List messages = List.of(msg("a"), msg("b")); + + assertEquals("a", selector.selectNext(messages, false).get().name()); + assertEquals("a", selector.selectNext(messages, false).get().name()); + assertEquals("a", selector.selectNext(messages, false).get().name()); + } + + @Test + @DisplayName("Sequential selector should advance index when advanceIndex=true") + void shouldAdvanceIndexWhenTrue() { + SequentialMessageSelector selector = new SequentialMessageSelector(); + List messages = List.of(msg("a"), msg("b")); + + assertEquals("a", selector.selectNext(messages, true).get().name()); + assertEquals("b", selector.selectNext(messages, true).get().name()); + assertEquals("a", selector.selectNext(messages, true).get().name()); // wraps + } + + @Test + @DisplayName("Sequential selector should reset index after reaching threshold") + void shouldResetAfterThreshold() { + SequentialMessageSelector selector = new SequentialMessageSelector(); + List messages = List.of(msg("a")); + + // simulate state: index = threshold - 1 + for (int i = 0; i < 1_000_000_000 - 1; i++) { + selector.selectNext(messages, true); + } + + // next call should reset to 0, still producing valid output + ScheduledMessage result = selector.selectNext(messages, true).orElseThrow(); + + assertEquals("a", result.name()); + } +} + diff --git a/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentsTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentsTest.java new file mode 100644 index 0000000..7523efd --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureComponentsTest.java @@ -0,0 +1,61 @@ +package com.github.imdmk.automessage.shared.adventure; + +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/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatterTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatterTest.java new file mode 100644 index 0000000..d64e0bf --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventureFormatterTest.java @@ -0,0 +1,73 @@ +package com.github.imdmk.automessage.shared.adventure; + +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/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholdersTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholdersTest.java new file mode 100644 index 0000000..e61d49f --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/shared/adventure/AdventurePlaceholdersTest.java @@ -0,0 +1,81 @@ +package com.github.imdmk.automessage.shared.adventure; + +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/automessage-plugin/build.gradle.kts b/automessage-plugin/build.gradle.kts new file mode 100644 index 0000000..9c5a9c1 --- /dev/null +++ b/automessage-plugin/build.gradle.kts @@ -0,0 +1,54 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("net.minecrell.plugin-yml.bukkit") version "0.6.0" + id("com.gradleup.shadow") version "9.2.1" +} + +dependencies { + implementation(project(":automessage-core")) +} + +tasks.build { + dependsOn(tasks.test) + dependsOn(tasks.shadowJar) +} + +tasks.withType { + archiveFileName.set("AutoMessage v${project.version} (MC 1.21.x).jar") + + mergeServiceFiles() + + exclude( + "META-INF/*.SF", + "META-INF/*.DSA", + "META-INF/*.RSA", + "module-info.class", + "org/intellij/lang/annotations/**", + "org/jetbrains/annotations/**" + ) + + val relocationPrefix = "com.github.imdmk.automessage.lib" + listOf( + "com.eternalcode.multification", + "dev.rollczi.litecommands", + "eu.okaeri.configs", + "net.kyori", + "org.bstats", + "org.yaml.snakeyaml", + ).forEach { pkg -> + relocate(pkg, "$relocationPrefix.$pkg") + } + + minimize() +} + +bukkit { + name = "AutoMessage" + version = project.version.toString() + apiVersion = "1.21" + main = "com.github.imdmk.automessage.AutoMessagePluginLoader" + author = "imDMK (dominiks8318@gmail.com)" + description = "High-performance plugin for fully customizable automatic server-wide broadcasts." + website = "https://github.com/imDMK/AutoMessage" +} \ No newline at end of file diff --git a/automessage-plugin/src/main/java/com/github/imdmk/automessage/AutoMessagePluginLoader.java b/automessage-plugin/src/main/java/com/github/imdmk/automessage/AutoMessagePluginLoader.java new file mode 100644 index 0000000..35128ea --- /dev/null +++ b/automessage-plugin/src/main/java/com/github/imdmk/automessage/AutoMessagePluginLoader.java @@ -0,0 +1,25 @@ +package com.github.imdmk.automessage; + +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public final class AutoMessagePluginLoader extends JavaPlugin { + + private volatile AutoMessagePlugin pluginCore; + + @Override + public void onEnable() { + final Plugin plugin = this; + + pluginCore = new AutoMessagePlugin(plugin); + pluginCore.enable(new DefaultPluginSettings()); + } + + @Override + public void onDisable() { + if (pluginCore != null) { + pluginCore.disable(); + pluginCore = null; + } + } +} diff --git a/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java b/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java new file mode 100644 index 0000000..b061706 --- /dev/null +++ b/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java @@ -0,0 +1,20 @@ +package com.github.imdmk.automessage; + +import com.github.imdmk.automessage.config.ConfigSection; +import com.github.imdmk.automessage.scheduled.ScheduledMessageConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; +import com.github.imdmk.automessage.shared.message.MessageConfig; + +import java.util.List; + +public final class DefaultPluginSettings implements PluginSettings { + + @Override + public List> configs() { + return List.of( + MessageConfig.class, + ScheduledMessageConfig.class, + MessageDispatcherConfig.class + ); + } +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index ba3a22b..0000000 --- a/build.gradle +++ /dev/null @@ -1,95 +0,0 @@ -plugins { - id("java-library") - - id("com.github.johnrengelman.shadow") version "8.1.1" - id("net.minecrell.plugin-yml.bukkit") version "0.6.0" - id("checkstyle") -} - -group = "com.github.imdmk.automessage" -version = "1.0.4" - -compileJava.options.encoding = "UTF-8" -compileTestJava.options.encoding = "UTF-8" - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -repositories { - mavenCentral() - - maven { url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" } - maven { url = "https://storehouse.okaeri.eu/repository/maven-public/" } - maven { url = "https://repo.panda-lang.org/releases" } - maven { url = "https://repo.eternalcode.pl/releases" } -} - -dependencies { - compileOnly("org.spigotmc:spigot-api:1.17-R0.1-SNAPSHOT") - - implementation("eu.okaeri:okaeri-configs-yaml-snakeyaml:5.0.9") - implementation("eu.okaeri:okaeri-configs-serdes-commons:5.0.9") - - implementation("net.kyori:adventure-platform-bukkit:4.4.0") - implementation("net.kyori:adventure-text-minimessage:4.23.0") - - implementation("dev.rollczi:litecommands-bukkit:3.10.6") - implementation("dev.rollczi:litecommands-annotations:3.10.6") - - implementation("com.eternalcode:multification-bukkit:1.2.1") - implementation("com.eternalcode:multification-okaeri:1.2.1") - implementation("com.eternalcode:gitcheck:1.0.0") - - implementation("org.bstats:bstats-bukkit:3.1.0") - - testImplementation platform("org.junit:junit-bom:5.13.2") - testImplementation("org.junit.jupiter:junit-jupiter:5.13.2") -} - -bukkit { - name = "AutoMessage" - version = "${project.version}" - apiVersion = "1.17" - main = "com.github.imdmk.automessage.AutoMessagePlugin" - author = "imDMK" - description = "Advanced plugin for sending automatic chat, bossbar, and title messages to players." - website = "https://github.com/imDMK/AutoMessage" -} - -test { - useJUnitPlatform() -} - -checkstyle { - toolVersion = "10.21.0" - configFile = file("checkstyle.xml") -} - -shadowJar { - archiveFileName.set("${project.name} v${project.version}.jar") - - dependsOn("checkstyleMain") - dependsOn("checkstyleTest") - dependsOn("test") - - exclude( - "org/intellij/lang/annotations/**", - "org/jetbrains/annotations/**", - "META-INF/**", - ) - - def prefix = "com.github.imdmk.automessage.lib" - [ - "net.kyori", - "dev.rollczi", - "org.yaml", - "org.bstats", - "org.json", - "com.eternalcode", - "eu.okaeri", - ].each { lib -> - relocate(lib, "${prefix}.${lib}") - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..88b122a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,40 @@ +apply(plugin = "java-library") + +group = "com.github.imdmk.automessage" +version = "2.0.0" + +subprojects { + version = "2.0.0" + apply(plugin = "java-library") + + repositories { + mavenCentral() + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") // Spigot + maven("https://repo.eternalcode.pl/releases") // Eternalcode + maven("https://storehouse.okaeri.eu/repository/maven-public/") // Okaeri + maven("https://repo.panda-lang.org/releases") // Litecommands + } + + extensions.configure { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + withJavadocJar() + withSourcesJar() + } + + tasks.withType().configureEach { + isFailOnError = false + + val opts = options as StandardJavadocDocletOptions + opts.addStringOption("Xdoclint:none", "-quiet") + } + + tasks.withType().configureEach { + useJUnitPlatform() + } + + tasks.withType().configureEach { + options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked", "-parameters")) + options.encoding = "UTF-8" + options.release.set(21) + } +} \ No newline at end of file diff --git a/checkstyle.xml b/checkstyle.xml deleted file mode 100644 index b46f726..0000000 --- a/checkstyle.xml +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 857f2a6..c1aa675 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,15 @@ -#Fri Apr 04 19:27:25 CEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists + +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.caching=true +org.gradle.vfs.watch=true + +org.gradle.configuration-cache=true + +systemProp.file.encoding=UTF-8 diff --git a/gradlew b/gradlew index 1b6c787..477c896 100644 --- a/gradlew +++ b/gradlew @@ -1,234 +1,89 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega \ No newline at end of file diff --git a/gradlew.bat b/gradlew.bat index 107acd3..477c896 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -86,4 +86,4 @@ exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal -:omega +:omega \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 532a25a..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'AutoMessage' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..bd3a62b --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "AutoMessage" +include("automessage-core") +include("automessage-plugin") \ No newline at end of file diff --git a/src/main/java/com/github/imdmk/automessage/AutoMessage.java b/src/main/java/com/github/imdmk/automessage/AutoMessage.java deleted file mode 100644 index 3ee9f4b..0000000 --- a/src/main/java/com/github/imdmk/automessage/AutoMessage.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.github.imdmk.automessage; - -import com.eternalcode.multification.notice.Notice; -import com.github.imdmk.automessage.configuration.ConfigurationManager; -import com.github.imdmk.automessage.configuration.PluginConfig; -import com.github.imdmk.automessage.feature.command.builder.configuration.CommandConfig; -import com.github.imdmk.automessage.feature.command.builder.configuration.CommandEditor; -import com.github.imdmk.automessage.feature.command.builder.handler.MissingPermissionHandler; -import com.github.imdmk.automessage.feature.command.builder.handler.UsageHandler; -import com.github.imdmk.automessage.feature.command.builder.player.PlayerArgument; -import com.github.imdmk.automessage.feature.command.builder.player.PlayerContextual; -import com.github.imdmk.automessage.feature.command.implementation.DelayCommand; -import com.github.imdmk.automessage.feature.command.implementation.DisableCommand; -import com.github.imdmk.automessage.feature.command.implementation.DispatchCommand; -import com.github.imdmk.automessage.feature.command.implementation.EnableCommand; -import com.github.imdmk.automessage.feature.command.implementation.ReloadCommand; -import com.github.imdmk.automessage.feature.message.MessageConfig; -import com.github.imdmk.automessage.feature.message.MessageResultHandler; -import com.github.imdmk.automessage.feature.message.MessageService; -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNoticeArgument; -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNoticeConfig; -import com.github.imdmk.automessage.feature.message.auto.dispatcher.AutoMessageDispatcher; -import com.github.imdmk.automessage.feature.message.auto.eligibility.AutoMessageEligibilityEvaluator; -import com.github.imdmk.automessage.feature.message.auto.eligibility.DefaultEligibilityEvaluator; -import com.github.imdmk.automessage.feature.update.UpdateController; -import com.github.imdmk.automessage.feature.update.UpdateService; -import com.github.imdmk.automessage.scheduler.BukkitTaskScheduler; -import com.github.imdmk.automessage.scheduler.TaskScheduler; -import com.google.common.base.Stopwatch; -import dev.rollczi.litecommands.LiteCommands; -import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; -import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bstats.bukkit.Metrics; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; -import java.util.stream.Stream; - -/** - * Core plugin initializer responsible for setting up configuration, - * services, commands, schedulers, and external integrations. - */ -class AutoMessage { - - private final Server server; - private final Logger logger; - - private final ConfigurationManager configurationManager; - private final MessageService messageService; - private final TaskScheduler taskScheduler; - - private final LiteCommands liteCommands; - private final Metrics metrics; - - AutoMessage(@NotNull Plugin plugin) { - Objects.requireNonNull(plugin, "plugin cannot be null."); - Stopwatch stopwatch = Stopwatch.createStarted(); - - this.server = plugin.getServer(); - this.logger = plugin.getLogger(); - - /* Configuration */ - this.configurationManager = new ConfigurationManager(this.logger, plugin.getDataFolder()); - - PluginConfig pluginConfig = this.configurationManager.create(PluginConfig.class); - MessageConfig messageConfig = this.configurationManager.create(MessageConfig.class); - AutoMessageNoticeConfig autoMessageNoticeConfig = this.configurationManager.create(AutoMessageNoticeConfig.class); - CommandConfig commandConfig = this.configurationManager.create(CommandConfig.class); - - /* Services */ - this.messageService = new MessageService(messageConfig, BukkitAudiences.create(plugin), MiniMessage.miniMessage()); - UpdateService updateService = new UpdateService(pluginConfig, plugin.getDescription()); - - /* Scheduler */ - this.taskScheduler = new BukkitTaskScheduler(plugin, this.server); - - /* Dispatcher */ - AutoMessageEligibilityEvaluator eligibilityEvaluator = new DefaultEligibilityEvaluator(); - - AutoMessageDispatcher autoMessageDispatcher = new AutoMessageDispatcher(this.server, this.configurationManager, autoMessageNoticeConfig, this.messageService, this.taskScheduler, eligibilityEvaluator); - autoMessageDispatcher.schedule(); - - /* Controllers */ - Stream.of( - new UpdateController(this.logger, pluginConfig, this.messageService, updateService, this.taskScheduler) - ).forEach(listener -> this.server.getPluginManager().registerEvents(listener, plugin)); - - /* LiteCommands */ - this.liteCommands = LiteBukkitFactory.builder("AutoMessage", plugin, this.server) - .argument(Player.class, new PlayerArgument(this.server, messageConfig)) - .argument(AutoMessageNotice.class, new AutoMessageNoticeArgument(messageConfig, autoMessageNoticeConfig)) - - .context(Player.class, new PlayerContextual()) - .result(Notice.class, new MessageResultHandler(this.messageService)) - - .missingPermission(new MissingPermissionHandler(this.messageService)) - .invalidUsage(new UsageHandler(this.messageService)) - - .commands( - new DelayCommand(this.messageService, autoMessageDispatcher), - new DisableCommand(this.messageService, autoMessageDispatcher), - new DispatchCommand(this.server, this.messageService, autoMessageDispatcher, eligibilityEvaluator), - new EnableCommand(this.messageService, autoMessageDispatcher), - new ReloadCommand(this.logger, this.configurationManager, this.messageService) - ) - - .editorGlobal(new CommandEditor(this.logger, commandConfig)) - - .build(); - - /* Metrics */ - this.metrics = new Metrics(plugin, AutoMessagePlugin.METRICS_SERVICE_ID); - - this.logger.info("Enabled plugin in " + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms."); - } - - /** - * Gracefully shuts down all plugin components and resources. - * Called during plugin disable. - */ - void disable() { - this.configurationManager.shutdown(); - this.messageService.close(); - this.liteCommands.unregister(); - this.metrics.shutdown(); - this.taskScheduler.shutdown(); - - this.logger.info("Successfully disabled plugin."); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java b/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java deleted file mode 100644 index f3f45e2..0000000 --- a/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.automessage; - -import org.bukkit.plugin.java.JavaPlugin; - -public class AutoMessagePlugin extends JavaPlugin { - - /** bStats Metrics service ID for reporting plugin statistics */ - public static final int METRICS_SERVICE_ID = 19487; - - private AutoMessage autoMessage; - - @Override - public void onEnable() { - this.autoMessage = new AutoMessage(this); - } - - @Override - public void onDisable() { - this.autoMessage.disable(); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/configuration/ConfigSection.java b/src/main/java/com/github/imdmk/automessage/configuration/ConfigSection.java deleted file mode 100644 index 1e8f5b0..0000000 --- a/src/main/java/com/github/imdmk/automessage/configuration/ConfigSection.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.imdmk.automessage.configuration; - -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 getSerdesPack(); - - public abstract @NotNull String getFileName(); -} diff --git a/src/main/java/com/github/imdmk/automessage/configuration/ConfigurationLoadException.java b/src/main/java/com/github/imdmk/automessage/configuration/ConfigurationLoadException.java deleted file mode 100644 index 3cb5d18..0000000 --- a/src/main/java/com/github/imdmk/automessage/configuration/ConfigurationLoadException.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.imdmk.automessage.configuration; - -public final class ConfigurationLoadException extends RuntimeException { - public ConfigurationLoadException(Throwable cause) { - super("Failed to load configuration", cause); - } - - public ConfigurationLoadException(String message) { - super(message); - } - - public ConfigurationLoadException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/main/java/com/github/imdmk/automessage/configuration/ConfigurationManager.java b/src/main/java/com/github/imdmk/automessage/configuration/ConfigurationManager.java deleted file mode 100644 index 3076d24..0000000 --- a/src/main/java/com/github/imdmk/automessage/configuration/ConfigurationManager.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.github.imdmk.automessage.configuration; - -import eu.okaeri.configs.ConfigManager; -import eu.okaeri.configs.OkaeriConfig; -import eu.okaeri.configs.exception.OkaeriException; -import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; -import org.jetbrains.annotations.NotNull; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.Constructor; -import org.yaml.snakeyaml.representer.Representer; -import org.yaml.snakeyaml.resolver.Resolver; - -import java.io.File; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class ConfigurationManager { - - private final Set configs = ConcurrentHashMap.newKeySet(); - - private final Logger logger; - private final File dataFolder; - private final ExecutorService executor; - - public ConfigurationManager(@NotNull Logger logger, @NotNull File dataFolder) { - this.logger = Objects.requireNonNull(logger, "logger cannot be null"); - this.dataFolder = Objects.requireNonNull(dataFolder, "dataFolder cannot be null"); - this.executor = Executors.newSingleThreadExecutor(); - } - - public T create(@NotNull Class config) { - T configFile = ConfigManager.create(config); - File file = new File(this.dataFolder, configFile.getFileName()); - - YamlSnakeYamlConfigurer yamlSnakeYamlConfigurer = this.createYamlSnakeYamlConfigurer(); - - configFile.withConfigurer(yamlSnakeYamlConfigurer); - configFile.withSerdesPack(configFile.getSerdesPack()); - configFile.withBindFile(file); - configFile.withRemoveOrphans(true); - configFile.saveDefaults(); - configFile.load(true); - - this.configs.add(configFile); - - return configFile; - } - - private @NotNull YamlSnakeYamlConfigurer createYamlSnakeYamlConfigurer() { - LoaderOptions loaderOptions = new LoaderOptions(); - Constructor constructor = new Constructor(loaderOptions); - - DumperOptions dumperOptions = new DumperOptions(); - dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.AUTO); - dumperOptions.setIndent(2); - dumperOptions.setSplitLines(false); - - Representer representer = new CustomRepresenter(dumperOptions); - Resolver resolver = new Resolver(); - - Yaml yaml = new Yaml(constructor, representer, dumperOptions, loaderOptions, resolver); - return new YamlSnakeYamlConfigurer(yaml); - } - - public @NotNull CompletableFuture reloadAll() { - return CompletableFuture.runAsync(this::loadAll, this.executor); - } - - private void loadAll() { - this.configs.forEach(this::load); - } - - public void load(@NotNull OkaeriConfig config) { - try { - config.load(true); - } - catch (OkaeriException exception) { - this.logger.log(Level.SEVERE, "Failed to load config: " + config.getClass().getSimpleName(), exception); - throw new ConfigurationLoadException(exception); - } - } - - public @NotNull CompletableFuture save(@NotNull OkaeriConfig config) { - return CompletableFuture.runAsync(() -> { - try { - config.save(); - } - catch (OkaeriException exception) { - this.logger.log(Level.SEVERE, "Failed to save config: " + config.getClass().getSimpleName(), exception); - throw new ConfigurationLoadException(exception); - } - }, this.executor); - } - - public void shutdown() { - this.logger.info("Shutting down ConfigurationManager executor"); - this.executor.shutdownNow(); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/configuration/PluginConfig.java b/src/main/java/com/github/imdmk/automessage/configuration/PluginConfig.java deleted file mode 100644 index 524fe7c..0000000 --- a/src/main/java/com/github/imdmk/automessage/configuration/PluginConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.imdmk.automessage.configuration; - -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import eu.okaeri.configs.serdes.commons.SerdesCommons; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; - -public class PluginConfig extends ConfigSection { - - @Comment("# Check for plugin update and send notification after administrator join to server?") - public boolean checkUpdate = true; - - @Comment("# How often should the plugin check for updates? Recommended value: 1 day") - public Duration updateInterval = Duration.ofDays(1); - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> { - registry.register(new SerdesCommons()); - }; - } - - @Override - public @NotNull String getFileName() { - return "pluginConfig.yml"; - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/Command.java b/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/Command.java deleted file mode 100644 index 738e2c0..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/Command.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.github.imdmk.automessage.feature.command.builder.configuration; - -import eu.okaeri.configs.OkaeriConfig; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class Command extends OkaeriConfig { - - public String name; - - public boolean enabled = true; - - public List aliases = new ArrayList<>(); - public List permissions = new ArrayList<>(); - - public Map subCommands = new HashMap<>(); - - public Command() {} - - public Command(@NotNull String name) { - this.name = name; - } - - public Command(@NotNull String name, @NotNull List aliases) { - this.name = name; - this.aliases = aliases; - } - - public Command(@NotNull String name, @NotNull List aliases, @NotNull List permissions) { - this.name = name; - this.aliases = aliases; - this.permissions = permissions; - } - - public Command(@NotNull String name, boolean enabled, @NotNull List aliases, @NotNull List permissions) { - this.name = name; - this.enabled = enabled; - this.aliases = aliases; - this.permissions = permissions; - } - - public Command( - @NotNull String name, - boolean enabled, - @NotNull List aliases, - @NotNull List permissions, - @NotNull Map subCommands - ) { - this.name = name; - this.enabled = enabled; - this.aliases = aliases; - this.permissions = permissions; - this.subCommands = subCommands; - } - - public @NotNull String name() { - return this.name; - } - - public boolean isEnabled() { - return this.enabled; - } - - public @NotNull List aliases() { - return Collections.unmodifiableList(this.aliases); - } - - public @NotNull List permissions() { - return Collections.unmodifiableList(this.permissions); - } - - public @NotNull Map subCommands() { - return Collections.unmodifiableMap(this.subCommands); - } -} - diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandConfig.java b/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandConfig.java deleted file mode 100644 index 861251f..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandConfig.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.github.imdmk.automessage.feature.command.builder.configuration; - -import com.github.imdmk.automessage.configuration.ConfigSection; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.annotation.Header; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -@Header({ - "#", - "# This file allows you to configure commands.", - "#" -}) -public class CommandConfig extends ConfigSection { - - @Comment("# Specifies to enable command editor feature.") - public boolean enabled = false; - - @Comment({ - "# This allows you to globally edit commands.", - "# For example, if you want to change the command name from /my-furnaces to /furnaces,", - "# you can configure it like this:", - "#", - "# commands:", - "# :", - "# name: \"\"", - "# enabled: true/false", - "# aliases:", - "# - \"\"", - "# permissions:", - "# - \"\"", - "# subCommands:", - "# :", - "# name: ", - "# enabled: true/false", - "# aliases:", - "# - \"\"", - "# permissions:", - "# - \"\"", - }) - public Map commands = Map.of( - "automessage", new Command( - "automessage", - true, - List.of("am"), - List.of(), - Map.of() - ) - ); - - public Optional getCommand(String name) { - return Optional.ofNullable(this.commands.get(name)); - } - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> {}; - } - - @Override - public @NotNull String getFileName() { - return "commandConfig.yml"; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandEditor.java b/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandEditor.java deleted file mode 100644 index 1246581..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/CommandEditor.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.imdmk.automessage.feature.command.builder.configuration; - -import dev.rollczi.litecommands.command.builder.CommandBuilder; -import dev.rollczi.litecommands.editor.Editor; -import dev.rollczi.litecommands.meta.Meta; -import dev.rollczi.litecommands.permission.PermissionSet; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; -import java.util.logging.Logger; - -public class CommandEditor implements Editor { - - private final Logger logger; - private final CommandConfig configuration; - - public CommandEditor(@NotNull Logger logger, @NotNull CommandConfig configuration) { - this.logger = logger; - this.configuration = configuration; - } - - @Override - public CommandBuilder edit(CommandBuilder context) { - if (!this.configuration.enabled) { - return context; - } - - return this.configuration.getCommand(context.name()) - .map(command -> { - CommandBuilder updated = this.updateCommand(context, command); - return this.updateSubCommand(updated, command.subCommands()); - }) - .map(command -> { - this.logger.info("Edited command " + command.name() + " via configuration."); - return command; - }) - .orElse(context); - } - - private @NotNull CommandBuilder updateCommand(@NotNull CommandBuilder context, @NotNull Command command) { - return context - .name(command.name()) - .aliases(command.aliases()) - .applyMeta(meta -> meta.list(Meta.PERMISSIONS, permissions -> permissions.add(new PermissionSet(command.permissions())))) - .enabled(command.isEnabled()); - } - - private @NotNull CommandBuilder updateSubCommand(@NotNull CommandBuilder context, @NotNull Map subCommands) { - for (Map.Entry entry : subCommands.entrySet()) { - String id = entry.getKey(); - SubCommand sub = entry.getValue(); - - context = context.editChild(id, child -> child - .name(sub.name()) - .aliases(sub.aliases()) - .applyMeta(meta -> meta.list(Meta.PERMISSIONS, permissions -> permissions.addAll(new PermissionSet(sub.permissions())))) - .enabled(sub.isEnabled())); - } - - return context; - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/SubCommand.java b/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/SubCommand.java deleted file mode 100644 index 6f64ead..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/configuration/SubCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.imdmk.automessage.feature.command.builder.configuration; - -import eu.okaeri.configs.OkaeriConfig; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class SubCommand extends OkaeriConfig { - - public String name; - public boolean enabled; - - public List aliases = new ArrayList<>(); - public List permissions = new ArrayList<>(); - - public SubCommand() {} - - public SubCommand(@NotNull String name, boolean enabled, @NotNull List aliases, @NotNull List permissions) { - this.name = name; - this.enabled = enabled; - this.aliases = aliases; - this.permissions = permissions; - } - - public @NotNull String name() { - return this.name; - } - - public boolean isEnabled() { - return this.enabled; - } - - public @NotNull List aliases() { - return Collections.unmodifiableList(this.aliases); - } - - public @NotNull List permissions() { - return Collections.unmodifiableList(this.permissions); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/UsageHandler.java b/src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/UsageHandler.java deleted file mode 100644 index 87fc371..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/handler/UsageHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.imdmk.automessage.feature.command.builder.handler; - -import com.github.imdmk.automessage.feature.message.MessageService; -import dev.rollczi.litecommands.handler.result.ResultHandlerChain; -import dev.rollczi.litecommands.invalidusage.InvalidUsage; -import dev.rollczi.litecommands.invalidusage.InvalidUsageHandler; -import dev.rollczi.litecommands.invocation.Invocation; -import dev.rollczi.litecommands.schematic.Schematic; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public class UsageHandler implements InvalidUsageHandler { - - private final MessageService messageService; - - public UsageHandler(@NotNull MessageService messageService) { - this.messageService = Objects.requireNonNull(messageService, "message service cannot be null"); - } - - @Override - public void handle(Invocation invocation, InvalidUsage result, ResultHandlerChain chain) { - CommandSender sender = invocation.sender(); - Schematic schematic = result.getSchematic(); - - if (schematic.isOnlyFirst()) { - this.messageService.create() - .viewer(sender) - .notice(notice -> notice.invalidCommandUsage) - .placeholder("{USAGE}", schematic.first()) - .send(); - return; - } - - this.messageService.create() - .viewer(sender) - .notice(notice -> notice.usageHeader) - .send(); - - for (String scheme : schematic.all()) { - this.messageService.create() - .viewer(sender) - .notice(notice -> notice.usageEntry) - .placeholder("{USAGE}", scheme) - .send(); - } - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerArgument.java b/src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerArgument.java deleted file mode 100644 index b5254ac..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerArgument.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.imdmk.automessage.feature.command.builder.player; - -import com.github.imdmk.automessage.feature.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 dev.rollczi.litecommands.suggestion.SuggestionContext; -import dev.rollczi.litecommands.suggestion.SuggestionResult; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.Optional; - -public class PlayerArgument extends ArgumentResolver { - - private final Server server; - private final MessageConfig messageConfig; - - public PlayerArgument(@NotNull Server server, @NotNull MessageConfig messageConfig) { - this.server = Objects.requireNonNull(server, "server cannot be null"); - this.messageConfig = Objects.requireNonNull(messageConfig, "messageConfiguration cannot be null"); - } - - @Override - protected ParseResult parse(Invocation invocation, Argument context, String argument) { - return Optional.ofNullable(this.server.getPlayer(argument)) - .map(ParseResult::success) - .orElseGet(() -> ParseResult.failure(this.messageConfig.playerNotFound)); - } - - @Override - public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { - return this.server.getOnlinePlayers().stream() - .map(Player::getName) - .collect(SuggestionResult.collector()); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerContextual.java b/src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerContextual.java deleted file mode 100644 index d2e99d1..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/builder/player/PlayerContextual.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.imdmk.automessage.feature.command.builder.player; - - -import dev.rollczi.litecommands.context.ContextProvider; -import dev.rollczi.litecommands.context.ContextResult; -import dev.rollczi.litecommands.invocation.Invocation; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -public class PlayerContextual implements ContextProvider { - - @Override - public ContextResult provide(Invocation invocation) { - if (invocation.sender() instanceof Player player) { - return ContextResult.ok(() -> player); - } - - return ContextResult.error("Only player can use this command."); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DelayCommand.java b/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DelayCommand.java deleted file mode 100644 index 54b5e65..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DelayCommand.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.imdmk.automessage.feature.command.implementation; - -import com.github.imdmk.automessage.feature.message.MessageService; -import com.github.imdmk.automessage.feature.message.auto.dispatcher.AutoMessageDispatcher; -import com.github.imdmk.automessage.util.DurationUtil; -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 dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.util.Objects; - -@Command(name = "automessage delay") -@Permission("command.automessage.delay") -public class DelayCommand { - - private final MessageService messageService; - private final AutoMessageDispatcher autoMessageDispatcher; - - public DelayCommand( - @NotNull MessageService messageService, - @NotNull AutoMessageDispatcher autoMessageDispatcher - ) { - this.messageService = Objects.requireNonNull(Objects.requireNonNull(messageService, "messageService cannot be null")); - this.autoMessageDispatcher = Objects.requireNonNull(autoMessageDispatcher, "autoMessageDispatcher cannot be null"); - } - - @Execute - void show(@Context CommandSender sender) { - this.messageService.create() - .viewer(sender) - .notice(notice -> notice.autoMessageDelay) - .placeholder("{DELAY}", DurationUtil.format(this.autoMessageDispatcher.getDelay())) - .send(); - } - - @Execute(name = "set") - void set(@Context CommandSender sender, @Arg Duration delay) { - this.autoMessageDispatcher.changeDelay(DurationUtil.toTicks(delay)); - - this.messageService.create() - .viewer(sender) - .notice(notice -> notice.autoMessageDelayChange) - .placeholder("{DELAY}", DurationUtil.format(delay)) - .send(); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DisableCommand.java b/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DisableCommand.java deleted file mode 100644 index a66cb10..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DisableCommand.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.imdmk.automessage.feature.command.implementation; - -import com.github.imdmk.automessage.feature.message.MessageService; -import com.github.imdmk.automessage.feature.message.auto.dispatcher.AutoMessageDispatcher; -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 java.util.Objects; - -@Command(name = "automessage disable") -@Permission("command.automessage.disable") -public class DisableCommand { - - private final MessageService messageService; - private final AutoMessageDispatcher dispatcher; - - public DisableCommand(@NotNull MessageService messageService, @NotNull AutoMessageDispatcher dispatcher) { - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher cannot be null"); - } - - @Execute - void enable(@Context CommandSender sender) { - if (!this.dispatcher.isEnabled()) { - this.messageService.send(sender, notice -> notice.autoMessageAlreadyDisabled); - return; - } - - this.dispatcher.setEnabled(false); - this.dispatcher.cancel(); // Cancel task - - this.messageService.send(sender, notice -> notice.autoMessageDisable); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DispatchCommand.java b/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DispatchCommand.java deleted file mode 100644 index f737a33..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/DispatchCommand.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.imdmk.automessage.feature.command.implementation; - -import com.github.imdmk.automessage.feature.message.MessageService; -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import com.github.imdmk.automessage.feature.message.auto.dispatcher.AutoMessageDispatcher; -import com.github.imdmk.automessage.feature.message.auto.eligibility.AutoMessageEligibilityEvaluator; -import com.github.imdmk.automessage.feature.message.auto.selector.AutoMessageSelector; -import com.github.imdmk.automessage.feature.message.auto.selector.AutoMessageSelectorFactory; -import com.github.imdmk.automessage.feature.message.auto.selector.AutoMessageSelectorMode; -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 dev.rollczi.litecommands.annotations.permission.Permission; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -@Command(name = "automessage dispatch") -@Permission("command.automessage.dispatch") -public class DispatchCommand { - - private final Server server; - private final MessageService messageService; - private final AutoMessageDispatcher dispatcher; - private final AutoMessageEligibilityEvaluator evaluator; - private final AutoMessageSelector randomSelector; - - public DispatchCommand( - @NotNull Server server, - @NotNull MessageService messageService, - @NotNull AutoMessageDispatcher dispatcher, - @NotNull AutoMessageEligibilityEvaluator evaluator - ) { - this.server = Objects.requireNonNull(server, "server cannot be null"); - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher cannot be null"); - this.evaluator = Objects.requireNonNull(evaluator, "evaluator cannot be null"); - this.randomSelector = AutoMessageSelectorFactory.create(AutoMessageSelectorMode.RANDOM, evaluator); - } - - @Execute(name = "random") - void random(@Context CommandSender sender) { - this.server.getOnlinePlayers().forEach(player -> this.dispatcher.dispatch(player, this.randomSelector)); - this.messageService.send(sender, notice -> notice.autoMessageRandomDispatched); - } - - @Execute(name = "random") - void random(@Context CommandSender sender, @Arg Player player) { - this.dispatcher.dispatch(player, this.randomSelector); - this.messageService.send(sender, notice -> notice.autoMessageRandomDispatched); - } - - @Execute(name = "select") - void select(@Context CommandSender sender, @Arg AutoMessageNotice autoMessage) { - this.server.getOnlinePlayers() - .stream() - .filter(player -> this.evaluator.canReceive(player, autoMessage)) - .forEach(player -> this.dispatcher.dispatch(player, autoMessage)); - - this.messageService.send(sender, notice -> notice.autoMessageSelectedDispatched); - } - - @Execute(name = "select") - void select(@Context CommandSender sender, @Arg AutoMessageNotice autoMessage, @Arg Player player) { - if (!this.evaluator.canReceive(player, autoMessage)) { - this.messageService.send(sender, notice -> notice.autoMessageSelectedCannotReceive); - return; - } - - this.dispatcher.dispatch(player, autoMessage); - this.messageService.send(sender, notice -> notice.autoMessageSelectedDispatched); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/EnableCommand.java b/src/main/java/com/github/imdmk/automessage/feature/command/implementation/EnableCommand.java deleted file mode 100644 index ad1ab59..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/EnableCommand.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.imdmk.automessage.feature.command.implementation; - -import com.github.imdmk.automessage.feature.message.MessageService; -import com.github.imdmk.automessage.feature.message.auto.dispatcher.AutoMessageDispatcher; -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 java.util.Objects; - -@Command(name = "automessage enable") -@Permission("command.automessage.enable") -public class EnableCommand { - - private final MessageService messageService; - private final AutoMessageDispatcher dispatcher; - - public EnableCommand(@NotNull MessageService messageService, @NotNull AutoMessageDispatcher dispatcher) { - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher cannot be null"); - } - - @Execute - void enable(@Context CommandSender sender) { - if (this.dispatcher.isEnabled()) { - this.messageService.send(sender, notice -> notice.autoMessageAlreadyEnabled); - return; - } - - this.dispatcher.setEnabled(true); - this.dispatcher.schedule(); // Reset task - - this.messageService.send(sender, notice -> notice.autoMessageEnable); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/ReloadCommand.java b/src/main/java/com/github/imdmk/automessage/feature/command/implementation/ReloadCommand.java deleted file mode 100644 index 47851de..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/command/implementation/ReloadCommand.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.imdmk.automessage.feature.command.implementation; - -import com.github.imdmk.automessage.configuration.ConfigurationManager; -import com.github.imdmk.automessage.feature.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.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.logging.Level; -import java.util.logging.Logger; - -@Command(name = "automessage reload") -@Permission("command.automessage.reload") -public class ReloadCommand { - - private final Logger logger; - private final ConfigurationManager configurationManager; - private final MessageService messageService; - - public ReloadCommand( - @NotNull Logger logger, - @NotNull ConfigurationManager configurationManager, - @NotNull MessageService messageService - ) { - this.logger = Objects.requireNonNull(logger, "logger cannot be null"); - this.configurationManager = Objects.requireNonNull(configurationManager, "configurationManager cannot be null"); - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - } - - @Execute - void reload(@Context CommandSender sender) { - this.configurationManager.reloadAll() - .thenAccept(v -> this.messageService.send(sender, notice -> notice.reload)) - .exceptionally(throwable -> { - this.messageService.send(sender, notice -> notice.reloadError); - this.logger.log(Level.SEVERE, "Failed to reload plugin configuration.", throwable); - return null; - }); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/MessageConfig.java b/src/main/java/com/github/imdmk/automessage/feature/message/MessageConfig.java deleted file mode 100644 index 9681ac8..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/MessageConfig.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.github.imdmk.automessage.feature.message; - -import com.eternalcode.multification.notice.Notice; -import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults; -import com.eternalcode.multification.okaeri.MultificationSerdesPack; -import com.github.imdmk.automessage.configuration.ConfigSection; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -public class MessageConfig extends ConfigSection { - - @Comment({ - "# Sent when the automatic message delay is changed", - "# {DELAY} - New delay value" - }) - public Notice autoMessageDelayChange = Notice.chat("Changed auto message delay to {DELAY}."); - - @Comment({ - "# Sent when querying the current automatic message delay", - "# {DELAY} - Current delay value" - }) - public Notice autoMessageDelay = Notice.chat("Current auto message delay is {DELAY}."); - - @Comment("# Shown when auto messages are successfully enabled.") - public Notice autoMessageEnable = Notice.chat("Enabled auto messages!"); - - @Comment("# Shown when auto messages are already enabled.") - public Notice autoMessageAlreadyEnabled = Notice.chat("Auto messages are currently enabled!"); - - @Comment("# Shown when auto messages are successfully disabled.") - public Notice autoMessageDisable = Notice.chat("Disabled auto messages!"); - - @Comment("# Sent when automatic messages are already disabled") - public Notice autoMessageAlreadyDisabled = Notice.chat("Auto messages are already disabled!"); - - @Comment("# Sent when a random automatic message is dispatched") - public Notice autoMessageRandomDispatched = Notice.chat("Dispatched a random auto message."); - - @Comment("# Sent when a selected automatic message is dispatched") - public Notice autoMessageSelectedDispatched = Notice.chat("Dispatched a selected auto message."); - - @Comment({ - "# Sent when the selected target player cannot receive the selected message", - "# Typically due to insufficient group or permission as defined in the configuration" - }) - public Notice autoMessageSelectedCannotReceive = Notice.chat("Selected player cannot receive the selected auto message (e.g. missing required group or permission)."); - - @Comment("# Sent when the provided auto message name does not exist") - public Notice autoMessageNotFound = Notice.chat("Auto message with the given name not found."); - - @Comment({ - "# Sent when a command is used incorrectly", - "# {USAGE} - Correct command usage" - }) - public Notice invalidCommandUsage = Notice.chat("Invalid usage: {USAGE}"); - - @Comment("# Header for multiple valid usages of a command") - public Notice usageHeader = Notice.chat("Invalid usage:"); - - @Comment({ - "# Entry in the list of valid usages", - "# {USAGE} - Correct command usage" - }) - public Notice usageEntry = Notice.chat("- {USAGE}"); - - @Comment({ - "# Sent when command sender lacks required permissions", - "# {PERMISSIONS} - Required permission nodes" - }) - public Notice missingPermissions = Notice.chat("Missing permissions: {PERMISSIONS}"); - - @Comment("# Sent when the player could not be found") - public Notice playerNotFound = Notice.chat("Player not found."); - - @Comment("# Sent when successfully reloaded all plugin configuration files") - public Notice reload = Notice.chat("The plugin configuration files has been reloaded. May note that not all functions are reloaded."); - - @Comment("# Sent when there is an error loading plugin configuration files") - public Notice reloadError = Notice.chat("Failed to reload plugin configuration files. Please see the console."); - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> { - registry.register(new MultificationSerdesPack(NoticeResolverDefaults.createRegistry())); - }; - } - - @Override - public @NotNull String getFileName() { - return "messageConfig.yml"; - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/MessageResultHandler.java b/src/main/java/com/github/imdmk/automessage/feature/message/MessageResultHandler.java deleted file mode 100644 index 3275096..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/MessageResultHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.imdmk.automessage.feature.message; - -import com.eternalcode.multification.notice.Notice; -import dev.rollczi.litecommands.handler.result.ResultHandler; -import dev.rollczi.litecommands.handler.result.ResultHandlerChain; -import dev.rollczi.litecommands.invocation.Invocation; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public final class MessageResultHandler implements ResultHandler { - - private final MessageService messageService; - - public MessageResultHandler(@NotNull MessageService messageService) { - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - } - - @Override - public void handle(Invocation invocation, Notice result, ResultHandlerChain chain) { - this.messageService.create() - .viewer(invocation.sender()) - .notice(result) - .send(); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/MessageService.java b/src/main/java/com/github/imdmk/automessage/feature/message/MessageService.java deleted file mode 100644 index 4e3aff8..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/MessageService.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.github.imdmk.automessage.feature.message; - -import com.eternalcode.multification.adventure.AudienceConverter; -import com.eternalcode.multification.bukkit.BukkitMultification; -import com.eternalcode.multification.notice.Notice; -import com.eternalcode.multification.notice.provider.NoticeProvider; -import com.eternalcode.multification.translation.TranslationProvider; -import net.kyori.adventure.platform.AudienceProvider; -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.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public final class MessageService extends BukkitMultification { - - 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 = Objects.requireNonNull(messageConfig, "messageConfiguration cannot be null"); - this.audienceProvider = Objects.requireNonNull(audienceProvider, "audienceProvider cannot be null"); - this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage cannot be null"); - } - - @Override - protected @NotNull TranslationProvider translationProvider() { - return locale -> this.messageConfig; - } - - @Override - protected @NotNull ComponentSerializer serializer() { - return this.miniMessage; - } - - @Override - protected @NotNull AudienceConverter audienceConverter() { - return commandSender -> { - if (commandSender instanceof Player player) { - return this.audienceProvider.player(player.getUniqueId()); - } - - return this.audienceProvider.console(); - }; - } - - public void send(@NotNull CommandSender sender, @NotNull NoticeProvider notice) { - this.create().viewer(sender).notice(notice).send(); - } - - public void send(@NotNull CommandSender sender, @NotNull Notice notice) { - this.create().viewer(sender).notice(notice).send(); - } - - public void sendAsync(@NotNull CommandSender sender, @NotNull Notice notice) { - this.create().viewer(sender).notice(notice).sendAsync(); - } - - public void close() { - this.audienceProvider.close(); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNotice.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNotice.java deleted file mode 100644 index 36e9885..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNotice.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto; - -import com.eternalcode.multification.notice.Notice; -import com.github.imdmk.automessage.feature.message.auto.sound.AutoMessageSound; -import org.jetbrains.annotations.CheckReturnValue; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.Unmodifiable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Represents an auto-message notice with optional permission and group requirements. - */ -public class AutoMessageNotice { - - private final @NotNull String name; - private final @NotNull List notices; - - private final @Nullable AutoMessageSound sound; - private final @Nullable String requiredPermission; - private final @Nullable String requiredGroup; - - private final boolean ignoreAdmins; - - private AutoMessageNotice( - @NotNull String name, - @NotNull List notices, - @Nullable AutoMessageSound sound, - @Nullable String requiredPermission, - @Nullable String requiredGroup, - boolean ignoreAdmins - ) { - this.name = Objects.requireNonNull(name, "name cannot be null"); - this.notices = Objects.requireNonNull(notices, "notice cannot be null"); - this.sound = sound; - this.requiredPermission = requiredPermission; - this.requiredGroup = requiredGroup; - this.ignoreAdmins = ignoreAdmins; - } - - /** - * @return the associated AutoMessageNotice name - */ - public @NotNull String getName() { - return this.name; - } - - /** - * @return the associated {@link Notice} - */ - public @Unmodifiable List getNotices() { - return Collections.unmodifiableList(this.notices); - } - - /** - * @return the associated {@link AutoMessageSound} - */ - public Optional getSound() { - return Optional.ofNullable(this.sound); - } - - /** - * @return optional required permission - */ - public Optional getRequiredPermission() { - return Optional.ofNullable(this.requiredPermission); - } - - /** - * @return optional required group - */ - public Optional getRequiredGroup() { - return Optional.ofNullable(this.requiredGroup); - } - - /** - * @return ignoreAdmins boolean - */ - public boolean isIgnoreAdmins() { - return this.ignoreAdmins; - } - - /** - * Creates a new builder instance. - * - * @return the builder - */ - public static @NotNull Builder builder() { - return new Builder(); - } - - /** - * Builder for {@link AutoMessageNotice}. - */ - public static class Builder { - - private String name; - private List notices = new ArrayList<>(); - - private AutoMessageSound sound; - private String requiredPermission; - private String requiredGroup; - private boolean ignoreAdmins; - - @Contract("_ -> this") - @CheckReturnValue - public @NotNull Builder name(@NotNull String name) { - Objects.requireNonNull(name, "name cannot be null"); - this.name = name; - return this; - } - - @Contract("_ -> this") - @CheckReturnValue - public @NotNull Builder notice(@NotNull Notice notice) { - Objects.requireNonNull(notice, "notice cannot be null"); - this.notices.add(notice); - return this; - } - - @Contract("_ -> this") - @CheckReturnValue - public @NotNull Builder notices(@NotNull List notices) { - Objects.requireNonNull(notices, "notices cannot be null"); - this.notices = notices; - return this; - } - - @Contract("_ -> this") - @CheckReturnValue - public @NotNull Builder sound(@Nullable AutoMessageSound sound) { - this.sound = sound; - return this; - } - - @Contract("_ -> this") - @CheckReturnValue - public @NotNull Builder requiredPermission(@Nullable String requiredPermission) { - this.requiredPermission = requiredPermission; - return this; - } - - @Contract("_ -> this") - @CheckReturnValue - public @NotNull Builder requiredGroup(@Nullable String requiredGroup) { - this.requiredGroup = requiredGroup; - return this; - } - - @Contract("_ -> this") - @CheckReturnValue - public @NotNull Builder ignoreAdmins(boolean ignore) { - this.ignoreAdmins = ignore; - return this; - } - - /** - * Builds the {@link AutoMessageNotice} instance. - * - * @return new AutoMessageNotice instance - * @throws IllegalStateException if notice is not set - */ - @CheckReturnValue - public @NotNull AutoMessageNotice build() { - return new AutoMessageNotice(this.name, this.notices, this.sound, this.requiredPermission, this.requiredGroup, this.ignoreAdmins); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeArgument.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeArgument.java deleted file mode 100644 index e97749f..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeArgument.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto; - -import com.github.imdmk.automessage.feature.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 dev.rollczi.litecommands.suggestion.SuggestionContext; -import dev.rollczi.litecommands.suggestion.SuggestionResult; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public class AutoMessageNoticeArgument extends ArgumentResolver { - - private final MessageConfig messageConfig; - private final AutoMessageNoticeConfig noticeConfig; - - public AutoMessageNoticeArgument(@NotNull MessageConfig messageConfig, @NotNull AutoMessageNoticeConfig noticeConfig) { - this.messageConfig = Objects.requireNonNull(messageConfig, "messageConfig cannot be null"); - this.noticeConfig = Objects.requireNonNull(noticeConfig, "noticeConfig cannot be null"); - } - - @Override - protected ParseResult parse(Invocation invocation, Argument context, String argument) { - return this.noticeConfig.getMessage(argument) - .map(ParseResult::success) - .orElseGet(() -> ParseResult.failure(this.messageConfig.autoMessageNotFound)); - } - - @Override - public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { - return this.noticeConfig.messages.stream() - .map(AutoMessageNotice::getName) - .collect(SuggestionResult.collector()); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeConfig.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeConfig.java deleted file mode 100644 index 9bdcece..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeConfig.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto; - -import com.eternalcode.multification.notice.Notice; -import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults; -import com.eternalcode.multification.okaeri.MultificationSerdesPack; -import com.github.imdmk.automessage.configuration.ConfigSection; -import com.github.imdmk.automessage.feature.message.auto.selector.AutoMessageSelectorMode; -import com.github.imdmk.automessage.feature.message.auto.sound.AutoMessageSound; -import com.github.imdmk.automessage.feature.message.auto.sound.AutoMessageSoundSerializer; -import com.github.imdmk.automessage.feature.message.auto.sound.SoundSerializer; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import eu.okaeri.configs.serdes.commons.SerdesCommons; -import net.kyori.adventure.bossbar.BossBar; -import org.bukkit.Sound; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class AutoMessageNoticeConfig extends ConfigSection { - - @Comment("# How often should automatic messages be sent?") - public Duration delay = Duration.ofSeconds(10); - - @Comment({ - "# Defines the selection strategy for automatic messages.", - "# RANDOM - messages are chosen randomly.", - "# SEQUENTIAL - messages are sent in order." - }) - public AutoMessageSelectorMode mode = AutoMessageSelectorMode.SEQUENTIAL; - - @Comment({ - "# List of automatic messages to be dispatched.", - "# Supports different Notice types like chat, actionbar, title, boss bar.", - "# To make a new line in chat message, use \n" - }) - public List messages = Arrays.asList( - AutoMessageNotice.builder() - .name("first-message") - .notice(Notice.chat("[!] This is first announcement of automessage plugin!")) - .sound(new AutoMessageSound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1, 1)) - .build(), - - AutoMessageNotice.builder() - .name("second-message-actionbar") - .notice(Notice.actionbar("[!] This is second announcement of automessage plugin!")) - .build(), - - AutoMessageNotice.builder() - .name("third-message-title") - .notice(Notice.title("[!]", "This is third announcement!")) - .build(), - - AutoMessageNotice.builder() - .name("fourth-message-bossbar") - .notice(Notice.bossBar(BossBar.Color.RED, BossBar.Overlay.PROGRESS, Duration.ofSeconds(5L), "[!] This is fourth announcement!")) - .sound(new AutoMessageSound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1, 1)) - .build(), - - AutoMessageNotice.builder() - .name("multiple-chat-actionbar") - .notices(List.of( - Notice.chat("[!] This is multiple announcements!"), - Notice.actionbar("[!] This is multiple announcements!") - )) - .build(), - - AutoMessageNotice.builder() - .name("only-vip-permission") - .notice(Notice.chat("[!] This a announcement only for players with vip permission!")) - .requiredPermission("vip") - .ignoreAdmins(true) - .build(), - - AutoMessageNotice.builder() - .name("only-vip-group") - .notice(Notice.chat("[!] This a announcement only for players with vip group!")) - .requiredGroup("vip") - .ignoreAdmins(true) - .build() - ); - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> { - registry.register(new SerdesCommons()); - registry.register(new AutoMessageNoticeSerializer()); - registry.register(new MultificationSerdesPack(NoticeResolverDefaults.createRegistry())); - - registry.register(new AutoMessageSoundSerializer()); - registry.register(new SoundSerializer()); - }; - } - - @Override - public @NotNull String getFileName() { - return "autoMessageConfig.yml"; - } - - public Optional getMessage(@NotNull String name) { - return this.messages.stream() - .filter(notice -> notice.getName().equals(name)) - .findAny(); - } - - public void setDelay(@NotNull Duration delay) { - Objects.requireNonNull(delay, "delay cannot be null"); - this.delay = delay; - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeSerializer.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeSerializer.java deleted file mode 100644 index 612308d..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/AutoMessageNoticeSerializer.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto; - -import com.eternalcode.multification.notice.Notice; -import com.github.imdmk.automessage.feature.message.auto.sound.AutoMessageSound; -import eu.okaeri.configs.schema.GenericsDeclaration; -import eu.okaeri.configs.serdes.DeserializationData; -import eu.okaeri.configs.serdes.ObjectSerializer; -import eu.okaeri.configs.serdes.SerializationData; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Optional; - -public class AutoMessageNoticeSerializer implements ObjectSerializer { - - @Override - public boolean supports(@NotNull Class type) { - return AutoMessageNotice.class.isAssignableFrom(type); - } - - @Override - public void serialize(@NotNull AutoMessageNotice message, @NotNull SerializationData data, @NotNull GenericsDeclaration generics) { - data.add("name", message.getName(), String.class); - data.addCollection("notices", message.getNotices(), Notice.class); - - message.getSound().ifPresent(sound -> data.add("sound", sound, AutoMessageSound.class)); - message.getRequiredPermission().ifPresent(permission -> data.add("requiredPermission", permission, String.class)); - message.getRequiredGroup().ifPresent(group -> data.add("requiredGroup", group, String.class)); - - if (message.isIgnoreAdmins()) { - data.add("ignoreAdmins", message.isIgnoreAdmins(), Boolean.class); - } - } - - @Override - public AutoMessageNotice deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { - String name = data.get("name", String.class); - List notices = data.getAsList("notices", Notice.class); - - AutoMessageSound sound = data.get("sound", AutoMessageSound.class); - String requiredPermission = data.get("requiredPermission", String.class); - String requiredGroup = data.get("requiredGroup", String.class); - - boolean ignoreAdmins = Optional.ofNullable(data.get("ignoreAdmins", Boolean.class)) - .orElse(false); - - return AutoMessageNotice.builder() - .name(name) - .notices(notices) - .sound(sound) - .requiredPermission(requiredPermission) - .requiredGroup(requiredGroup) - .ignoreAdmins(ignoreAdmins) - .build(); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatchTask.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatchTask.java deleted file mode 100644 index 425a16c..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatchTask.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.dispatcher; - -import org.bukkit.Server; -import org.bukkit.scheduler.BukkitRunnable; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -/** - * Scheduled task that periodically dispatches automatic messages. - */ -final class AutoMessageDispatchTask extends BukkitRunnable { - - private final Server server; - private final AutoMessageDispatcher dispatcher; - - public AutoMessageDispatchTask(@NotNull Server server, @NotNull AutoMessageDispatcher dispatcher) { - this.server = Objects.requireNonNull(server, "server cannot be null"); - this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher cannot be null"); - } - - @Override - public void run() { - if (this.dispatcher.isEnabled()) { - this.server.getOnlinePlayers().forEach(this.dispatcher::dispatch); - } - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatcher.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatcher.java deleted file mode 100644 index 102ec7b..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/dispatcher/AutoMessageDispatcher.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.dispatcher; - -import com.github.imdmk.automessage.configuration.ConfigurationManager; -import com.github.imdmk.automessage.feature.message.MessageService; -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNoticeConfig; -import com.github.imdmk.automessage.feature.message.auto.eligibility.AutoMessageEligibilityEvaluator; -import com.github.imdmk.automessage.feature.message.auto.selector.AutoMessageSelector; -import com.github.imdmk.automessage.feature.message.auto.selector.AutoMessageSelectorFactory; -import com.github.imdmk.automessage.scheduler.TaskScheduler; -import com.github.imdmk.automessage.util.DurationUtil; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitTask; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.util.Objects; - -/** - * Handles dispatching of automatic messages to online players - * based on configured strategy, eligibility, and scheduling. - *

- * Messages are selected via {@link AutoMessageSelector} and sent using {@link MessageService}. - */ -public final class AutoMessageDispatcher { - - /** - * Delay in ticks before the first automatic message is dispatched. - */ - private static final long INITIAL_DELAY_TICKS = 0L; - - /** - * Indicates that no task is currently scheduled. - */ - private static final int TASK_NOT_RUN_ID = 0; - - private final Server server; - private final ConfigurationManager configurationManager; - private final AutoMessageNoticeConfig configuration; - private final MessageService messageService; - private final TaskScheduler taskScheduler; - private final AutoMessageSelector selector; - - /** - * Delay between automatic messages, stored in server ticks. - */ - private volatile long delayTicks; - - /** - * ID of the currently scheduled Bukkit task; {@link #TASK_NOT_RUN_ID} if no task is scheduled. - */ - private volatile int currentTask; - - /** - * Indicates whether automatic message dispatching is enabled. - */ - private volatile boolean enabled; - - /** - * Constructs a new dispatcher using the given dependencies and selector factory. - * Initializes delay based on configuration. - * - * @param server the Bukkit server instance - * @param configurationManager the configuration manager used to persist delay updates - * @param configuration the configuration section for auto messages - * @param messageService the message service used to send messages - * @param taskScheduler the scheduler used to manage dispatch tasks - * @param eligibilityEvaluator evaluator to determine message eligibility - */ - public AutoMessageDispatcher( - @NotNull Server server, - @NotNull ConfigurationManager configurationManager, - @NotNull AutoMessageNoticeConfig configuration, - @NotNull MessageService messageService, - @NotNull TaskScheduler taskScheduler, - @NotNull AutoMessageEligibilityEvaluator eligibilityEvaluator - ) { - this.server = Objects.requireNonNull(server, "server cannot be null"); - this.configurationManager = Objects.requireNonNull(configurationManager, "configurationManager cannot be null"); - this.configuration = Objects.requireNonNull(configuration, "configuration cannot be null"); - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - this.taskScheduler = Objects.requireNonNull(taskScheduler, "taskScheduler cannot be null"); - - this.selector = AutoMessageSelectorFactory.create(this.configuration.mode, eligibilityEvaluator); - this.delayTicks = DurationUtil.toTicks(Objects.requireNonNull(this.configuration.delay, "delay cannot be null")); - this.enabled = true; - } - - /** - * Full constructor used for restoring dispatcher state (e.g., from persistence). - * - * @param server the Bukkit server instance - * @param configurationManager the configuration manager - * @param configuration the auto message configuration - * @param messageService the message service - * @param taskScheduler the task scheduler - * @param selector the message selector strategy - * @param delayTicks the dispatch delay in ticks - * @param currentTask the currently scheduled task ID - * @param enabled whether dispatching is enabled - */ - public AutoMessageDispatcher( - @NotNull Server server, - @NotNull ConfigurationManager configurationManager, - @NotNull AutoMessageNoticeConfig configuration, - @NotNull MessageService messageService, - @NotNull TaskScheduler taskScheduler, - @NotNull AutoMessageSelector selector, - long delayTicks, - int currentTask, - boolean enabled - ) { - this.server = Objects.requireNonNull(server, "server cannot be null"); - this.configurationManager = Objects.requireNonNull(configurationManager, "configurationManager cannot be null"); - this.configuration = Objects.requireNonNull(configuration, "configuration cannot be null"); - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - this.taskScheduler = Objects.requireNonNull(taskScheduler, "taskScheduler cannot be null"); - this.selector = Objects.requireNonNull(selector, "selector cannot be null"); - this.delayTicks = delayTicks; - this.currentTask = currentTask; - this.enabled = enabled; - } - - /** - * Dispatches an automatic message to the specified player if any eligible message is found. - * - * @param player the player to receive the message (must not be {@code null}) - */ - public void dispatch(@NotNull Player player) { - this.dispatch(player, this.selector); - } - - /** - * Dispatches an automatic message using the given selector. - * - * @param player the player to receive the message - * @param selector the selector used to choose a message - */ - public void dispatch(@NotNull Player player, @NotNull AutoMessageSelector selector) { - selector.selectFor(player, this.configuration.messages) - .ifPresent(message -> this.dispatch(player, message)); - } - - /** - * Sends the contents of the specified automatic message to the player. - * This includes sounds and all notice components. - * - * @param player the player to receive the message - * @param autoMessage the message to be dispatched - */ - public void dispatch(@NotNull Player player, @NotNull AutoMessageNotice autoMessage) { - autoMessage.getSound().ifPresent(sound -> sound.play(player)); - autoMessage.getNotices().forEach(notice -> this.messageService.sendAsync(player, notice)); - } - - /** - * Updates the delay between automatic messages and reschedules the dispatch task. - * - * @param newDelayTicks new delay in ticks (must be > 0) - * @throws IllegalArgumentException if {@code newDelayTicks} is less than or equal to zero - */ - public void changeDelay(long newDelayTicks) { - if (newDelayTicks <= 0) { - throw new IllegalArgumentException("Delay must be positive"); - } - - if (this.delayTicks == newDelayTicks) { - return; - } - - this.configuration.setDelay(DurationUtil.fromTicks(newDelayTicks)); - this.configurationManager.save(this.configuration); - - this.delayTicks = newDelayTicks; - this.schedule(); - } - - /** - * Schedules or reschedules the repeating asynchronous task that dispatches messages - * at intervals defined by {@link #delayTicks}. Cancels any previously scheduled task. - *

- * This method is synchronized to prevent race conditions during rescheduling. - */ - public synchronized void schedule() { - if (this.isTaskScheduled()) { - this.taskScheduler.cancelTask(this.currentTask); - this.selector.reset(); - } - - BukkitTask task = this.taskScheduler.runTimerAsync( - new AutoMessageDispatchTask(this.server, this), INITIAL_DELAY_TICKS, this.delayTicks - ); - this.currentTask = task.getTaskId(); - } - - /** - * Cancels the currently running auto message task, if any. - * This method is synchronized to prevent concurrent modification. - */ - public synchronized void cancel() { - if (this.isTaskScheduled()) { - this.taskScheduler.cancelTask(this.currentTask); - this.selector.reset(); - } - } - - /** - * Returns the currently active delay between message dispatches. - * - * @return delay between dispatches as a {@link Duration} - */ - @NotNull - public Duration getDelay() { - return DurationUtil.fromTicks(this.delayTicks); - } - - /** - * Returns whether a dispatch task is currently scheduled. - * - * @return {@code true} if a task is scheduled; {@code false} otherwise - */ - public boolean isTaskScheduled() { - return this.currentTask > TASK_NOT_RUN_ID; - } - - /** - * Enables or disables automatic dispatching of messages. - * When disabled, tasks will continue to run, but no messages will be sent. - * - * @param enabled {@code true} to enable; {@code false} to disable - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - /** - * Returns whether automatic message dispatching is currently enabled. - * - * @return {@code true} if enabled; {@code false} otherwise - */ - public boolean isEnabled() { - return this.enabled; - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/AutoMessageEligibilityEvaluator.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/AutoMessageEligibilityEvaluator.java deleted file mode 100644 index ee62e6e..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/AutoMessageEligibilityEvaluator.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.eligibility; - -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -/** - * Evaluates whether a player is eligible to receive a specific auto message notice. - */ -public interface AutoMessageEligibilityEvaluator { - - /** - * Determines whether the given player is eligible to receive the specified auto message notice. - * - * @param player the player to evaluate, must not be {@code null} - * @param notice the auto message notice to check eligibility against, must not be {@code null} - * @return {@code true} if the player is eligible to receive the notice; {@code false} otherwise - */ - boolean canReceive(@NotNull Player player, @NotNull AutoMessageNotice notice); - -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/DefaultEligibilityEvaluator.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/DefaultEligibilityEvaluator.java deleted file mode 100644 index 117ae3e..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/eligibility/DefaultEligibilityEvaluator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.eligibility; - -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public final class DefaultEligibilityEvaluator implements AutoMessageEligibilityEvaluator { - - private static final String LUCK_PERMS_GROUP_PREFIX = "group."; - - @Override - public boolean canReceive(@NotNull Player player, @NotNull AutoMessageNotice notice) { - if (notice.isIgnoreAdmins() && player.isOp()) { - return false; - } - - return notice.getRequiredPermission() - .map(player::hasPermission) - .orElse(true) - && notice.getRequiredGroup() - .map(rank -> player.hasPermission(LUCK_PERMS_GROUP_PREFIX + rank)) - .orElse(true); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelector.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelector.java deleted file mode 100644 index 55479b5..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelector.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.selector; - -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Optional; - -/** - * Selects an appropriate auto message notice for a player from a list of available messages. - */ -public interface AutoMessageSelector { - - /** - * Selects a suitable {@link AutoMessageNotice} for the given player based on the provided messages. - * - * @param player the player for whom to select the message, must not be {@code null} - * @param messages the list of available messages to select from, must not be {@code null} - * @return an {@link Optional} containing the selected auto message notice if any is applicable; otherwise, empty - */ - Optional selectFor(@NotNull Player player, @NotNull List messages); - - /** - * Resets any internal state maintained by the selector, preparing it for fresh selection cycles. - */ - void reset(); -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorFactory.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorFactory.java deleted file mode 100644 index ef9d3f6..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.selector; - -import com.github.imdmk.automessage.feature.message.auto.eligibility.AutoMessageEligibilityEvaluator; -import org.jetbrains.annotations.NotNull; - -/** - * Factory for creating {@link AutoMessageSelector} instances based on the specified mode. - */ -public final class AutoMessageSelectorFactory { - - private AutoMessageSelectorFactory() { - throw new UnsupportedOperationException("Factory class cannot be instantiated."); - } - - /** - * Creates an {@link AutoMessageSelector} according to the given {@link AutoMessageSelectorMode}. - * - * @param mode the selection mode, must not be {@code null} - * @param evaluator the eligibility evaluator used by the selector, must not be {@code null} - * @return a new instance of {@link AutoMessageSelector} matching the specified mode - */ - public static AutoMessageSelector create( - @NotNull AutoMessageSelectorMode mode, - @NotNull AutoMessageEligibilityEvaluator evaluator - ) { - return switch (mode) { - case SEQUENTIAL -> new SequentialAutoMessageSelector(evaluator); - case RANDOM -> new RandomAutoMessageSelector(evaluator); - }; - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorMode.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorMode.java deleted file mode 100644 index 7565ff1..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/AutoMessageSelectorMode.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.selector; - -/** - * Represents the strategy used for selecting and dispatching automatic messages. - *

- * This enum defines how messages should be selected from the configured list. - */ -public enum AutoMessageSelectorMode { - - /** - * Messages are selected randomly from the configured list. - * Each dispatch may return a different message, without any defined order. - */ - RANDOM, - - /** - * Messages are selected sequentially from the configured list. - * Each dispatch returns the next message in order, cycling back to the start after the last one. - */ - SEQUENTIAL - -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/RandomAutoMessageSelector.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/RandomAutoMessageSelector.java deleted file mode 100644 index c6424ba..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/RandomAutoMessageSelector.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.selector; - -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import com.github.imdmk.automessage.feature.message.auto.eligibility.AutoMessageEligibilityEvaluator; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Random; - -final class RandomAutoMessageSelector implements AutoMessageSelector { - - private final AutoMessageEligibilityEvaluator evaluator; - - private final Random random = new Random(); - - public RandomAutoMessageSelector(@NotNull AutoMessageEligibilityEvaluator evaluator) { - this.evaluator = Objects.requireNonNull(evaluator, "evaluator cannot be null"); - } - - @Override - public Optional selectFor(@NotNull Player player, @NotNull List messages) { - List filtered = messages.stream() - .filter(notice -> this.evaluator.canReceive(player, notice)) - .toList(); - - if (filtered.isEmpty()) { - return Optional.empty(); - } - - return Optional.of(filtered.get(this.random.nextInt(filtered.size()))); - } - - @Override - public void reset() {} -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/SequentialAutoMessageSelector.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/SequentialAutoMessageSelector.java deleted file mode 100644 index 18c45e2..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/selector/SequentialAutoMessageSelector.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.selector; - -import com.github.imdmk.automessage.feature.message.auto.AutoMessageNotice; -import com.github.imdmk.automessage.feature.message.auto.eligibility.AutoMessageEligibilityEvaluator; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; - -final class SequentialAutoMessageSelector implements AutoMessageSelector { - - private static final int INITIAL_POSITION = 0; - - private final AutoMessageEligibilityEvaluator evaluator; - private final AtomicInteger position = new AtomicInteger(INITIAL_POSITION); - - public SequentialAutoMessageSelector(@NotNull AutoMessageEligibilityEvaluator evaluator) { - this.evaluator = Objects.requireNonNull(evaluator, "evaluator cannot be null"); - } - - @Override - public Optional selectFor(@NotNull Player player, @NotNull List messages) { - int size = messages.size(); - - for (int i = 0; i < size; i++) { - int index = Math.floorMod(this.position.getAndIncrement(), size);; - - AutoMessageNotice candidate = messages.get(index); - if (candidate == null) { - return Optional.empty(); - } - - if (this.evaluator.canReceive(player, candidate)) { - return Optional.of(candidate); - } - } - - return Optional.empty(); - } - - @Override - public void reset() { - this.position.set(INITIAL_POSITION); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSound.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSound.java deleted file mode 100644 index 2dcdde3..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSound.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.sound; - -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -/** - * Represents a sound effect configuration to be played during a jump. - *

- * Wraps a {@link Sound} along with volume and pitch parameters - * - * @param sound the {@link Sound} to be played (must not be null) - * @param volume the volume of the sound (typically between 0.0 and 1.0+) - * @param pitch the pitch of the sound (typically between 0.5 and 2.0) - */ -public record AutoMessageSound(@NotNull Sound sound, float volume, float pitch) { - - /** - * Plays the configured sound effect for the specified player at their current location. - * - * @param player the {@link Player} to play the sound for (must not be null) - */ - public void play(@NotNull Player player) { - player.playSound(player.getLocation(), this.sound, this.volume, this.pitch); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSoundSerializer.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSoundSerializer.java deleted file mode 100644 index 570287b..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/AutoMessageSoundSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.sound; - -import eu.okaeri.configs.schema.GenericsDeclaration; -import eu.okaeri.configs.serdes.DeserializationData; -import eu.okaeri.configs.serdes.ObjectSerializer; -import eu.okaeri.configs.serdes.SerializationData; -import org.bukkit.Sound; -import org.jetbrains.annotations.NotNull; - -public class AutoMessageSoundSerializer implements ObjectSerializer { - - @Override - public boolean supports(@NotNull Class type) { - return AutoMessageSound.class.isAssignableFrom(type); - } - - @Override - public void serialize(@NotNull AutoMessageSound autoSound, @NotNull SerializationData data, @NotNull GenericsDeclaration generics) { - data.add("sound", autoSound.sound(), Sound.class); - data.add("volume", autoSound.volume(), float.class); - data.add("pitch", autoSound.pitch(), float.class); - } - - @Override - public AutoMessageSound deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { - Sound sound = data.get("sound", Sound.class); - float volume = data.get("volume", float.class); - float pitch = data.get("pitch", float.class); - - return new AutoMessageSound(sound, volume, pitch); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/SoundSerializer.java b/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/SoundSerializer.java deleted file mode 100644 index c0c6157..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/message/auto/sound/SoundSerializer.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.imdmk.automessage.feature.message.auto.sound; - -import eu.okaeri.configs.schema.GenericsDeclaration; -import eu.okaeri.configs.serdes.DeserializationData; -import eu.okaeri.configs.serdes.ObjectSerializer; -import eu.okaeri.configs.serdes.SerializationData; -import org.bukkit.Sound; -import org.jetbrains.annotations.NotNull; - -public class SoundSerializer implements ObjectSerializer { - - @Override - public boolean supports(@NotNull Class type) { - return Sound.class.isAssignableFrom(type); - } - - @Override - public void serialize(@NotNull Sound sound, @NotNull SerializationData data, @NotNull GenericsDeclaration generics) { - data.setValue(sound.name(), String.class); - } - - @Override - public Sound deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { - return Sound.valueOf(data.getValue(String.class)); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/update/UpdateController.java b/src/main/java/com/github/imdmk/automessage/feature/update/UpdateController.java deleted file mode 100644 index da7e2d7..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/update/UpdateController.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.github.imdmk.automessage.feature.update; - -import com.eternalcode.gitcheck.GitCheckResult; -import com.eternalcode.gitcheck.git.GitException; -import com.eternalcode.multification.notice.Notice; -import com.github.imdmk.automessage.configuration.PluginConfig; -import com.github.imdmk.automessage.feature.message.MessageService; -import com.github.imdmk.automessage.scheduler.TaskScheduler; -import com.github.imdmk.automessage.util.DurationUtil; -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.jetbrains.annotations.NotNull; - -import java.util.Objects; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class UpdateController implements Listener { - - private static final String PREFIX = "[AutoMessage Plugin] "; - private static final Notice UPDATE_AVAILABLE = Notice.chat( - " ", - PREFIX + "A new update is available!", - "- We strongly recommend downloading it!", - " " - ); - private static final Notice UPDATE_EXCEPTION = Notice.chat( - " ", - PREFIX + "An error occurred while checking for plugin update! Next update check: {UPDATE_CHECK_INTERVAL}", - " " - ); - - private final Logger logger; - private final PluginConfig pluginConfig; - private final MessageService messageService; - private final UpdateService updateService; - private final TaskScheduler taskScheduler; - - public UpdateController( - @NotNull Logger logger, - @NotNull PluginConfig pluginConfig, - @NotNull MessageService messageService, - @NotNull UpdateService updateService, - @NotNull TaskScheduler taskScheduler - ) { - this.logger = Objects.requireNonNull(logger, "logger cannot be null"); - this.pluginConfig = Objects.requireNonNull(pluginConfig, "pluginConfiguration cannot be null"); - this.messageService = Objects.requireNonNull(messageService, "messageService cannot be null"); - this.updateService = Objects.requireNonNull(updateService, "updateService cannot be null"); - this.taskScheduler = Objects.requireNonNull(taskScheduler, "taskScheduler cannot be null"); - } - - @EventHandler(priority = EventPriority.HIGHEST) - void onPlayerJoin(PlayerJoinEvent event) { - if (!this.pluginConfig.checkUpdate) { - return; - } - - Player player = event.getPlayer(); - - if (!player.isOp()) { - return; - } - - if (this.updateService.shouldCheck()) { - this.taskScheduler.runAsync(() -> this.checkForUpdate(player)); - } - } - - private void checkForUpdate(@NotNull Player player) { - try { - GitCheckResult result = this.updateService.check(); - if (result.isUpToDate()) { - return; - } - - this.sendNotice(player, UPDATE_AVAILABLE); - } - catch (GitException exception) { - this.logger.log(Level.SEVERE, "An error occurred while checking for update", exception); - this.sendNotice(player, UPDATE_EXCEPTION); - } - } - - private void sendNotice(@NotNull Player player, @NotNull Notice notice) { - this.messageService.create() - .notice(notice) - .placeholder("{UPDATE_CHECK_INTERVAL}", DurationUtil.format(this.pluginConfig.updateInterval)) - .viewer(player) - .send(); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/feature/update/UpdateService.java b/src/main/java/com/github/imdmk/automessage/feature/update/UpdateService.java deleted file mode 100644 index e764d88..0000000 --- a/src/main/java/com/github/imdmk/automessage/feature/update/UpdateService.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.imdmk.automessage.feature.update; - -import com.eternalcode.gitcheck.GitCheck; -import com.eternalcode.gitcheck.GitCheckResult; -import com.eternalcode.gitcheck.git.GitRepository; -import com.eternalcode.gitcheck.git.GitTag; -import com.github.imdmk.automessage.configuration.PluginConfig; -import org.bukkit.plugin.PluginDescriptionFile; -import org.jetbrains.annotations.NotNull; - -import java.time.Instant; -import java.util.Objects; - -public class UpdateService { - - private static final GitRepository GIT_REPOSITORY = GitRepository.of("imDMK", "AutoMessage"); - private static final GitCheck GIT_CHECK = new GitCheck(); - - private final PluginConfig config; - private final PluginDescriptionFile descriptionFile; - - private Instant latestCheck; - - public UpdateService(@NotNull PluginConfig config, @NotNull PluginDescriptionFile descriptionFile) { - this.config = Objects.requireNonNull(config, "pluginConfiguration cannot be null"); - this.descriptionFile = Objects.requireNonNull(descriptionFile, "pluginDescriptionFile cannot be null"); - } - - public @NotNull GitCheckResult check() { - this.latestCheck = Instant.now(); - - GitTag tag = GitTag.of("v" + this.descriptionFile.getVersion()); - return GIT_CHECK.checkRelease(GIT_REPOSITORY, tag); - } - - public boolean shouldCheck() { - if (this.latestCheck == null) { - return true; - } - - Instant nextCheckTime = this.latestCheck.plus(this.config.updateInterval); - return Instant.now().isAfter(nextCheckTime); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/scheduler/BukkitTaskScheduler.java b/src/main/java/com/github/imdmk/automessage/scheduler/BukkitTaskScheduler.java deleted file mode 100644 index 5fd12de..0000000 --- a/src/main/java/com/github/imdmk/automessage/scheduler/BukkitTaskScheduler.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.imdmk.automessage.scheduler; - -import org.bukkit.Server; -import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public final class BukkitTaskScheduler implements TaskScheduler { - - private final Plugin plugin; - private final BukkitScheduler bukkitScheduler; - - public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull Server server) { - this.plugin = Objects.requireNonNull(plugin, "plugin cannot be null"); - this.bukkitScheduler = Objects.requireNonNull(server.getScheduler(), "server cannot be null"); - } - - @Override - public BukkitTask runSync(@NotNull Runnable runnable) { - Objects.requireNonNull(runnable, "runnable cannot be null"); - return this.bukkitScheduler.runTask(this.plugin, runnable); - } - - @Override - public BukkitTask runAsync(@NotNull Runnable runnable) { - Objects.requireNonNull(runnable, "runnable cannot be null"); - return this.bukkitScheduler.runTaskAsynchronously(this.plugin, runnable); - } - - @Override - public BukkitTask runLaterAsync(@NotNull Runnable runnable, long delay) { - Objects.requireNonNull(runnable, "runnable cannot be null"); - return this.bukkitScheduler.runTaskLaterAsynchronously(this.plugin, runnable, delay); - } - - @Override - public BukkitTask runTimerAsync(@NotNull Runnable runnable, long delay, long period) { - Objects.requireNonNull(runnable, "runnable cannot be null"); - return this.bukkitScheduler.runTaskTimerAsynchronously(this.plugin, runnable, delay, period); - } - - @Override - public void cancelTask(int taskId) { - this.bukkitScheduler.cancelTask(taskId); - } - - @Override - public void shutdown() { - this.bukkitScheduler.cancelTasks(this.plugin); - } -} diff --git a/src/main/java/com/github/imdmk/automessage/scheduler/TaskScheduler.java b/src/main/java/com/github/imdmk/automessage/scheduler/TaskScheduler.java deleted file mode 100644 index 1f7e951..0000000 --- a/src/main/java/com/github/imdmk/automessage/scheduler/TaskScheduler.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.imdmk.automessage.scheduler; - -import org.bukkit.scheduler.BukkitTask; -import org.jetbrains.annotations.NotNull; - -/** - * Provides methods for scheduling tasks to be executed either synchronously on the main thread - * or asynchronously on a separate thread. - *

- * Implementations of this interface should ensure that synchronous tasks run on the main server thread, - * while asynchronous tasks run off the main thread to prevent blocking. - */ -public interface TaskScheduler { - - /** - * Executes the given {@link Runnable} synchronously on the main server thread as soon as possible. - * - * @param runnable the task to run synchronously; must not be null - * @throws NullPointerException if runnable is null - */ - BukkitTask runSync(@NotNull Runnable runnable); - - /** - * Executes the given {@link Runnable} asynchronously on a separate thread as soon as possible. - * - * @param runnable the task to run asynchronously; must not be null - * @throws NullPointerException if runnable is null - */ - BukkitTask runAsync(@NotNull Runnable runnable); - - /** - * Executes the given {@link Runnable} asynchronously after the specified delay. - * The delay unit depends on the implementation (typically server ticks or milliseconds). - * - * @param runnable the task to run asynchronously; must not be null - * @param delay the delay before executing the task, in implementation-specific units (e.g. ticks) - * @throws NullPointerException if runnable is null - */ - BukkitTask runLaterAsync(@NotNull Runnable runnable, long delay); - - /** - * Executes the given {@link Runnable} repeatedly on a timer asynchronously, - * starting after the initial delay and repeating with the specified period. - * The delay and period units depend on the implementation (e.g., server ticks or milliseconds). - * - * @param runnable the task to run asynchronously on a timer; must not be null - * @param delay the initial delay before first execution, in implementation-specific units - * @param period the period between later executions, in implementation-specific units - * @throws NullPointerException if runnable is null - */ - BukkitTask runTimerAsync(@NotNull Runnable runnable, long delay, long period); - - /** - * Cancels a scheduled task identified by its task ID. - *

- * If the task with the specified ID is currently scheduled or running, it will be cancelled, - * preventing any future executions of the task. If no task with the given ID exists or - * it has already completed or been cancelled, this method has no effect. - *

- * - * @param taskId the unique identifier of the scheduled task to cancel - */ - void cancelTask(int taskId); - - /** - * Shuts down the scheduler and cancels all pending asynchronous tasks. - *

- * This method should be called during plugin disable or application shutdown - * to ensure that no background tasks continue running after the application stops. - * After calling this method, the scheduler should reject any new tasks. - *

- * - * @implNote Implementations should ensure proper termination of all internal executor services - * or scheduling mechanisms to prevent resource leaks or thread hangs. - */ - void shutdown(); -} diff --git a/src/main/java/com/github/imdmk/automessage/util/DurationUtil.java b/src/main/java/com/github/imdmk/automessage/util/DurationUtil.java deleted file mode 100644 index 8be2abe..0000000 --- a/src/main/java/com/github/imdmk/automessage/util/DurationUtil.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.imdmk.automessage.util; - -import dev.rollczi.litecommands.time.DurationParser; -import dev.rollczi.litecommands.time.TemporalAmountParser; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -/** - * Utility class for parsing and formatting {@link Duration} values. - */ -public final class DurationUtil { - - private static final TemporalAmountParser DATE_TIME_PARSER = new DurationParser() - .withUnit("s", ChronoUnit.SECONDS) - .withUnit("m", ChronoUnit.MINUTES) - .withUnit("h", ChronoUnit.HOURS) - .withUnit("d", ChronoUnit.DAYS) - .withUnit("w", ChronoUnit.WEEKS) - .withUnit("mo", ChronoUnit.MONTHS) - .withUnit("y", ChronoUnit.YEARS); - - private DurationUtil() { - throw new UnsupportedOperationException("This is utility class."); - } - - /** - * Formats a given duration into a human-readable string. - * - * @param duration the duration to format - * @return a formatted string or "<1s" if duration is zero or negative - */ - @NotNull - public static String format(@NotNull final Duration duration) { - if (duration.isZero() || duration.isNegative()) { - return "<1s"; - } - - return DATE_TIME_PARSER.format(duration); - } - - /** - * Converts a duration to Minecraft ticks (1 tick = 50ms). - * - * @param duration the duration to convert - * @return duration in ticks - */ - public static long toTicks(@NotNull final Duration duration) { - if (duration.isZero() || duration.isNegative()) { - return 0L; - } - - return duration.toMillis() / 50; - } - - /** - * Converts Minecraft ticks to a {@link Duration}. - * 1 tick = 50 milliseconds. - * - * @param ticks the number of ticks to convert - * @return a {@link Duration} representing the given number of ticks - */ - @NotNull - public static Duration fromTicks(long ticks) { - if (ticks <= 0) { - return Duration.ZERO; - } - - return Duration.ofMillis(ticks * 50L); - } - - @NotNull - public static TemporalAmountParser parser() { - return DATE_TIME_PARSER; - } -} From 18a37b3b0849912db0bd7d4cca1c30b813283344 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sat, 6 Dec 2025 16:03:50 +0100 Subject: [PATCH 02/12] Update gradle.yml to use JDK 21 version. --- .github/workflows/gradle.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f277b6a..6ef6214 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -21,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Setup Gradle @@ -37,4 +37,4 @@ jobs: run: ./gradlew build - name: Build the Jar - run: ./gradlew shadowJar + run: ./gradlew shadowJar \ No newline at end of file From 97403818c5dc2d0752a40cf4eba2222cf760aad9 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sat, 6 Dec 2025 16:06:31 +0100 Subject: [PATCH 03/12] Correct gradlew file. --- gradlew | 323 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 234 insertions(+), 89 deletions(-) diff --git a/gradlew b/gradlew index 477c896..a58591e 100644 --- a/gradlew +++ b/gradlew @@ -1,89 +1,234 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega \ No newline at end of file +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" \ No newline at end of file From a36fb285a44556c112049742018c04e97ec6bb0d Mon Sep 17 00:00:00 2001 From: DMK <81445555+imDMK@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:09:44 +0100 Subject: [PATCH 04/12] Update automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../scheduled/dispatcher/MessageDispatcherConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java index 8f376aa..a5fe0f0 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java @@ -30,7 +30,7 @@ public void setEnabled(boolean enabled) { @Override public @NotNull OkaeriSerdesPack getSerdesPack() { return registry -> { - registry.register(new SerdesCommons()); + // SerdesCommons is registered globally in ConfigManager }; } From d0b8acda0eb7b1ec2daf347aa53996ea59a545d9 Mon Sep 17 00:00:00 2001 From: DMK <81445555+imDMK@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:09:51 +0100 Subject: [PATCH 05/12] Update automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../imdmk/automessage/scheduled/ScheduledMessageConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java index 5fec508..9cccabc 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java @@ -31,7 +31,7 @@ public final class ScheduledMessageConfig extends ConfigSection { "# Message order is preserved and processed sequentially by the dispatcher.", "#" }) - public List messages = Arrays.asList( + public List messages = List.of( ScheduledMessageBuilder.create() .name("first-message") .addNotices( From 2b26171c04822efb3d73e523837287b112290d0d Mon Sep 17 00:00:00 2001 From: DMK <81445555+imDMK@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:10:00 +0100 Subject: [PATCH 06/12] Update automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../imdmk/automessage/command/dispatcher/EnableCommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java index aebcf55..8599fc6 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java @@ -28,6 +28,7 @@ public EnableCommand( @Execute void enable(@Context CommandSender sender) { dispatcherConfig.setEnabled(true); + dispatcherConfig.save(); messageService.send(sender, n -> n.dispatcherMessages.dispatcherEnabled()); } } From a4627286ef5aa3bbfb116e5a6a2e53012435cb61 Mon Sep 17 00:00:00 2001 From: DMK <81445555+imDMK@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:10:31 +0100 Subject: [PATCH 07/12] Update automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../imdmk/automessage/platform/logger/BukkitPluginLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java index 0553682..f1db3be 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java @@ -70,7 +70,7 @@ public void warn(@NotNull String message) { @Override public void warn(@NotNull Throwable throwable) { - logger.warning(throwable.getMessage()); + logger.log(Level.WARNING, "A warning occurred, see stack trace for details.", throwable); } @Override From 1053cf14d436c2f888759b6566bdb5502aa6a13d Mon Sep 17 00:00:00 2001 From: DMK <81445555+imDMK@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:10:36 +0100 Subject: [PATCH 08/12] Update automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../imdmk/automessage/platform/logger/BukkitPluginLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java index f1db3be..d85264c 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/logger/BukkitPluginLogger.java @@ -85,7 +85,7 @@ public void warn(@NotNull String message, @NotNull Object... args) { @Override public void error(@NotNull Throwable throwable) { - logger.severe(throwable.getMessage()); + logger.log(Level.SEVERE, "An error occurred, see stack trace for details.", throwable); } @Override From 233cbf41baf55227e6ec79fe386069c8dfb12f18 Mon Sep 17 00:00:00 2001 From: DMK <81445555+imDMK@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:10:49 +0100 Subject: [PATCH 09/12] Update automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../imdmk/automessage/command/dispatcher/DisableCommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java index 69431cc..b6dbff7 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java @@ -28,6 +28,7 @@ public DisableCommand( @Execute void disable(@Context CommandSender sender) { dispatcherConfig.setEnabled(false); + dispatcherConfig.save(); messageService.send(sender, n -> n.dispatcherMessages.dispatcherDisabled()); } } \ No newline at end of file From b884d0926a1ca294dfa2e196ab4cd01d555b13fb Mon Sep 17 00:00:00 2001 From: imDMK Date: Sun, 7 Dec 2025 14:52:48 +0100 Subject: [PATCH 10/12] Improve ReloadCommand nad DispatcherCommands. --- .../imdmk/automessage/AutoMessagePlugin.java | 2 +- .../command/dispatcher/DisableCommand.java | 6 ++++ .../command/dispatcher/EnableCommand.java | 6 ++++ .../messages/DispatcherMessages.java | 3 ++ .../messages/DispatcherMessagesImpl.java | 28 ++++++++++++++++--- .../command/reload/ReloadCommand.java | 22 +++++++++------ .../dispatcher/MessageDispatcherConfig.java | 5 +++- 7 files changed, 57 insertions(+), 15 deletions(-) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java index c7338c5..db99749 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java @@ -96,7 +96,7 @@ void enable(@NotNull PluginSettings settings) { .commands( new DisableCommand(messageDispatcherConfig, messageService), new EnableCommand(messageDispatcherConfig, messageService), - new ReloadCommand(logger, configManager, messageService) + new ReloadCommand(logger, configManager, taskScheduler, messageService) ) .build(); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java index b6dbff7..1e667c3 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java @@ -27,8 +27,14 @@ public DisableCommand( @Execute void disable(@Context CommandSender sender) { + if (!dispatcherConfig.isEnabled()) { + messageService.send(sender, n -> n.dispatcherMessages.dispatcherAlreadyDisabled()); + return; + } + dispatcherConfig.setEnabled(false); dispatcherConfig.save(); + messageService.send(sender, n -> n.dispatcherMessages.dispatcherDisabled()); } } \ No newline at end of file diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java index 8599fc6..1f0730e 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java @@ -27,8 +27,14 @@ public EnableCommand( @Execute void enable(@Context CommandSender sender) { + if (dispatcherConfig.isEnabled()) { + messageService.send(sender, n -> n.dispatcherMessages.dispatcherAlreadyEnabled()); + return; + } + dispatcherConfig.setEnabled(true); dispatcherConfig.save(); + messageService.send(sender, n -> n.dispatcherMessages.dispatcherEnabled()); } } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java index 8e5be29..5d378ee 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessages.java @@ -6,6 +6,9 @@ public interface DispatcherMessages { Notice dispatcherEnabled(); + Notice dispatcherAlreadyEnabled(); + Notice dispatcherDisabled(); + Notice dispatcherAlreadyDisabled(); } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java index 39e35ef..0c01be4 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java @@ -6,14 +6,24 @@ public final class DispatcherMessagesImpl extends OkaeriConfig implements DispatcherMessages { - @Comment({"#", "# Message shown when automatic message dispatching gets enabled.", "#"}) + @Comment({"#", "# Sent when automatic message dispatching is successfully enabled.", "#"}) Notice dispatcherEnabled = Notice.chat( - "Automatic messages have been enabled." + "Automatic messages have been enabled." ); - @Comment({"#", "# Message shown when automatic message dispatching gets disabled.", "#"}) + @Comment({"#", "# Sent when a user attempts to enable dispatching, but it is already active.", "#"}) + Notice dispatcherAlreadyEnabled = Notice.chat( + "Automatic messages are already enabled." + ); + + @Comment({"#", "# Sent when automatic message dispatching is successfully disabled.", "#"}) Notice dispatcherDisabled = Notice.chat( - "Automatic messages have been disabled." + "Automatic messages have been disabled." + ); + + @Comment({"#", "# Sent when a user attempts to disable dispatching, but it is already inactive.", "#"}) + Notice dispatcherAlreadyDisabled = Notice.chat( + "Automatic messages are already disabled." ); @Override @@ -21,8 +31,18 @@ public Notice dispatcherEnabled() { return dispatcherEnabled; } + @Override + public Notice dispatcherAlreadyEnabled() { + return dispatcherAlreadyEnabled; + } + @Override public Notice dispatcherDisabled() { return dispatcherDisabled; } + + @Override + public Notice dispatcherAlreadyDisabled() { + return dispatcherAlreadyDisabled; + } } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java index 7adbfa3..a79f9cc 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/ReloadCommand.java @@ -2,6 +2,7 @@ import com.github.imdmk.automessage.config.ConfigManager; import com.github.imdmk.automessage.platform.logger.PluginLogger; +import com.github.imdmk.automessage.platform.scheduler.TaskScheduler; import com.github.imdmk.automessage.shared.message.MessageService; import com.github.imdmk.automessage.shared.validate.Validator; import dev.rollczi.litecommands.annotations.command.Command; @@ -11,34 +12,37 @@ import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; -import java.util.concurrent.CompletableFuture; - @Command(name = "automessage reload") @Permission("command.automessage.reload") public final class ReloadCommand { private final PluginLogger logger; private final ConfigManager configManager; + private final TaskScheduler taskScheduler; private final MessageService messageService; public ReloadCommand( @NotNull PluginLogger logger, @NotNull ConfigManager configManager, + @NotNull TaskScheduler taskScheduler, @NotNull MessageService messageService ) { this.logger = Validator.notNull(logger, "logger"); this.configManager = Validator.notNull(configManager, "configManager"); + this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler"); this.messageService = Validator.notNull(messageService, "messageService"); } @Execute void reload(@Context CommandSender sender) { - CompletableFuture.runAsync(configManager::loadAll) - .thenAccept(v -> messageService.send(sender, n -> n.reloadMessages.configReloadedSuccess())) - .exceptionally(e -> { - logger.error(e, "Failed to reload plugin config"); - messageService.send(sender, n -> n.reloadMessages.configReloadFailed()); - return null; - }); + taskScheduler.runAsync(() -> { + try { + configManager.loadAll(); + messageService.send(sender, n -> n.reloadMessages.configReloadedSuccess()); + } catch (Exception e) { + logger.error(e, "Failed to reload plugin config"); + messageService.send(sender, n -> n.reloadMessages.configReloadFailed()); + } + }); } } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java index a5fe0f0..6386eb9 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java @@ -4,7 +4,6 @@ import com.github.imdmk.automessage.scheduled.selector.MessageSelectorType; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import eu.okaeri.configs.serdes.commons.SerdesCommons; import org.jetbrains.annotations.NotNull; import java.time.Duration; @@ -27,6 +26,10 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + public boolean isEnabled() { + return enabled; + } + @Override public @NotNull OkaeriSerdesPack getSerdesPack() { return registry -> { From 1ac962a3c0bbca202dc4870477c92b0a53825d17 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sun, 7 Dec 2025 16:16:20 +0100 Subject: [PATCH 11/12] Add Config tests, improve ConfigManager, add Javadocs. --- .../imdmk/automessage/AutoMessagePlugin.java | 23 ++-- .../command/dispatcher/DisableCommand.java | 6 +- .../command/dispatcher/EnableCommand.java | 6 +- .../messages/DispatcherMessagesImpl.java | 4 +- .../reload/messages/ReloadMessagesImpl.java | 4 +- .../config/ConfigAccessException.java | 17 +++ .../automessage/config/ConfigBinder.java | 27 ++++ .../automessage/config/ConfigFactory.java | 22 +++ .../automessage/config/ConfigLifecycle.java | 39 ++++++ .../config/ConfigLoadException.java | 41 ------ .../automessage/config/ConfigManager.java | 105 ++++---------- .../automessage/config/ConfigRepresenter.java | 4 +- .../config/YamlConfigurerFactory.java | 36 +++++ .../messages/LiteCommandsMessagesImpl.java | 6 +- ...nfig.java => ScheduledMessagesConfig.java} | 68 ++++++++-- .../audience/filter/AudienceFilter.java | 21 +++ .../scheduled/audience/rule/AudienceRule.java | 44 +++++- .../dispatcher/DispatchFilteredTarget.java | 20 ++- .../dispatcher/DispatchPlayersTarget.java | 12 +- .../scheduled/dispatcher/DispatchTarget.java | 41 ++++++ .../dispatcher/MessageDispatcher.java | 49 +++++-- .../dispatcher/MessageDispatcherTask.java | 10 +- ...fig.java => MessagesDispatcherConfig.java} | 10 +- .../automessage/config/ConfigManagerTest.java | 128 ++++++++++++++++++ .../automessage/config/SampleConfig.java | 20 +++ .../automessage/DefaultPluginSettings.java | 8 +- build.gradle.kts | 2 - gradle/wrapper/gradle-wrapper.properties | 2 +- 28 files changed, 580 insertions(+), 195 deletions(-) create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigAccessException.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigBinder.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigFactory.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLifecycle.java delete mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/config/YamlConfigurerFactory.java rename automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/{ScheduledMessageConfig.java => ScheduledMessagesConfig.java} (58%) rename automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/{MessageDispatcherConfig.java => MessagesDispatcherConfig.java} (80%) create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/config/ConfigManagerTest.java create mode 100644 automessage-core/src/test/java/com/github/imdmk/automessage/config/SampleConfig.java diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java index db99749..bbed51a 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java @@ -12,11 +12,11 @@ import com.github.imdmk.automessage.platform.logger.PluginLogger; import com.github.imdmk.automessage.platform.scheduler.BukkitTaskScheduler; import com.github.imdmk.automessage.platform.scheduler.TaskScheduler; -import com.github.imdmk.automessage.scheduled.ScheduledMessageConfig; +import com.github.imdmk.automessage.scheduled.ScheduledMessagesConfig; import com.github.imdmk.automessage.scheduled.audience.filter.AudienceFilter; import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcher; -import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherTask; +import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; import com.github.imdmk.automessage.scheduled.selector.MessageSelector; import com.github.imdmk.automessage.scheduled.selector.MessageSelectorFactory; import com.github.imdmk.automessage.shared.message.MessageConfig; @@ -72,20 +72,20 @@ void enable(@NotNull PluginSettings settings) { messageService = new MessageService(configManager.require(MessageConfig.class), bukkitAudiences); taskScheduler = new BukkitTaskScheduler(plugin, server.getScheduler()); - final MessageDispatcherConfig messageDispatcherConfig = configManager.require(MessageDispatcherConfig.class); - final ScheduledMessageConfig scheduledMessageConfig = configManager.require(ScheduledMessageConfig.class); + final MessagesDispatcherConfig messagesDispatcherConfig = configManager.require(MessagesDispatcherConfig.class); + final ScheduledMessagesConfig scheduledMessagesConfig = configManager.require(ScheduledMessagesConfig.class); final AudienceFilter audienceFilter = AudienceFilter.createDefault(); - final MessageSelector messageSelector = MessageSelectorFactory.create(messageDispatcherConfig.selector); + final MessageSelector messageSelector = MessageSelectorFactory.create(messagesDispatcherConfig.selector); final MessageDispatcher messageDispatcher = new MessageDispatcher( messageService, messageSelector, audienceFilter, - () -> scheduledMessageConfig.messages + () -> scheduledMessagesConfig.messages ); - MessageDispatcherTask messageDispatcherTask = new MessageDispatcherTask(server, messageDispatcherConfig, messageDispatcher); + MessageDispatcherTask messageDispatcherTask = new MessageDispatcherTask(server, messagesDispatcherConfig, messageDispatcher); taskScheduler.runTimerAsync(messageDispatcherTask); liteCommands = LiteBukkitFactory.builder(PLUGIN_PREFIX, plugin, server) @@ -94,8 +94,8 @@ void enable(@NotNull PluginSettings settings) { .result(Notice.class, new NoticeResultHandlerImpl(messageService)) .commands( - new DisableCommand(messageDispatcherConfig, messageService), - new EnableCommand(messageDispatcherConfig, messageService), + new DisableCommand(messagesDispatcherConfig, messageService), + new EnableCommand(messagesDispatcherConfig, messageService), new ReloadCommand(logger, configManager, taskScheduler, messageService) ) @@ -107,7 +107,10 @@ void enable(@NotNull PluginSettings settings) { } void disable() { - Validator.ifNotNull(configManager, ConfigManager::saveAll); +// Validator.ifNotNull(configManager, manager -> { +// manager.saveAll(); +// manager.clearAll(); +// }); Validator.ifNotNull(bukkitAudiences, BukkitAudiences::close); Validator.ifNotNull(taskScheduler, TaskScheduler::shutdown); Validator.ifNotNull(liteCommands, LiteCommands::unregister); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java index 1e667c3..bb8ca0d 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java @@ -1,6 +1,6 @@ package com.github.imdmk.automessage.command.dispatcher; -import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; import com.github.imdmk.automessage.shared.message.MessageService; import com.github.imdmk.automessage.shared.validate.Validator; import dev.rollczi.litecommands.annotations.command.Command; @@ -14,11 +14,11 @@ @Permission("command.automessage.disable") public final class DisableCommand { - private final MessageDispatcherConfig dispatcherConfig; + private final MessagesDispatcherConfig dispatcherConfig; private final MessageService messageService; public DisableCommand( - @NotNull MessageDispatcherConfig dispatcherConfig, + @NotNull MessagesDispatcherConfig dispatcherConfig, @NotNull MessageService messageService ) { this.dispatcherConfig = Validator.notNull(dispatcherConfig, "dispatcherConfig"); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java index 1f0730e..23c94df 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java @@ -1,6 +1,6 @@ package com.github.imdmk.automessage.command.dispatcher; -import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; import com.github.imdmk.automessage.shared.message.MessageService; import com.github.imdmk.automessage.shared.validate.Validator; import dev.rollczi.litecommands.annotations.command.Command; @@ -14,11 +14,11 @@ @Permission("command.automessage.enable") public final class EnableCommand { - private final MessageDispatcherConfig dispatcherConfig; + private final MessagesDispatcherConfig dispatcherConfig; private final MessageService messageService; public EnableCommand( - @NotNull MessageDispatcherConfig dispatcherConfig, + @NotNull MessagesDispatcherConfig dispatcherConfig, @NotNull MessageService messageService ) { this.dispatcherConfig = Validator.notNull(dispatcherConfig, "dispatcherConfig"); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java index 0c01be4..8118737 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/messages/DispatcherMessagesImpl.java @@ -4,7 +4,9 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; -public final class DispatcherMessagesImpl extends OkaeriConfig implements DispatcherMessages { +public final class DispatcherMessagesImpl + extends OkaeriConfig + implements DispatcherMessages { @Comment({"#", "# Sent when automatic message dispatching is successfully enabled.", "#"}) Notice dispatcherEnabled = Notice.chat( diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java index 68df57a..e43251a 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/reload/messages/ReloadMessagesImpl.java @@ -4,7 +4,9 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; -public final class ReloadMessagesImpl extends OkaeriConfig implements ReloadMessages { +public final class ReloadMessagesImpl + extends OkaeriConfig + implements ReloadMessages { @Comment({"#", "# Message shown when all configuration files are successfully reloaded.", "#"}) Notice configReloadedSuccess = Notice.chat( diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigAccessException.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigAccessException.java new file mode 100644 index 0000000..8916dae --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigAccessException.java @@ -0,0 +1,17 @@ +package com.github.imdmk.automessage.config; + +public class ConfigAccessException extends RuntimeException { + + public ConfigAccessException(String message) { + super(message); + } + + public ConfigAccessException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigAccessException(Throwable cause) { + super(cause); + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigBinder.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigBinder.java new file mode 100644 index 0000000..945883a --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigBinder.java @@ -0,0 +1,27 @@ +package com.github.imdmk.automessage.config; + +import com.github.imdmk.automessage.shared.validate.Validator; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import eu.okaeri.configs.serdes.commons.SerdesCommons; +import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +final class ConfigBinder { + + void bind(@NotNull ConfigSection config, @NotNull File file) { + Validator.notNull(config, "config"); + Validator.notNull(file, "file"); + + final OkaeriSerdesPack serdesPack = config.getSerdesPack(); + final YamlSnakeYamlConfigurer yamlConfigurer = YamlConfigurerFactory.create(); + + config.withConfigurer(yamlConfigurer) + .withSerdesPack(serdesPack) + .withSerdesPack(new SerdesCommons()) + .withBindFile(file) + .withRemoveOrphans(true); + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigFactory.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigFactory.java new file mode 100644 index 0000000..2d47557 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigFactory.java @@ -0,0 +1,22 @@ +package com.github.imdmk.automessage.config; + +import com.github.imdmk.automessage.shared.validate.Validator; +import eu.okaeri.configs.ConfigManager; +import eu.okaeri.configs.exception.OkaeriException; +import org.jetbrains.annotations.NotNull; + +final class ConfigFactory { + + @NotNull T instantiate(@NotNull Class type) { + Validator.notNull(type, "type"); + + try { + return ConfigManager.create(type); + } catch (OkaeriException e) { + throw new IllegalStateException( + "Failed to instantiate config: " + type.getName(), e + ); + } + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLifecycle.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLifecycle.java new file mode 100644 index 0000000..a0a2013 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLifecycle.java @@ -0,0 +1,39 @@ +package com.github.imdmk.automessage.config; + +import com.github.imdmk.automessage.platform.logger.PluginLogger; +import com.github.imdmk.automessage.shared.validate.Validator; +import eu.okaeri.configs.exception.OkaeriException; +import org.jetbrains.annotations.NotNull; + +final class ConfigLifecycle { + + private final PluginLogger logger; + + ConfigLifecycle(@NotNull PluginLogger logger) { + this.logger = Validator.notNull(logger, "logger"); + } + + void initialize(@NotNull ConfigSection config) { + config.saveDefaults(); + load(config); + } + + 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); + } + } + + void save(@NotNull ConfigSection config) { + try { + config.save(); + } catch (Exception e) { + logger.error(e, "Failed to save config %s", config.getClass().getSimpleName()); + throw new ConfigAccessException(e); + } + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java deleted file mode 100644 index e577075..0000000 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigLoadException.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.imdmk.automessage.config; - -import org.jetbrains.annotations.NotNull; - -/** - * Thrown to indicate that a configuration file failed to load or save. - *

- * Wraps exceptions raised by the OkaeriConfig framework or underlying I/O operations. - * Used to propagate configuration-related errors as unchecked exceptions. - *

- */ -public final class ConfigLoadException extends RuntimeException { - - /** - * Creates a new {@link ConfigLoadException} with a default message and cause. - * - * @param cause the underlying exception that caused this failure - */ - public ConfigLoadException(@NotNull Throwable cause) { - super("Failed to load configuration", cause); - } - - /** - * Creates a new {@link ConfigLoadException} with a custom message. - * - * @param message the detail message describing the failure - */ - public ConfigLoadException(@NotNull String message) { - super(message); - } - - /** - * Creates a new {@link ConfigLoadException} with a custom message and cause. - * - * @param message the detail message describing the failure - * @param cause the underlying exception that caused this failure - */ - public ConfigLoadException(@NotNull String message, @NotNull Throwable cause) { - super(message, cause); - } -} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java index ef36ef8..9699ddd 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigManager.java @@ -2,18 +2,8 @@ import com.github.imdmk.automessage.platform.logger.PluginLogger; import com.github.imdmk.automessage.shared.validate.Validator; -import eu.okaeri.configs.exception.OkaeriException; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import eu.okaeri.configs.serdes.commons.SerdesCommons; -import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.Constructor; -import org.yaml.snakeyaml.representer.Representer; -import org.yaml.snakeyaml.resolver.Resolver; import java.io.File; import java.util.Collections; @@ -27,44 +17,33 @@ public final class ConfigManager { private final Set configs = ConcurrentHashMap.newKeySet(); private final Map, ConfigSection> byType = new ConcurrentHashMap<>(); - private final PluginLogger logger; private final File dataFolder; + private final ConfigFactory factory; + private final ConfigBinder binder; + private final ConfigLifecycle lifecycle; + public ConfigManager(@NotNull PluginLogger logger, @NotNull File dataFolder) { - this.logger = Validator.notNull(logger, "logger"); this.dataFolder = Validator.notNull(dataFolder, "dataFolder"); - } - - public @NotNull T create(@NotNull Class configClass) { - final T config = eu.okaeri.configs.ConfigManager.create(configClass); - final String fileName = config.getFileName(); - if (fileName.isBlank()) { - throw new IllegalStateException( - "Missing config file name for " + configClass.getName() - + " – override getFileName() to return a non-empty path, e.g. 'config.yml'." - ); - } - - final OkaeriSerdesPack serdes = Validator.notNull(config.getSerdesPack(), "config serdes pack"); + this.factory = new ConfigFactory(); + this.binder = new ConfigBinder(); + this.lifecycle = new ConfigLifecycle(logger); + } - final File file = new File(dataFolder, fileName); - final YamlSnakeYamlConfigurer configurer = createYamlSnakeYamlConfigurer(); + public @NotNull T create(@NotNull Class type) { + final T config = factory.instantiate(type); + final File file = new File(dataFolder, config.getFileName()); - config.withConfigurer(configurer, serdes); - config.withSerdesPack(new SerdesCommons()); - config.withBindFile(file); - config.withRemoveOrphans(true); - config.saveDefaults(); - config.load(true); + binder.bind(config, file); + lifecycle.initialize(config); - configs.add(config); - byType.put(configClass, config); + register(type, config); return config; } - public void createAll(@NotNull List> configClasses) { - configClasses.forEach(this::create); + public void createAll(@NotNull List> types) { + types.forEach(this::create); } @SuppressWarnings("unchecked") @@ -73,56 +52,21 @@ public T get(@NotNull Class type) { } public @NotNull T require(@NotNull Class type) { - final T config = get(type); + T config = get(type); + if (config == null) { throw new IllegalStateException("Config not created: " + type.getName()); } - return config; - } - - private @NotNull YamlSnakeYamlConfigurer createYamlSnakeYamlConfigurer() { - final LoaderOptions loader = new LoaderOptions(); - loader.setAllowRecursiveKeys(false); - loader.setMaxAliasesForCollections(50); - - final Constructor constructor = new Constructor(loader); - final DumperOptions options = new DumperOptions(); - options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - options.setIndent(2); - options.setSplitLines(false); - - final Representer representer = new ConfigRepresenter(options); - final Resolver resolver = new Resolver(); - - final Yaml yaml = new Yaml(constructor, representer, options, loader, resolver); - return new YamlSnakeYamlConfigurer(yaml); + return config; } public void loadAll() { - configs.forEach(this::load); - } - - private 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 ConfigLoadException(e); - } + configs.forEach(lifecycle::load); } public void saveAll() { - configs.forEach(this::save); - } - - private void save(@NotNull ConfigSection config) { - try { - config.save(); - } catch (OkaeriException e) { - logger.error(e, "Failed to save config: %s", config.getClass().getSimpleName()); - throw new ConfigLoadException(e); - } + configs.forEach(lifecycle::save); } public @NotNull @Unmodifiable Set getConfigs() { @@ -133,4 +77,9 @@ public void clearAll() { configs.clear(); byType.clear(); } -} + + private void register(Class type, ConfigSection config) { + configs.add(config); + byType.put(type, config); + } +} \ No newline at end of file diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java index 6f12146..0fa0b35 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/ConfigRepresenter.java @@ -11,9 +11,9 @@ import java.util.LinkedHashMap; import java.util.Map; -public final class ConfigRepresenter extends Representer { +final class ConfigRepresenter extends Representer { - public ConfigRepresenter(DumperOptions options) { + ConfigRepresenter(DumperOptions options) { super(options); this.representers.put(String.class, new RepresentString()); this.representers.put(Boolean.class, new RepresentBoolean()); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/config/YamlConfigurerFactory.java b/automessage-core/src/main/java/com/github/imdmk/automessage/config/YamlConfigurerFactory.java new file mode 100644 index 0000000..3656b47 --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/config/YamlConfigurerFactory.java @@ -0,0 +1,36 @@ +package com.github.imdmk.automessage.config; + +import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; +import org.yaml.snakeyaml.resolver.Resolver; + +final class YamlConfigurerFactory { + + private YamlConfigurerFactory() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } + + static YamlSnakeYamlConfigurer create() { + final LoaderOptions loader = new LoaderOptions(); + loader.setAllowRecursiveKeys(false); + loader.setMaxAliasesForCollections(50); + + final Constructor constructor = new Constructor(loader); + + final DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setIndent(2); + options.setSplitLines(false); + + final Representer representer = new ConfigRepresenter(options); + final Resolver resolver = new Resolver(); + + final Yaml yaml = new Yaml(constructor, representer, options, loader, resolver); + return new YamlSnakeYamlConfigurer(yaml); + } +} + diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java index e859a5f..3a43b20 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/platform/litecommands/messages/LiteCommandsMessagesImpl.java @@ -4,7 +4,9 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; -public final class LiteCommandsMessagesImpl extends OkaeriConfig implements LiteCommandsMessages { +public final class LiteCommandsMessagesImpl + extends OkaeriConfig + implements LiteCommandsMessages { @Comment({ "#", @@ -50,7 +52,7 @@ public final class LiteCommandsMessagesImpl extends OkaeriConfig implements Lite "#" }) Notice commandUsageEntry = Notice.chat( - "{USAGE}" + "{USAGE}" ); @Override diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessagesConfig.java similarity index 58% rename from automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java rename to automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessagesConfig.java index 9cccabc..f290f6d 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessageConfig.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessagesConfig.java @@ -14,21 +14,71 @@ import org.jetbrains.annotations.NotNull; import java.time.Duration; -import java.util.Arrays; import java.util.List; -public final class ScheduledMessageConfig extends ConfigSection { +public final class ScheduledMessagesConfig extends ConfigSection { @Comment({ "#", - "# List of all scheduled messages dispatched automatically by the plugin.", - "# Each entry contains:", - "# - name: unique identifier", - "# - notices: one or more message types (chat, title, actionbar, bossbar, sound)", - "# - rules: audience restrictions (permissions, groups)", + "# List of scheduled messages automatically dispatched by the plugin.", "#", - "# You may freely remove, modify, or add new message entries.", - "# Message order is preserved and processed sequentially by the dispatcher.", + "# Each message entry consists of the following fields:", + "#", + "# name: Unique identifier for internal use and debugging.", + "#", + "# notices: One or more message types to send to the player.", + "# Supported notice formats:", + "# • Chat message:", + "# - \"Hello world!\"", + "#", + "# • Actionbar:", + "# - actionbar: \"Actionbar message!\"", + "#", + "# • Title:", + "# - title: \"Title text\"", + "# subtitle: \"Subtitle text\"", + "#", + "# • BossBar:", + "# - bossbar:", + "# message: \"BossBar text\"", + "# duration: 5s", + "# color: RED", + "# overlay: PROGRESS", + "#", + "# • Sound:", + "# - sound: \"minecraft:entity.player.levelup MASTER 1.0 1.0\"", + "#", + "# rules: Conditions restricting who will receive this message.", + "# Supported audience rules:", + "# • Permission rule:", + "# - type: PERMISSION", + "# permission: myplugin.vip", + "#", + "# • Group rule:", + "# - type: GROUP", + "# group: vip", + "#", + "# Example full message entry:", + "#", + "# - name: example-message", + "# notices:", + "# - \"Hello from AutoMessage!\"", + "# - actionbar: \"Actionbar example\"", + "# - title: \"Warning\"", + "# subtitle: \"Something happened\"", + "# - bossbar:", + "# message: \"BossBar example\"", + "# duration: 3s", + "# color: BLUE", + "# overlay: PROGRESS", + "# - sound: \"minecraft:block.note_block.pling MASTER 1.0 1.5\"", + "#", + "# rules:", + "# - type: PERMISSION", + "# permission: myplugin.staff", + "#", + "# You may add, remove, or edit entries as needed.", + "# The dispatcher reads messages sequentially.", "#" }) public List messages = List.of( diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java index 3ce55c2..c15e357 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/filter/AudienceFilter.java @@ -4,10 +4,31 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +/** + * Determines whether a given player is allowed to receive a specific {@link ScheduledMessage}. + * + *

Audience filters enable selective delivery based on rules such as permissions, + * groups, worlds, conditions, or any custom logic implemented in {@link AudienceFilterImpl}.

+ */ public interface AudienceFilter { + /** + * Evaluates whether the given player should receive the specified scheduled message. + * + * @param player the player being considered (never null) + * @param message the scheduled message evaluated against the player (never null) + * @return true if the player is allowed to receive the message; false otherwise + */ boolean allows(@NotNull Player player, @NotNull ScheduledMessage message); + /** + * Creates the default audience filter used by the plugin. + * + *

The default implementation respects all {@link com.github.imdmk.automessage.scheduled.audience.rule.AudienceRule} + * entries declared on the scheduled message. It does not introduce any implicit filtering.

+ * + * @return a new default {@link AudienceFilter} instance + */ static AudienceFilter createDefault() { return new AudienceFilterImpl(); } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java index b906cfa..2345dab 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/audience/rule/AudienceRule.java @@ -3,25 +3,59 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +/** + * Represents a rule used to determine whether a player is eligible + * to receive a particular scheduled message. + * + *

Each rule implements a single boolean check and is composable. + * The dispatcher evaluates all rules attached to a message; if any rule + * returns {@code false}, the message is not sent to the player.

+ * + *

This interface is sealed: only built-in rule types are permitted.

+ */ public sealed interface AudienceRule permits AudiencePermissionRule, AudienceGroupRule { - static AudienceGroupRule group(String group) { + /** + * Creates a rule that allows only players belonging to a specific group. + * + * @param group the required group identifier (not null) + * @return a new group-based audience rule + */ + static @NotNull AudienceGroupRule group(@NotNull String group) { return new AudienceGroupRule(group); } - static AudiencePermissionRule permission(String permission) { + /** + * Creates a rule that allows only players having a specific permission node. + * + * @param permission the required permission (not null) + * @return a new permission-based audience rule + */ + static @NotNull AudiencePermissionRule permission(@NotNull String permission) { return new AudiencePermissionRule(permission); } + /** + * Evaluates whether the player satisfies this rule. + * + * @param player the player being evaluated (never null) + * @return true if the rule allows the player; false otherwise + */ boolean test(@NotNull Player player); + /** + * Enumeration of supported audience rule types. + */ enum Type { PERMISSION, GROUP } - @NotNull - Type type(); + /** + * Returns the type of this rule. + * + * @return the rule type (never null) + */ + @NotNull Type type(); } - diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java index 0bebd8e..f3a938f 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchFilteredTarget.java @@ -7,20 +7,26 @@ import java.util.Collection; import java.util.function.Predicate; +/** + * Dispatch target representing a collection of players filtered by a predicate. + * + *

Filtering is performed on each call to {@link #recipients()}, ensuring the + * result always reflects the current online player state and rule logic.

+ */ public record DispatchFilteredTarget( - @NotNull Collection targets, - @NotNull Predicate filter + @NotNull Collection players, + @NotNull Predicate predicate ) implements DispatchTarget { public DispatchFilteredTarget { - Validator.notNull(targets, "targets cannot be null"); - Validator.notNull(filter, "filter cannot be null"); + Validator.notNull(players, "players cannot be null"); + Validator.notNull(predicate, "predicate cannot be null"); } @Override public @NotNull Collection recipients() { - return targets.stream() - .filter(filter) - .toList(); + return players.stream() + .filter(predicate) + .toList(); // safe: creates an immutable snapshot view } } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java index a9d3b33..4d4aae5 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchPlayersTarget.java @@ -6,16 +6,22 @@ import java.util.Collection; +/** + * Simple dispatch target representing a predefined collection of recipient players. + * + *

This implementation performs no filtering or transformation. + * The provided collection is used as-is when dispatching messages.

+ */ public record DispatchPlayersTarget( - @NotNull Collection targets + @NotNull Collection players ) implements DispatchTarget { public DispatchPlayersTarget { - Validator.notNull(targets, "targets cannot be null"); + Validator.notNull(players, "players cannot be null"); } @Override public @NotNull Collection recipients() { - return targets; + return players; } } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java index 8532b20..321402f 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/DispatchTarget.java @@ -7,18 +7,59 @@ import java.util.List; import java.util.function.Predicate; +/** + * Represents a group of players to whom a scheduled message should be dispatched. + * + *

This abstraction allows the dispatcher to target:

+ *
    + *
  • a single player,
  • + *
  • a fixed collection of players,
  • + *
  • or a dynamically filtered subset of players.
  • + *
+ * + *

Implementations are expected to be immutable and lightweight.

+ */ public interface DispatchTarget { + /** + * Returns the collection of players who should receive the message. + *

This method never returns null, but may return an empty collection.

+ * + * @return a non-null collection of target recipients + */ @NotNull Collection recipients(); + /** + * Creates a dispatch target consisting of a single player. + * + * @param player a non-null player + * @return a dispatch target containing exactly this player + */ static @NotNull DispatchTarget player(@NotNull Player player) { return new DispatchPlayersTarget(List.of(player)); } + /** + * Creates a dispatch target from a provided collection of players. + * + * @param players a non-null collection of players + * @return a dispatch target for these players + */ static @NotNull DispatchTarget players(@NotNull Collection players) { return new DispatchPlayersTarget(players); } + /** + * Creates a dispatch target based on a collection of players filtered by a predicate. + * + *

The filter is evaluated lazily by the target implementation whenever + * {@link #recipients()} is invoked. This allows the dispatcher to handle + * dynamic state changes (permissions, groups, world, AFK status, etc.).

+ * + * @param players a non-null base collection of players + * @param filter a non-null predicate used to determine eligibility + * @return a filtered dispatch target + */ static @NotNull DispatchTarget filtered( @NotNull Collection players, @NotNull Predicate filter diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java index 797aecb..c0af866 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcher.java @@ -13,6 +13,16 @@ import java.util.Optional; import java.util.function.Supplier; +/** + * Coordinates selecting and dispatching scheduled messages to an audience. + * + *

This class is stateless and thread-safe under the assumption that: + *

    + *
  • {@link MessageSelector} is thread-safe or externally synchronized.
  • + *
  • {@link Supplier} provides a thread-safe messages source.
  • + *
+ *

+ */ public final class MessageDispatcher { private final MessageService messageService; @@ -29,13 +39,23 @@ public MessageDispatcher( this.messageService = Validator.notNull(messageService, "messageService"); this.selector = Validator.notNull(selector, "selector"); this.filter = Validator.notNull(filter, "filter"); - this.messagesSupplier = Validator.notNull(messagesSupplier, "messages"); + this.messagesSupplier = Validator.notNull(messagesSupplier, "messagesSupplier"); } + /** + * Selects the next scheduled message and dispatches it to the target audience. + * The selector index is automatically advanced. + */ public void dispatchNext(@NotNull DispatchTarget target) { dispatchNext(target, true); } + /** + * Selects the next scheduled message and dispatches it to the provided audience. + * + * @param target the recipients wrapper + * @param advanceSelectorIndex whether the selector should advance its internal index + */ public void dispatchNext( @NotNull DispatchTarget target, boolean advanceSelectorIndex @@ -46,33 +66,36 @@ public void dispatchNext( } final Optional next = selector.selectNext(messages, advanceSelectorIndex); - if (next.isEmpty()) { - return; - } - - dispatch(next.get(), target); + next.ifPresent(message -> dispatch(message, target)); } + /** + * Dispatches a specific message to all players permitted by the audience filter. + * + * @param message the message to send + * @param target the target audience + */ public void dispatch( @NotNull ScheduledMessage message, @NotNull DispatchTarget target ) { - for (final Player recipient : target.recipients()) { - if (!filter.allows(recipient, message)) { - continue; + for (final Player player : target.recipients()) { + if (filter.allows(player, message)) { + sendToPlayer(player, message); } - - sendToPlayer(recipient, message); } } + /** + * Sends all message notices to a single player asynchronously. + */ private void sendToPlayer( - @NotNull Player recipient, + @NotNull Player player, @NotNull ScheduledMessage message ) { for (final Notice notice : message.notices()) { messageService.create() - .viewer(recipient) + .viewer(player) .notice(notice) .sendAsync(); } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java index 70ec4d6..c20ff53 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java @@ -3,19 +3,21 @@ import com.github.imdmk.automessage.platform.scheduler.PluginTask; import com.github.imdmk.automessage.shared.validate.Validator; import org.bukkit.Server; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import java.time.Duration; +import java.util.Collection; public final class MessageDispatcherTask implements PluginTask { private final Server server; - private final MessageDispatcherConfig dispatcherConfig; + private final MessagesDispatcherConfig dispatcherConfig; private final MessageDispatcher messageDispatcher; public MessageDispatcherTask( @NotNull Server server, - @NotNull MessageDispatcherConfig dispatcherConfig, + @NotNull MessagesDispatcherConfig dispatcherConfig, @NotNull MessageDispatcher messageDispatcher ) { this.server = Validator.notNull(server, "server"); @@ -29,12 +31,12 @@ public void run() { return; } - var onlinePlayers = server.getOnlinePlayers(); + Collection onlinePlayers = server.getOnlinePlayers(); if (onlinePlayers.isEmpty()) { return; } - var target = DispatchTarget.players(onlinePlayers); + DispatchTarget target = DispatchTarget.players(onlinePlayers); messageDispatcher.dispatchNext(target); } diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessagesDispatcherConfig.java similarity index 80% rename from automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java rename to automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessagesDispatcherConfig.java index 6386eb9..47ebb25 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessagesDispatcherConfig.java @@ -8,7 +8,7 @@ import java.time.Duration; -public final class MessageDispatcherConfig extends ConfigSection { +public final class MessagesDispatcherConfig extends ConfigSection { @Comment("# Should the automatic dispatcher enabled?") public boolean enabled = true; @@ -17,7 +17,7 @@ public final class MessageDispatcherConfig extends ConfigSection { public Duration period = Duration.ofSeconds(10); @Comment("# How long to wait before the first automatic dispatch.") - public Duration initialDelay = Duration.ZERO; + public Duration initialDelay = Duration.ofSeconds(10); @Comment("# Strategy used to select the next message.") public MessageSelectorType selector = MessageSelectorType.SEQUENTIAL; @@ -32,13 +32,11 @@ public boolean isEnabled() { @Override public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> { - // SerdesCommons is registered globally in ConfigManager - }; + return registry -> {}; } @Override public @NotNull String getFileName() { - return "messageDispatcher.yml"; + return "messagesDispatcher.yml"; } } diff --git a/automessage-core/src/test/java/com/github/imdmk/automessage/config/ConfigManagerTest.java b/automessage-core/src/test/java/com/github/imdmk/automessage/config/ConfigManagerTest.java new file mode 100644 index 0000000..9eb08e7 --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/config/ConfigManagerTest.java @@ -0,0 +1,128 @@ +package com.github.imdmk.automessage.config; + +import com.github.imdmk.automessage.platform.logger.PluginLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ConfigManagerTest { + + private ConfigManager manager; + + private ConfigFactory factory; + private ConfigBinder binder; + private ConfigLifecycle lifecycle; + + private final PluginLogger logger = mock(PluginLogger.class); + private final File dataFolder = new File("build/test-configs"); + + @BeforeEach + @DisplayName("Setup test environment and inject mock dependencies") + void setUp() { + manager = new ConfigManager(logger, dataFolder); + + factory = mock(ConfigFactory.class); + binder = mock(ConfigBinder.class); + lifecycle = mock(ConfigLifecycle.class); + + inject("factory", factory); + inject("binder", binder); + inject("lifecycle", lifecycle); + } + + @Test + @DisplayName("create(): should instantiate, bind, initialize and register config") + void create_shouldInstantiateBindInitializeAndRegister() { + // given + SampleConfig config = new SampleConfig(); + when(factory.instantiate(SampleConfig.class)).thenReturn(config); + + // when + SampleConfig result = manager.create(SampleConfig.class); + + // then + assertSame(config, result); + + verify(factory).instantiate(SampleConfig.class); + verify(binder).bind(eq(config), any(File.class)); + verify(lifecycle).initialize(config); + + assertTrue(manager.getConfigs().contains(config)); + assertSame(config, manager.get(SampleConfig.class)); + } + + @Test + @DisplayName("createAll(): should call create() for each provided config class") + void createAll_shouldCreateEachConfig() { + when(factory.instantiate(SampleConfig.class)).thenReturn(new SampleConfig()); + + manager.createAll(List.of(SampleConfig.class)); + + verify(factory).instantiate(SampleConfig.class); + } + + @Test + @DisplayName("require(): should throw when config has not been created earlier") + void require_shouldThrowWhenNotCreated() { + assertThrows(IllegalStateException.class, () -> manager.require(SampleConfig.class)); + } + + @Test + @DisplayName("loadAll(): should delegate loading to lifecycle") + void loadAll_shouldDelegateToLifecycle() { + SampleConfig config = new SampleConfig(); + when(factory.instantiate(SampleConfig.class)).thenReturn(config); + + manager.create(SampleConfig.class); + manager.loadAll(); + + verify(lifecycle).load(config); + } + + @Test + @DisplayName("saveAll(): should delegate saving to lifecycle") + void saveAll_shouldDelegateToLifecycle() { + SampleConfig config = new SampleConfig(); + when(factory.instantiate(SampleConfig.class)).thenReturn(config); + + manager.create(SampleConfig.class); + manager.saveAll(); + + verify(lifecycle).save(config); + } + + @Test + @DisplayName("clearAll(): should remove all configs from registry") + void clearAll_shouldRemoveAllConfigs() { + when(factory.instantiate(SampleConfig.class)).thenReturn(new SampleConfig()); + + manager.create(SampleConfig.class); + manager.clearAll(); + + assertTrue(manager.getConfigs().isEmpty()); + assertNull(manager.get(SampleConfig.class)); + } + + private void inject(String field, Object value) { + try { + var f = ConfigManager.class.getDeclaredField(field); + f.setAccessible(true); + f.set(manager, value); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/automessage-core/src/test/java/com/github/imdmk/automessage/config/SampleConfig.java b/automessage-core/src/test/java/com/github/imdmk/automessage/config/SampleConfig.java new file mode 100644 index 0000000..e0c9562 --- /dev/null +++ b/automessage-core/src/test/java/com/github/imdmk/automessage/config/SampleConfig.java @@ -0,0 +1,20 @@ +package com.github.imdmk.automessage.config; + +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import org.jetbrains.annotations.NotNull; + +public class SampleConfig extends ConfigSection { + + public int value = 5; + + @Override + public @NotNull OkaeriSerdesPack getSerdesPack() { + return registry -> {}; + } + + @Override + public @NotNull String getFileName() { + return "sample.yml"; + } +} + diff --git a/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java b/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java index b061706..66ced5e 100644 --- a/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java +++ b/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java @@ -1,8 +1,8 @@ package com.github.imdmk.automessage; import com.github.imdmk.automessage.config.ConfigSection; -import com.github.imdmk.automessage.scheduled.ScheduledMessageConfig; -import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; +import com.github.imdmk.automessage.scheduled.ScheduledMessagesConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; import com.github.imdmk.automessage.shared.message.MessageConfig; import java.util.List; @@ -13,8 +13,8 @@ public final class DefaultPluginSettings implements PluginSettings { public List> configs() { return List.of( MessageConfig.class, - ScheduledMessageConfig.class, - MessageDispatcherConfig.class + ScheduledMessagesConfig.class, + MessagesDispatcherConfig.class ); } } diff --git a/build.gradle.kts b/build.gradle.kts index 88b122a..5b1a37a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,3 @@ -apply(plugin = "java-library") - group = "com.github.imdmk.automessage" version = "2.0.0" 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 From 14e2a5ce61eb0cd56aa56baa791e02d9ac8f8ff9 Mon Sep 17 00:00:00 2001 From: imDMK Date: Sun, 7 Dec 2025 16:31:52 +0100 Subject: [PATCH 12/12] Improve config documentation. --- .../imdmk/automessage/AutoMessagePlugin.java | 12 +-- .../command/dispatcher/DisableCommand.java | 6 +- .../command/dispatcher/EnableCommand.java | 6 +- .../scheduled/ScheduledMessagesConfig.java | 49 ++++++++++ .../dispatcher/MessageDispatcherConfig.java | 90 +++++++++++++++++++ .../dispatcher/MessageDispatcherTask.java | 4 +- .../dispatcher/MessagesDispatcherConfig.java | 42 --------- .../shared/message/MessageConfig.java | 28 ++++++ .../automessage/DefaultPluginSettings.java | 4 +- 9 files changed, 183 insertions(+), 58 deletions(-) create mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java delete mode 100644 automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessagesDispatcherConfig.java diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java index bbed51a..982b2ae 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/AutoMessagePlugin.java @@ -15,8 +15,8 @@ import com.github.imdmk.automessage.scheduled.ScheduledMessagesConfig; import com.github.imdmk.automessage.scheduled.audience.filter.AudienceFilter; import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcher; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherTask; -import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; import com.github.imdmk.automessage.scheduled.selector.MessageSelector; import com.github.imdmk.automessage.scheduled.selector.MessageSelectorFactory; import com.github.imdmk.automessage.shared.message.MessageConfig; @@ -72,11 +72,11 @@ void enable(@NotNull PluginSettings settings) { messageService = new MessageService(configManager.require(MessageConfig.class), bukkitAudiences); taskScheduler = new BukkitTaskScheduler(plugin, server.getScheduler()); - final MessagesDispatcherConfig messagesDispatcherConfig = configManager.require(MessagesDispatcherConfig.class); + final MessageDispatcherConfig messageDispatcherConfig = configManager.require(MessageDispatcherConfig.class); final ScheduledMessagesConfig scheduledMessagesConfig = configManager.require(ScheduledMessagesConfig.class); final AudienceFilter audienceFilter = AudienceFilter.createDefault(); - final MessageSelector messageSelector = MessageSelectorFactory.create(messagesDispatcherConfig.selector); + final MessageSelector messageSelector = MessageSelectorFactory.create(messageDispatcherConfig.selector); final MessageDispatcher messageDispatcher = new MessageDispatcher( messageService, @@ -85,7 +85,7 @@ void enable(@NotNull PluginSettings settings) { () -> scheduledMessagesConfig.messages ); - MessageDispatcherTask messageDispatcherTask = new MessageDispatcherTask(server, messagesDispatcherConfig, messageDispatcher); + MessageDispatcherTask messageDispatcherTask = new MessageDispatcherTask(server, messageDispatcherConfig, messageDispatcher); taskScheduler.runTimerAsync(messageDispatcherTask); liteCommands = LiteBukkitFactory.builder(PLUGIN_PREFIX, plugin, server) @@ -94,8 +94,8 @@ void enable(@NotNull PluginSettings settings) { .result(Notice.class, new NoticeResultHandlerImpl(messageService)) .commands( - new DisableCommand(messagesDispatcherConfig, messageService), - new EnableCommand(messagesDispatcherConfig, messageService), + new DisableCommand(messageDispatcherConfig, messageService), + new EnableCommand(messageDispatcherConfig, messageService), new ReloadCommand(logger, configManager, taskScheduler, messageService) ) diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java index bb8ca0d..1e667c3 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/DisableCommand.java @@ -1,6 +1,6 @@ package com.github.imdmk.automessage.command.dispatcher; -import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; import com.github.imdmk.automessage.shared.message.MessageService; import com.github.imdmk.automessage.shared.validate.Validator; import dev.rollczi.litecommands.annotations.command.Command; @@ -14,11 +14,11 @@ @Permission("command.automessage.disable") public final class DisableCommand { - private final MessagesDispatcherConfig dispatcherConfig; + private final MessageDispatcherConfig dispatcherConfig; private final MessageService messageService; public DisableCommand( - @NotNull MessagesDispatcherConfig dispatcherConfig, + @NotNull MessageDispatcherConfig dispatcherConfig, @NotNull MessageService messageService ) { this.dispatcherConfig = Validator.notNull(dispatcherConfig, "dispatcherConfig"); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java index 23c94df..1f0730e 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/command/dispatcher/EnableCommand.java @@ -1,6 +1,6 @@ package com.github.imdmk.automessage.command.dispatcher; -import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; import com.github.imdmk.automessage.shared.message.MessageService; import com.github.imdmk.automessage.shared.validate.Validator; import dev.rollczi.litecommands.annotations.command.Command; @@ -14,11 +14,11 @@ @Permission("command.automessage.enable") public final class EnableCommand { - private final MessagesDispatcherConfig dispatcherConfig; + private final MessageDispatcherConfig dispatcherConfig; private final MessageService messageService; public EnableCommand( - @NotNull MessagesDispatcherConfig dispatcherConfig, + @NotNull MessageDispatcherConfig dispatcherConfig, @NotNull MessageService messageService ) { this.dispatcherConfig = Validator.notNull(dispatcherConfig, "dispatcherConfig"); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessagesConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessagesConfig.java index f290f6d..75f47ea 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessagesConfig.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/ScheduledMessagesConfig.java @@ -7,6 +7,7 @@ import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRule; import com.github.imdmk.automessage.scheduled.audience.rule.AudienceRuleSerializer; import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.annotation.Header; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.key.Key; @@ -16,6 +17,54 @@ import java.time.Duration; import java.util.List; +@Header({ + "# ============================================================================", + "# AutoMessage — scheduledMessages.yml", + "# ============================================================================", + "# This file defines all automatically dispatched messages used by AutoMessage.", + "# Each entry represents one scheduled announcement, which may contain multiple", + "# message formats such as chat, actionbar, title, bossbar, and sound notices.", + "#", + "# How it works:", + "# • Messages are dispatched in a sequence, based on the selector strategy", + "# configured in messagesDispatcher.yml.", + "# • Each message may contain access rules (audience filters) to restrict", + "# delivery to specific players (groups, permissions, etc.).", + "#", + "# Structure of a scheduled message entry:", + "# name: Unique identifier of the message.", + "# notices: One or more notices to send using Multification.", + "# Supported notice types:", + "# - Chat", + "# - Actionbar", + "# - Title / Subtitle", + "# - BossBar", + "# - Sound", + "#", + "# rules: Optional audience restrictions.", + "# Supported rule types:", + "# • PERMISSION -> Only players with this permission receive the message", + "# • GROUP -> Only players in the given group receive the message", + "#", + "# Editing recommendations:", + "# • You may freely add or remove message entries.", + "# • MiniMessage formatting (, , , etc.) is fully supported.", + "# • Sound format: \"namespace:key SOURCE volume pitch\".", + "# • Duration fields support values like: 5s, 2m, 500ms.", + "#", + "# After making changes, reload the plugin via:", + "# /automessage reload", + "#", + "# Source Code:", + "# https://github.com/imDMK/AutoMessage", + "#", + "# Support development:", + "# GitHub Sponsors: https://github.com/sponsors/imDMK", + "# PayPal: https://paypal.me/dominiksuliga", + "#", + "# Thank you for using AutoMessage!", + "# ============================================================================" +}) public final class ScheduledMessagesConfig extends ConfigSection { @Comment({ diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java new file mode 100644 index 0000000..3b93fdb --- /dev/null +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherConfig.java @@ -0,0 +1,90 @@ +package com.github.imdmk.automessage.scheduled.dispatcher; + +import com.github.imdmk.automessage.config.ConfigSection; +import com.github.imdmk.automessage.scheduled.selector.MessageSelectorType; +import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.annotation.Header; +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +@Header({ + "# ============================================================================", + "# AutoMessage — messagesDispatcher.yml", + "# ============================================================================", + "# This file controls how AutoMessage dispatches scheduled announcements.", + "# It defines startup timing, repeat intervals, and which message-selection", + "# strategy the dispatcher should use.", + "#", + "# Fields:", + "# enabled:", + "# Master switch controlling whether automatic message dispatching is active.", + "# true -> messages cycle automatically", + "# false -> announcements are paused", + "#", + "# period:", + "# Time between consecutive dispatch executions.", + "# Examples: 10s, 1m, 500ms", + "#", + "# initialDelay:", + "# How long the dispatcher should wait before sending the first message", + "# after plugin startup.", + "#", + "# selector:", + "# Strategy determining which scheduled message is selected next.", + "# Supported types:", + "# SEQUENTIAL – cycle through messages in order", + "# RANDOM – choose a random message each time", + "#", + "# Notes:", + "# • This file works together with scheduledMessages.yml.", + "# • After editing this file you must restart the server, as dispatcher", + "# scheduling is configured during plugin initialization.", + "#", + "# Source Code:", + "# https://github.com/imDMK/AutoMessage", + "#", + "# Support development:", + "# GitHub Sponsors: https://github.com/sponsors/imDMK", + "# PayPal: https://paypal.me/dominiksuliga", + "#", + "# ============================================================================" +}) +public final class MessageDispatcherConfig extends ConfigSection { + + @Comment({"#", "# Whether automatic scheduled-message dispatching is enabled.", "#"}) + public boolean enabled = true; + + @Comment({"#", "# Delay between automatic dispatch executions.", "#"}) + public Duration period = Duration.ofSeconds(10); + + @Comment({"#", "# Initial delay before the very first automatic message dispatch.", "#"}) + public Duration initialDelay = Duration.ofSeconds(10); + + @Comment({ + "#", + "# Strategy used to select which scheduled message will be dispatched next.", + "# Available options: SEQUENTIAL, RANDOM", + "#" + }) + public MessageSelectorType selector = MessageSelectorType.SEQUENTIAL; + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + @Override + public @NotNull OkaeriSerdesPack getSerdesPack() { + return registry -> {}; + } + + @Override + public @NotNull String getFileName() { + return "messagesDispatcher.yml"; + } +} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java index c20ff53..fc554b1 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessageDispatcherTask.java @@ -12,12 +12,12 @@ public final class MessageDispatcherTask implements PluginTask { private final Server server; - private final MessagesDispatcherConfig dispatcherConfig; + private final MessageDispatcherConfig dispatcherConfig; private final MessageDispatcher messageDispatcher; public MessageDispatcherTask( @NotNull Server server, - @NotNull MessagesDispatcherConfig dispatcherConfig, + @NotNull MessageDispatcherConfig dispatcherConfig, @NotNull MessageDispatcher messageDispatcher ) { this.server = Validator.notNull(server, "server"); diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessagesDispatcherConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessagesDispatcherConfig.java deleted file mode 100644 index 47ebb25..0000000 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/scheduled/dispatcher/MessagesDispatcherConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.imdmk.automessage.scheduled.dispatcher; - -import com.github.imdmk.automessage.config.ConfigSection; -import com.github.imdmk.automessage.scheduled.selector.MessageSelectorType; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; - -public final class MessagesDispatcherConfig extends ConfigSection { - - @Comment("# Should the automatic dispatcher enabled?") - public boolean enabled = true; - - @Comment("# Delay between messages (time between dispatch executions).") - public Duration period = Duration.ofSeconds(10); - - @Comment("# How long to wait before the first automatic dispatch.") - public Duration initialDelay = Duration.ofSeconds(10); - - @Comment("# Strategy used to select the next message.") - public MessageSelectorType selector = MessageSelectorType.SEQUENTIAL; - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public boolean isEnabled() { - return enabled; - } - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> {}; - } - - @Override - public @NotNull String getFileName() { - return "messagesDispatcher.yml"; - } -} diff --git a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java index 9b5c9eb..fd7d5f8 100644 --- a/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java +++ b/automessage-core/src/main/java/com/github/imdmk/automessage/shared/message/MessageConfig.java @@ -7,9 +7,37 @@ import com.github.imdmk.automessage.config.ConfigSection; import com.github.imdmk.automessage.platform.litecommands.messages.LiteCommandsMessagesImpl; import eu.okaeri.configs.annotation.Comment; +import eu.okaeri.configs.annotation.Header; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; +@Header({ + "# ============================================================================", + "# AutoMessage — messages.yml", + "# ============================================================================", + "# This configuration file defines all user-facing messages used by AutoMessage.", + "# It centralizes command responses, dispatcher status messages, and reload", + "# notifications, allowing full customization without modifying code.", + "#", + "# Sections:", + "# • liteCommandsMessages — errors, usage prompts, and permission responses", + "# • dispatcherMessages — messages for enabling/disabling automatic dispatching", + "# • reloadMessages — messages shown during configuration reload operations", + "#", + "# Editing Tips:", + "# • Colors follow MiniMessage syntax (, , , , etc.).", + "# • Reload the plugin after editing: /automessage reload", + "#", + "# Source Code:", + "# https://github.com/imDMK/AutoMessage", + "#", + "# Support development:", + "# GitHub Sponsors: https://github.com/sponsors/imDMK", + "# PayPal: https://paypal.me/dominiksuliga", + "#", + "# Thank you for using AutoMessage!", + "# ============================================================================" +}) public final class MessageConfig extends ConfigSection { @Comment({ diff --git a/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java b/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java index 66ced5e..2a30160 100644 --- a/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java +++ b/automessage-plugin/src/main/java/com/github/imdmk/automessage/DefaultPluginSettings.java @@ -2,7 +2,7 @@ import com.github.imdmk.automessage.config.ConfigSection; import com.github.imdmk.automessage.scheduled.ScheduledMessagesConfig; -import com.github.imdmk.automessage.scheduled.dispatcher.MessagesDispatcherConfig; +import com.github.imdmk.automessage.scheduled.dispatcher.MessageDispatcherConfig; import com.github.imdmk.automessage.shared.message.MessageConfig; import java.util.List; @@ -14,7 +14,7 @@ public List> configs() { return List.of( MessageConfig.class, ScheduledMessagesConfig.class, - MessagesDispatcherConfig.class + MessageDispatcherConfig.class ); } }