diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgPlaceholderSetup.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgPlaceholderSetup.java new file mode 100644 index 000000000..99829ed75 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgPlaceholderSetup.java @@ -0,0 +1,104 @@ +package com.eternalcode.core.feature.msg; + +import com.eternalcode.core.feature.msg.toggle.MsgState; +import com.eternalcode.core.feature.msg.toggle.MsgToggleRepository; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Controller; +import com.eternalcode.core.placeholder.PlaceholderRegistry; +import com.eternalcode.core.placeholder.PlaceholderReplacer; +import com.eternalcode.core.placeholder.cache.AsyncPlaceholderCacheRegistry; +import com.eternalcode.core.placeholder.cache.AsyncPlaceholderCached; +import com.eternalcode.core.publish.Subscribe; +import com.eternalcode.core.publish.event.EternalInitializeEvent; +import com.eternalcode.core.translation.Translation; +import com.eternalcode.core.translation.TranslationManager; +import java.time.Duration; +import java.util.UUID; +import java.util.function.Function; + +@Controller +public class MsgPlaceholderSetup { + + public static final String MSG_STATE_CACHE_KEY = "msg_state"; + + private final MsgService msgService; + private final MsgToggleRepository msgToggleRepository; + private final TranslationManager translationManager; + private final AsyncPlaceholderCacheRegistry cacheRegistry; + + @Inject + MsgPlaceholderSetup( + MsgService msgService, + MsgToggleRepository msgToggleRepository, + TranslationManager translationManager, + AsyncPlaceholderCacheRegistry cacheRegistry + ) { + this.msgService = msgService; + this.msgToggleRepository = msgToggleRepository; + this.translationManager = translationManager; + this.cacheRegistry = cacheRegistry; + } + + @Subscribe(EternalInitializeEvent.class) + void setUpPlaceholders(PlaceholderRegistry placeholderRegistry) { + Translation translation = this.translationManager.getMessages(); + + AsyncPlaceholderCached stateCache = this.cacheRegistry.register( + MSG_STATE_CACHE_KEY, + this.msgToggleRepository::getPrivateChatState, + Duration.ofMinutes(10) + ); + + placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of( + "socialspy_status", + player -> String.valueOf(this.msgService.isSpy(player.getUniqueId())) + )); + + placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of( + "socialspy_status_formatted", + player -> { + UUID uuid = player.getUniqueId(); + return this.msgService.isSpy(uuid) + ? translation.msg().placeholders().socialSpyEnabled() + : translation.msg().placeholders().socialSpyDisabled(); + } + )); + + placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of( + "msg_status", + player -> this.formatMsgState( + player.getUniqueId(), + stateCache, + translation, + state -> state.name().toLowerCase() + ) + )); + + placeholderRegistry.registerPlaceholder(PlaceholderReplacer.of( + "msg_status_formatted", + player -> this.formatMsgState( + player.getUniqueId(), + stateCache, + translation, + state -> state == MsgState.ENABLED + ? translation.msg().placeholders().msgEnabled() + : translation.msg().placeholders().msgDisabled() + ) + )); + } + + private String formatMsgState( + UUID uuid, + AsyncPlaceholderCached stateCache, + Translation translation, + Function formatter + ) { + MsgState state = stateCache.getCached(uuid); + + if (state == null) { + return translation.msg().placeholders().loading(); + } + + return formatter.apply(state); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/ENMsgMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/ENMsgMessages.java index 2f353548f..143dd1e47 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/ENMsgMessages.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/ENMsgMessages.java @@ -36,4 +36,22 @@ public class ENMsgMessages extends OkaeriConfig implements MsgMessages { public Notice socialSpyEnable = Notice.chat("SocialSpy has been {STATE}!"); public Notice socialSpyDisable = Notice.chat("SocialSpy has been {STATE}!"); + @Comment("# Formatowanie placeholderów") + public ENPlaceholders placeholders = new ENPlaceholders(); + + @Getter + @Accessors(fluent = true) + public static class ENPlaceholders extends OkaeriConfig implements MsgMessages.Placeholders { + private String loading = "Loading..."; + + private String msgEnabled = "Enabled"; + private String msgDisabled = "Disabled"; + private String socialSpyEnabled = "Enabled"; + private String socialSpyDisabled = "Disabled"; + } + + public ENPlaceholders placeholders() { + return this.placeholders; + } + } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/MsgMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/MsgMessages.java index 070f0b391..9f5831a70 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/MsgMessages.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/MsgMessages.java @@ -17,4 +17,16 @@ public interface MsgMessages { Notice otherMessagesDisabled(); Notice otherMessagesEnabled(); + Placeholders placeholders(); + + interface Placeholders { + String loading(); + + String msgEnabled(); + String msgDisabled(); + + String socialSpyEnabled(); + String socialSpyDisabled(); + } + } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/PLMsgMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/PLMsgMessages.java index ece54845e..581083182 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/PLMsgMessages.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/messages/PLMsgMessages.java @@ -37,4 +37,21 @@ public class PLMsgMessages extends OkaeriConfig implements MsgMessages { public Notice otherMessagesDisabled = Notice.chat("Wiadomości prywatne zostały wyłączone dla gracza {PLAYER}!"); public Notice otherMessagesEnabled = Notice.chat("Wiadomości prywatne zostały włączone dla gracza {PLAYER}!"); + @Comment("# Formatowanie placeholderów") + public PLPlaceholders placeholders = new PLPlaceholders(); + + @Getter + @Accessors(fluent = true) + public static class PLPlaceholders extends OkaeriConfig implements MsgMessages.Placeholders { + private String loading = "Ładowanie..."; + + private String msgEnabled = "Włączone"; + private String msgDisabled = "Wyłączone"; + private String socialSpyEnabled = "Włączony"; + private String socialSpyDisabled = "Wyłączony"; + } + + public PLPlaceholders placeholders() { + return this.placeholders; + } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleRepository.java index 51c429ae0..3853c49e8 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleRepository.java @@ -4,7 +4,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -interface MsgToggleRepository { +public interface MsgToggleRepository { CompletableFuture getPrivateChatState(UUID uuid); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleServiceImpl.java index 227d40e5d..d3d063523 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/toggle/MsgToggleServiceImpl.java @@ -1,41 +1,42 @@ package com.eternalcode.core.feature.msg.toggle; +import com.eternalcode.core.feature.msg.MsgPlaceholderSetup; import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; +import com.eternalcode.core.placeholder.cache.AsyncPlaceholderCacheRegistry; +import com.eternalcode.core.placeholder.cache.AsyncPlaceholderCached; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; @Service class MsgToggleServiceImpl implements MsgToggleService { private final MsgToggleRepository msgToggleRepository; - private final ConcurrentHashMap cachedToggleStates; + private final AsyncPlaceholderCacheRegistry cacheRegistry; @Inject - MsgToggleServiceImpl(MsgToggleRepository msgToggleRepository) { - this.cachedToggleStates = new ConcurrentHashMap<>(); + MsgToggleServiceImpl(MsgToggleRepository msgToggleRepository, AsyncPlaceholderCacheRegistry cacheRegistry) { this.msgToggleRepository = msgToggleRepository; - + this.cacheRegistry = cacheRegistry; } - @Override public CompletableFuture getState(UUID playerUniqueId) { - if (this.cachedToggleStates.containsKey(playerUniqueId)) { - return CompletableFuture.completedFuture(this.cachedToggleStates.get(playerUniqueId)); - } - - return this.msgToggleRepository.getPrivateChatState(playerUniqueId); + return this.msgToggleRepository.getPrivateChatState(playerUniqueId) + .thenApply(state -> { + this.withCache(cache -> cache.update(playerUniqueId, state)); + return state; + }); } @Override public CompletableFuture setState(UUID playerUniqueId, MsgState state) { - this.cachedToggleStates.put(playerUniqueId, state); + this.withCache(cache -> cache.update(playerUniqueId, state)); return this.msgToggleRepository.setPrivateChatState(playerUniqueId, state) .exceptionally(throwable -> { - this.cachedToggleStates.remove(playerUniqueId); + this.withCache(cache -> cache.invalidate(playerUniqueId)); return null; }); } @@ -48,4 +49,9 @@ public CompletableFuture toggleState(UUID playerUniqueId) { .thenApply(aVoid -> newState); }); } + + private void withCache(Consumer> action) { + this.cacheRegistry.get(MsgPlaceholderSetup.MSG_STATE_CACHE_KEY) + .ifPresent(action); + } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCacheController.java b/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCacheController.java new file mode 100644 index 000000000..b8d182a97 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCacheController.java @@ -0,0 +1,37 @@ +package com.eternalcode.core.placeholder.cache; + +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Controller; +import com.eternalcode.core.publish.Subscribe; +import com.eternalcode.core.publish.event.EternalReloadEvent; +import com.eternalcode.core.publish.event.EternalShutdownEvent; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +@Controller +class AsyncPlaceholderCacheController implements Listener { + + private final AsyncPlaceholderCacheRegistry cacheRegistry; + + @Inject + AsyncPlaceholderCacheController(AsyncPlaceholderCacheRegistry cacheRegistry) { + this.cacheRegistry = cacheRegistry; + } + + @EventHandler(priority = EventPriority.MONITOR) + void onPlayerQuit(PlayerQuitEvent event) { + this.cacheRegistry.invalidatePlayer(event.getPlayer().getUniqueId()); + } + + @Subscribe + void onDisable(EternalShutdownEvent event) { + this.cacheRegistry.invalidateAll(); + } + + @Subscribe + void onReload(EternalReloadEvent event) { + this.cacheRegistry.invalidateAll(); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCacheRegistry.java b/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCacheRegistry.java new file mode 100644 index 000000000..7b20a99b4 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCacheRegistry.java @@ -0,0 +1,53 @@ +package com.eternalcode.core.placeholder.cache; + +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Service; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +@Service +public class AsyncPlaceholderCacheRegistry { + + private static final Duration DEFAULT_EXPIRE_DURATION = Duration.ofMinutes(30); + + private final Map> caches = new ConcurrentHashMap<>(); + + @Inject + public AsyncPlaceholderCacheRegistry() { + } + + public AsyncPlaceholderCached register(String key, Function> loader) { + return this.register(key, loader, DEFAULT_EXPIRE_DURATION); + } + + public AsyncPlaceholderCached register(String key, Function> loader, Duration expireAfterWrite) { + AsyncPlaceholderCached cache = new AsyncPlaceholderCached<>(loader, expireAfterWrite); + this.caches.put(key, cache); + return cache; + } + + @SuppressWarnings("unchecked") + public Optional> get(String key) { + return Optional.ofNullable((AsyncPlaceholderCached) this.caches.get(key)); + } + + public void invalidatePlayer(UUID uuid) { + this.caches.values().forEach(cache -> cache.invalidate(uuid)); + } + + public void invalidateAll() { + this.caches.values().forEach(AsyncPlaceholderCached::clear); + } + + public void unregister(String key) { + AsyncPlaceholderCached cache = this.caches.remove(key); + if (cache != null) { + cache.clear(); + } + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCached.java b/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCached.java new file mode 100644 index 000000000..1e0bcd597 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/placeholder/cache/AsyncPlaceholderCached.java @@ -0,0 +1,57 @@ +package com.eternalcode.core.placeholder.cache; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.time.Duration; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +public class AsyncPlaceholderCached { + + private final Cache cache; + private final Function> loader; + private final Map> loading = new ConcurrentHashMap<>(); + + public AsyncPlaceholderCached(Function> loader, Duration expireAfterWrite) { + this.loader = loader; + this.cache = CacheBuilder.newBuilder() + .expireAfterWrite(expireAfterWrite) + .build(); + } + + public T getCached(UUID uuid) { + T cached = this.cache.getIfPresent(uuid); + if (cached != null) { + return cached; + } + + this.loading.computeIfAbsent(uuid, key -> + this.loader.apply(key).whenComplete((value, throwable) -> { + if (value != null) { + this.cache.put(key, value); + } + this.loading.remove(key); + }) + ); + + return null; + } + public void update(UUID uuid, T value) { + this.cache.put(uuid, value); + } + + public void invalidate(UUID uuid) { + this.cache.invalidate(uuid); + } + + public void clear() { + this.cache.invalidateAll(); + } + + public boolean contains(UUID uuid) { + return this.cache.getIfPresent(uuid) != null; + } +} diff --git a/eternalcore-plugin/build.gradle.kts b/eternalcore-plugin/build.gradle.kts index 0bf93dae7..bc0e2fe24 100644 --- a/eternalcore-plugin/build.gradle.kts +++ b/eternalcore-plugin/build.gradle.kts @@ -37,6 +37,10 @@ dependencies { tasks { runServer { minecraftVersion("1.21.8") - downloadPlugins.modrinth("luckperms", "v${Versions.LUCKPERMS}-bukkit") + + downloadPlugins { + modrinth("luckperms", "v${Versions.LUCKPERMS}-bukkit") + modrinth("placeholderapi", Versions.PLACEHOLDER_API) + } } }