diff --git a/eternalcore-afk-addon/build.gradle.kts b/eternalcore-afk-addon/build.gradle.kts new file mode 100644 index 000000000..c3dc3bc5e --- /dev/null +++ b/eternalcore-afk-addon/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + `java-library` + `eternalcode-java` + `eternalcore-repositories` + id("com.github.johnrengelman.shadow") + id("net.minecrell.plugin-yml.bukkit") +} + +group = "com.eternalcode.core" +version = "1.0.0" + +repositories { + maven("https://jitpack.io") +} + +dependencies { + compileOnly(project(":eternalcore-api")) + + compileOnly("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") + + implementation("net.dzikoysk:cdn:${Versions.CDN_CONFIGS}") + implementation("com.github.unldenis:holoeasy:3.1.1") + implementation("com.eternalcode:eternalcode-commons-adventure:1.1.1") + implementation("com.eternalcode:eternalcode-commons-shared:1.1.1") +} + +bukkit { + main = "com.eternalcode.core.addon.afk.AfkAddonPlugin" + apiVersion = "1.13" + prefix = "EternalCore-AfkAddon" + author = "P1otrulla" + name = "EternalCore-DiscordSpyAddon" + description = "AfkAddon for EternalCore!" + website = "https://eternalcode.pl" + version = "${project.version}" + depend = listOf("EternalCore", "ProtocolLib") +} + +tasks.shadowJar { + archiveFileName.set("EternalCore-AfkAddon v${project.version}.jar") + + listOf( + "com.github.unldenis", + "net.dzikoysk" + ).forEach { + relocate(it, "com.eternalcode.core.addon.afk.shared.$it") + } +} + + + diff --git a/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkAddonPlugin.java b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkAddonPlugin.java new file mode 100644 index 000000000..88f65d9cc --- /dev/null +++ b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkAddonPlugin.java @@ -0,0 +1,42 @@ +package com.eternalcode.core.addon.afk; + +import com.eternalcode.commons.adventure.AdventureLegacyColorPostProcessor; +import com.eternalcode.commons.adventure.AdventureLegacyColorPreProcessor; +import com.eternalcode.core.EternalCoreApi; +import com.eternalcode.core.EternalCoreApiProvider; +import com.eternalcode.core.addon.afk.config.ConfigService; +import com.eternalcode.core.addon.afk.config.PluginConfiguration; +import com.eternalcode.core.feature.afk.AfkService; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Server; +import org.bukkit.plugin.java.JavaPlugin; + +public class AfkAddonPlugin extends JavaPlugin { + + private ConfigService configService; + private PluginConfiguration configuration; + + private AfkHologramService afkHologramService; + + private MiniMessage miniMessage; + + @Override + public void onEnable() { + this.miniMessage = MiniMessage.builder() + .postProcessor(new AdventureLegacyColorPostProcessor()) + .preProcessor(new AdventureLegacyColorPreProcessor()) + .build(); + + this.configService = new ConfigService(this.getDataFolder()); + this.configuration = this.configService.load(new PluginConfiguration()); + + AfkService afkService = EternalCoreApiProvider.provide().getAfkService(); + Server server = this.getServer(); + + this.afkHologramService = new AfkHologramService(this.configuration, this.miniMessage, afkService, this); + server.getPluginManager().registerEvents(new AfkController(this.afkHologramService, afkService), this); + server.getScheduler().runTaskTimer(this, new AfkHologramTask(this.afkHologramService, server), 20L, 20L); + } + + +} diff --git a/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkController.java b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkController.java new file mode 100644 index 000000000..642175f54 --- /dev/null +++ b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkController.java @@ -0,0 +1,30 @@ +package com.eternalcode.core.addon.afk; + +import com.eternalcode.core.feature.afk.AfkService; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; + +import java.util.UUID; + +public class AfkController implements Listener { + + private final AfkHologramService afkHologramService; + private final AfkService afkService; + + public AfkController(AfkHologramService afkHologramService, AfkService afkService) { + this.afkHologramService = afkHologramService; + this.afkService = afkService; + } + + @EventHandler + void onMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + UUID uuid = player.getUniqueId(); + + if (this.afkService.isAfk(uuid)) { + this.afkHologramService.deleteHologram(uuid); + } + } +} diff --git a/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkHologramService.java b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkHologramService.java new file mode 100644 index 000000000..a841b69a8 --- /dev/null +++ b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkHologramService.java @@ -0,0 +1,105 @@ +package com.eternalcode.core.addon.afk; + +import com.eternalcode.commons.time.DurationParser; +import com.eternalcode.core.addon.afk.config.PluginConfiguration; +import com.eternalcode.core.feature.afk.Afk; +import com.eternalcode.core.feature.afk.AfkService; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Location; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.holoeasy.HoloEasy; +import org.holoeasy.config.HologramKey; +import org.holoeasy.hologram.Hologram; +import org.holoeasy.pool.IHologramPool; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection; +import static org.holoeasy.builder.HologramBuilder.hologram; +import static org.holoeasy.builder.HologramBuilder.item; +import static org.holoeasy.builder.HologramBuilder.textline; + +public class AfkHologramService { + + private static final String HOLOGRAM_NAME_PREFIX = "afk#%s,%s,%s,%s"; + private static final int HOLOGRAM_VIEW_DISTANCE = 0x32; + + private final Map holograms = new HashMap<>(); + private final PluginConfiguration configuration; + private final MiniMessage miniMessage; + private final AfkService afkService; + private final IHologramPool pool; + private final Plugin plugin; + private final Server server; + + public AfkHologramService(PluginConfiguration configuration, MiniMessage miniMessage, AfkService afkService, Plugin plugin) { + this.configuration = configuration; + this.miniMessage = miniMessage; + this.afkService = afkService; + this.plugin = plugin; + this.server = plugin.getServer(); + + this.pool = HoloEasy.startPool(plugin, HOLOGRAM_VIEW_DISTANCE); + } + + public void createHologram(Player player) { + if (!this.afkService.isAfk(player.getUniqueId())) { + return; + } + Afk afk = this.afkService.getAfk(player.getUniqueId()); + + Duration afkSince = Duration.between(afk.getStart(), Instant.now()); + Location location = player.getLocation().clone(); + + HologramKey key = new HologramKey(this.plugin, fetchName(location), this.pool); + Hologram hologram = hologram(key, this.fetchLocation(player), () -> { + if (this.configuration.hologramItemEnabled) { + item(new ItemStack(this.configuration.hologramItem)); + } + this.configuration.hologramEntries.stream() + .map(entry -> entry.replace("{TIME}", DurationParser.TIME_UNITS.format(afkSince))) + .forEach(entry -> textline(legacySection().serialize(this.miniMessage.deserialize(entry)))); + }); + + this.holograms.put(player.getUniqueId(), key); + + this.showForAll(hologram); + } + + public void deleteHologram(UUID uuid) { + HologramKey hologramKey = this.holograms.remove(uuid); + + this.pool.remove(hologramKey); + } + + public void updateHologram(Player player) { + this.deleteHologram(player.getUniqueId()); + this.createHologram(player); + } + + void showForAll(Hologram hologram) { + this.server.getOnlinePlayers().forEach(hologram::show); + } + + String fetchName(Location location) { + return String.format(HOLOGRAM_NAME_PREFIX, location.getWorld(), location.getX(), location.getY(), location.getZ()); + } + + Location fetchLocation(Player player) { + return player.getLocation().add(0, 2, 0).clone(); + } + + public Set getAfkPlayers() { + return Collections.unmodifiableSet(this.holograms.keySet()); + } +} diff --git a/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkHologramTask.java b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkHologramTask.java new file mode 100644 index 000000000..3d48cadad --- /dev/null +++ b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/AfkHologramTask.java @@ -0,0 +1,31 @@ +package com.eternalcode.core.addon.afk; + +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class AfkHologramTask implements Runnable { + + private final AfkHologramService afkHologramService; + private final Server server; + + public AfkHologramTask(AfkHologramService afkHologramService, Server server) { + this.afkHologramService = afkHologramService; + this.server = server; + } + + @Override + public void run() { + for (UUID uuid : this.afkHologramService.getAfkPlayers()) { + Player player = this.server.getPlayer(uuid); + + if (player == null) { + this.afkHologramService.deleteHologram(uuid); + continue; + } + + this.afkHologramService.updateHologram(player); + } + } +} diff --git a/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/ConfigService.java b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/ConfigService.java new file mode 100644 index 000000000..a51596269 --- /dev/null +++ b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/ConfigService.java @@ -0,0 +1,48 @@ +package com.eternalcode.core.addon.afk.config; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; +import net.dzikoysk.cdn.Cdn; +import net.dzikoysk.cdn.CdnFactory; +import net.dzikoysk.cdn.reflect.Visibility; + +public class ConfigService { + + private final Cdn cdn = CdnFactory + .createYamlLike() + .getSettings() + .withMemberResolver(Visibility.PRIVATE) + .build(); + + private final Set configs = new HashSet<>(); + private final File dataFolder; + + public ConfigService(File dataFolder) { + this.dataFolder = dataFolder; + } + + public T load(T config) { + cdn.load(config.resource(this.dataFolder), config).orThrow(RuntimeException::new); + + cdn.render(config, config.resource(this.dataFolder)).orThrow(RuntimeException::new); + + this.configs.add(config); + + return config; + } + + public T save(T config) { + cdn.render(config, config.resource(this.dataFolder)).orThrow(RuntimeException::new); + + this.configs.add(config); + + return config; + } + + public void reload() { + for (ReloadableConfig config : this.configs) { + load(config); + } + } +} diff --git a/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/PluginConfiguration.java b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/PluginConfiguration.java new file mode 100644 index 000000000..95fe6718d --- /dev/null +++ b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/PluginConfiguration.java @@ -0,0 +1,25 @@ +package com.eternalcode.core.addon.afk.config; + +import net.dzikoysk.cdn.source.Resource; +import net.dzikoysk.cdn.source.Source; +import org.bukkit.Material; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +public class PluginConfiguration implements ReloadableConfig { + + public boolean hologramItemEnabled = true; + public Material hologramItem = Material.BARRIER; + + public List hologramEntries = Arrays.asList( + "&6This player is &cAFK", + "&6Since: &c{TIME}" + ); + + @Override + public Resource resource(File folder) { + return Source.of(folder, "config.yml"); + } +} diff --git a/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/ReloadableConfig.java b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/ReloadableConfig.java new file mode 100644 index 000000000..11509ae03 --- /dev/null +++ b/eternalcore-afk-addon/src/main/java/com/eternalcode/core/addon/afk/config/ReloadableConfig.java @@ -0,0 +1,9 @@ +package com.eternalcode.core.addon.afk.config; + +import java.io.File; +import net.dzikoysk.cdn.source.Resource; + +public interface ReloadableConfig { + + Resource resource(File folder); +} diff --git a/eternalcore-api/src/main/java/com/eternalcode/core/feature/afk/AfkService.java b/eternalcore-api/src/main/java/com/eternalcode/core/feature/afk/AfkService.java index 0baf1a58e..f481b0966 100644 --- a/eternalcore-api/src/main/java/com/eternalcode/core/feature/afk/AfkService.java +++ b/eternalcore-api/src/main/java/com/eternalcode/core/feature/afk/AfkService.java @@ -15,6 +15,14 @@ public interface AfkService { */ boolean isAfk(UUID playerUniqueId); + /** + * Returns the AFK status of a uniqueId. + * + * @param playerUniqueId Unique identifier of the player. + * @return Afk object representing player's AFK status. + */ + Afk getAfk(UUID playerUniqueId); + /** * Switches player's AFK status. * @@ -46,4 +54,4 @@ public interface AfkService { * @return Created an Afk object representing player's AFK status. */ Afk markAfk(UUID playerUniqueId, AfkReason reason); -} \ No newline at end of file +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkServiceImpl.java index 3cc439e82..c49ee7aad 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkServiceImpl.java @@ -104,6 +104,11 @@ public boolean isAfk(UUID playerUniqueId) { return this.afkByPlayer.containsKey(playerUniqueId); } + @Override + public Afk getAfk(UUID playerUniqueId) { + return this.afkByPlayer.get(playerUniqueId); + } + @ApiStatus.Internal boolean isInactive(UUID playerUniqueId) { Instant now = Instant.now(); diff --git a/settings.gradle.kts b/settings.gradle.kts index 9969928a8..ef13c80c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,4 +6,5 @@ include(":eternalcore-paper") include(":eternalcore-plugin") include(":eternalcore-docs-api") include(":eternalcore-docs-generate") -include(":eternalcore-api-example") \ No newline at end of file +include(":eternalcore-api-example") +include("eternalcore-afk-addon")