Add FancyVisuals plugin

This commit is contained in:
Oliver
2025-03-15 19:04:44 +01:00
parent 75172c39a4
commit 2a9ca30f64
29 changed files with 1627 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
fancyhologramsVersion=2.4.2
fancyvisualsVersion=0.0.1
fancylibVersion=36
fancysitulaVersion=0.0.13
jdbVersion=1.0.0

120
plugins/fancyvisuals/.gitignore vendored Normal file
View File

@@ -0,0 +1,120 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
run/
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting
# Cache of project
.gradletasknamecache
**/build/
# Common working directory
run/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

View File

@@ -0,0 +1,51 @@
# FancyVisuals
**Do not use this plugin in production! It is still in development and may contain bugs and unfinished features.**
This is a plugin to customise most visual components of your minecraft server. This includes the scoreboard, tablist,
bossbar, actionbar, title, chat and nametags. This plugin is highly customisable and can be used to create a unique
experience for your players.
This plugin is packet based (powered by FancySitula), meaning it is blazing fast and has no impact on server
performance. You can use placeholders by PlaceholderAPI anywhere in the plugin.
## Features
The plugin is divided into multiple modules, each of which can be configured individually.
### Nametags
- With the nametags module, you can customise the nametags (text above the player's head) of players
- The nametags are implemented using display entities
- The nametags can have multiple lines and a configurable background
- You can use MiniMessage for coloring and formatting
- Placeholders by PlaceholderAPI are supported
### Scoreboard
Comming soon
### Tablist
Comming soon
### Bossbar
Comming soon
### Actionbar
Comming soon
### Title & Subtitle
Comming soon
### Chat
Comming soon
## Installation
Paper **1.20.5** - **1.21.4** with **Java 21** (or higher) is required.
**Spigot** is **not** supported.

View File

@@ -0,0 +1,57 @@
plugins {
id("java-library")
id("maven-publish")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
}
tasks {
publishing {
repositories {
maven {
name = "fancypluginsReleases"
url = uri("https://repo.fancyplugins.de/releases")
credentials(PasswordCredentials::class)
authentication {
isAllowInsecureProtocol = true
create<BasicAuthentication>("basic")
}
}
maven {
name = "fancypluginsSnapshots"
url = uri("https://repo.fancyplugins.de/snapshots")
credentials(PasswordCredentials::class)
authentication {
isAllowInsecureProtocol = true
create<BasicAuthentication>("basic")
}
}
}
publications {
create<MavenPublication>("maven") {
groupId = "de.oliver"
artifactId = "FancyVisuals"
version = findProperty("fancyvisualsVersion") as String
from(project.components["java"])
}
}
}
java {
withSourcesJar()
withJavadocJar()
}
javadoc {
options.encoding = Charsets.UTF_8.name()
}
compileJava {
options.encoding = Charsets.UTF_8.name()
options.release = 21
}
}

View File

@@ -0,0 +1,34 @@
package de.oliver.fancyvisuals.api;
/**
* Enum representing different contexts in which operations can be performed.
* Each context is associated with a specific priority level that indicates its specificity.
*/
public enum Context {
SERVER(1),
WORLD(2),
GROUP(3),
PLAYER(4),
;
private final int priority;
Context(int priority) {
this.priority = priority;
}
public String getName() {
return name().toLowerCase();
}
/**
* Retrieves the priority level associated with this context.
* Higher priority levels indicate more specific contexts.
*
* @return the priority level as an integer
*/
public int getPriority() {
return priority;
}
}

View File

@@ -0,0 +1,11 @@
package de.oliver.fancyvisuals.api;
import de.oliver.fancyvisuals.api.nametags.NametagRepository;
import org.bukkit.plugin.java.JavaPlugin;
public interface FancyVisualsAPI {
JavaPlugin getPlugin();
NametagRepository getNametagRepository();
}

View File

@@ -0,0 +1,33 @@
package de.oliver.fancyvisuals.api.nametags;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public record Nametag(
@SerializedName("text_lines")
@NotNull List<String> textLines,
@SerializedName("background_color")
@NotNull String backgroundColor,
@SerializedName("text_shadow")
@NotNull Boolean textShadow,
@SerializedName("text_alignment")
@NotNull TextAlignment textAlignment
) {
public enum TextAlignment {
@SerializedName("left")
LEFT,
@SerializedName("right")
RIGHT,
@SerializedName("center")
CENTER
}
}

View File

@@ -0,0 +1,55 @@
package de.oliver.fancyvisuals.api.nametags;
import de.oliver.fancyvisuals.api.Context;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* The {@code NametagRepository} interface provides methods for retrieving {@code NametagStore} and {@code Nametag}
* instances based on different contexts.
*/
public interface NametagRepository {
/**
* The default {@code Nametag} instance used when no specific nametag is found for a given context or player.
*/
Nametag DEFAULT_NAMETAG = new Nametag(
List.of("<gradient:#8c0010:#803c12>%player_name%</gradient>"),
"#000000",
true,
Nametag.TextAlignment.CENTER
);
/**
* Retrieves the {@code NametagStore} associated with the given context.
*
* @param context the context for which the store is to be retrieved. This determines if the store is for SERVER, WORLD, GROUP, or PLAYER.
* @return the NametagStore associated with the provided context.
*/
@NotNull NametagStore getStore(@NotNull Context context);
/**
* Retrieves the {@code Nametag} associated with the specified {@code id} within the given {@code context}.
*
* @param context the context for which the nametag is to be retrieved. This determines if the nametag is for SERVER, WORLD, GROUP, or PLAYER.
* @param id the unique identifier for the nametag.
* @return the Nametag associated with the given id; may return null if no such nametag is found within the specified context.
*/
default @Nullable Nametag getNametag(@NotNull Context context, @NotNull String id) {
return getStore(context).getNametag(id);
}
/**
* Retrieves the appropriate {@code Nametag} for the specified {@code Player} based on various contexts.
* The method checks the PLAYER, GROUP, WORLD, and SERVER contexts in order, returning the first matching nametag found.
* If no matching nametag is found in any context, a default nametag is returned.
*
* @param player the Player for whom the nametag is being retrieved
* @return the Nametag associated with the player, or a default nametag if no specific nametag is found
*/
@NotNull Nametag getNametagForPlayer(@NotNull Player player);
}

View File

@@ -0,0 +1,44 @@
package de.oliver.fancyvisuals.api.nametags;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* The {@code NametagStore} interface defines operations for storing, retrieving, and managing {@code Nametag} objects
* associated with unique identifiers.
*/
public interface NametagStore {
/**
* Associates the specified {@code Nametag} with the provided {@code id} in the store.
*
* @param id the unique identifier for the nametag.
* @param nametag the Nametag object to be associated with the specified id.
*/
void setNametag(@NotNull String id, @NotNull Nametag nametag);
/**
* Retrieves the {@code Nametag} associated with the specified {@code id}.
*
* @param id the unique identifier for the nametag.
* @return the Nametag associated with the given id; may return null if no such nametag is found.
*/
@Nullable Nametag getNametag(@NotNull String id);
/**
* Removes the {@code Nametag} associated with the specified {@code id} from the store.
*
* @param id the unique identifier for the nametag to be removed.
*/
void removeNametag(@NotNull String id);
/**
* Retrieves a list of all the Nametags in the store.
*
* @return a list of Nametag objects; the list may be empty if no nametags are present in the store.
*/
@NotNull List<Nametag> getNametags();
}

View File

@@ -0,0 +1,131 @@
import net.minecrell.pluginyml.paper.PaperPluginDescription
plugins {
id("java-library")
id("xyz.jpenilla.run-paper")
id("com.gradleup.shadow")
id("net.minecrell.plugin-yml.paper")
}
runPaper.folia.registerTask()
allprojects {
group = "de.oliver"
version = findProperty("fancyvisualsVersion") as String
description = "Simple, lightweight and fast visual plugin using packets"
repositories {
mavenLocal()
mavenCentral()
maven(url = "https://repo.papermc.io/repository/maven-public/")
maven(url = "https://repo.fancyplugins.de/releases")
maven(url = "https://repo.lushplugins.org/releases")
maven(url = "https://jitpack.io")
}
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
implementation(project(":plugins:fancyvisuals:api"))
compileOnly("de.oliver:FancyLib:33") // loaded in FancyVisualLoader
compileOnly("de.oliver:FancySitula:0.0.13") // loaded in FancyVisualLoader
compileOnly("de.oliver.FancyAnalytics:api:0.0.8") // loaded in FancyVisualLoader
compileOnly("de.oliver.FancyAnalytics:logger:0.0.5") // loaded in FancyVisualLoader
implementation("org.lushplugins:ChatColorHandler:4.0.0")
compileOnly("com.github.MilkBowl:VaultAPI:1.7.1")
// commands
implementation("org.incendo:cloud-core:2.0.0")
implementation("org.incendo:cloud-paper:2.0.0-beta.10")
implementation("org.incendo:cloud-annotations:2.0.0")
annotationProcessor("org.incendo:cloud-annotations:2.0.0")
}
paper {
main = "de.oliver.fancyvisuals.FancyVisuals"
bootstrapper = "de.oliver.fancyvisuals.loaders.FancyVisualsBootstrapper"
loader = "de.oliver.fancyvisuals.loaders.FancyVisualsLoader"
foliaSupported = true
version = findProperty("fancyvisualsVersion") as String
description = "Simple, lightweight and fast visuals plugin using packets"
apiVersion = "1.19"
serverDependencies {
register("PlaceholderAPI") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("MiniPlaceholders") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("LuckPerms") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("PermissionsEx") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
}
}
tasks {
runServer {
minecraftVersion("1.21.4")
downloadPlugins {
hangar("ViaVersion", "5.0.3")
hangar("ViaBackwards", "5.0.3")
hangar("PlaceholderAPI", "2.11.6")
// modrinth("multiverse-core", "4.3.11")
}
}
shadowJar {
archiveClassifier.set("")
dependsOn(":plugins:fancyvisuals:api:jar")
}
compileJava {
options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything
options.release = 21
// For cloud-annotations, see https://cloud.incendo.org/annotations/#command-components
options.compilerArgs.add("-parameters")
}
javadoc {
options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything
}
processResources {
filteringCharset = Charsets.UTF_8.name() // We want UTF-8 for everything
val props = mapOf(
"description" to project.description,
"version" to project.version,
"hash" to getCurrentCommitHash(),
)
inputs.properties(props)
filesMatching("paper-plugin.yml") {
expand(props)
}
filesMatching("version.yml") {
expand(props)
}
}
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
fun getCurrentCommitHash(): String {
return ""
}

View File

@@ -0,0 +1,118 @@
package de.oliver.fancyvisuals;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import de.oliver.fancyanalytics.logger.ExtendedFancyLogger;
import de.oliver.fancyanalytics.logger.LogLevel;
import de.oliver.fancylib.FancyLib;
import de.oliver.fancysitula.api.IFancySitula;
import de.oliver.fancyvisuals.analytics.AnalyticsManager;
import de.oliver.fancyvisuals.api.FancyVisualsAPI;
import de.oliver.fancyvisuals.api.nametags.NametagRepository;
import de.oliver.fancyvisuals.config.FancyVisualsConfig;
import de.oliver.fancyvisuals.config.NametagConfig;
import de.oliver.fancyvisuals.nametags.listeners.NametagListeners;
import de.oliver.fancyvisuals.nametags.store.JsonNametagRepository;
import de.oliver.fancyvisuals.nametags.visibility.PlayerNametagScheduler;
import de.oliver.fancyvisuals.playerConfig.JsonPlayerConfigStore;
import de.oliver.fancyvisuals.utils.VaultHelper;
import org.bukkit.Bukkit;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class FancyVisuals extends JavaPlugin implements FancyVisualsAPI {
private static final ExtendedFancyLogger logger = IFancySitula.LOGGER;
private static FancyVisuals instance;
private final AnalyticsManager analyticsManager;
private final FancyVisualsConfig fancyVisualsConfig;
private final NametagConfig nametagConfig;
private ExecutorService workerExecutor;
private JsonPlayerConfigStore playerConfigStore;
private NametagRepository nametagRepository;
private PlayerNametagScheduler nametagScheduler;
public FancyVisuals() {
instance = this;
this.analyticsManager = new AnalyticsManager("34c5a33d-0ff0-48b1-8b1c-53620a690c6e", "981ce185-c961-4618-bf61-71a8ed6c3962", "SxIBSDA2MDVkMGUwOTk3MzQ3NjCmP0UU");
this.fancyVisualsConfig = new FancyVisualsConfig();
this.nametagConfig = new NametagConfig();
}
public static FancyVisuals get() {
return instance;
}
public static @NotNull ExtendedFancyLogger getFancyLogger() {
return logger;
}
@Override
public void onLoad() {
FancyLib fancyLib = new FancyLib(this);
IFancySitula.LOGGER.setCurrentLevel(LogLevel.DEBUG);
// config
fancyVisualsConfig.load();
nametagConfig.load();
// worker executor
this.workerExecutor = Executors.newFixedThreadPool(
fancyVisualsConfig.getAmountWorkerThreads(),
new ThreadFactoryBuilder()
.setNameFormat("FancyVisualsWorker-%d")
.build()
);
// Player config
playerConfigStore = new JsonPlayerConfigStore();
// Nametags
nametagRepository = new JsonNametagRepository();
nametagScheduler = new PlayerNametagScheduler(workerExecutor, nametagConfig.getDistributionBucketSize());
// analytics
analyticsManager.init();
}
@Override
public void onEnable() {
PluginManager pluginManager = Bukkit.getPluginManager();
// Vault
VaultHelper.loadVault();
// Nametags
nametagScheduler.init();
pluginManager.registerEvents(new NametagListeners(), this);
}
@Override
public void onDisable() {
}
@Override
public JavaPlugin getPlugin() {
return instance;
}
public JsonPlayerConfigStore getPlayerConfigStore() {
return playerConfigStore;
}
@Override
public NametagRepository getNametagRepository() {
return nametagRepository;
}
public PlayerNametagScheduler getNametagScheduler() {
return nametagScheduler;
}
}

View File

@@ -0,0 +1,44 @@
package de.oliver.fancyvisuals.analytics;
import de.oliver.fancyanalytics.api.FancyAnalyticsAPI;
import de.oliver.fancyanalytics.api.MetricSupplier;
import de.oliver.fancyvisuals.FancyVisuals;
import de.oliver.fancyvisuals.api.Context;
import de.oliver.fancyvisuals.api.nametags.NametagRepository;
import java.util.logging.Logger;
public class AnalyticsManager {
private final FancyAnalyticsAPI fa;
public AnalyticsManager(String userId, String projectId, String apiKey) {
this.fa = new FancyAnalyticsAPI(userId, projectId, apiKey);
}
public void init() {
fa.registerDefaultPluginMetrics(FancyVisuals.get());
fa.registerLogger(FancyVisuals.get().getLogger());
fa.registerLogger(Logger.getGlobal());
registerNametagMetrics();
fa.initialize();
}
private void registerNametagMetrics() {
NametagRepository repo = FancyVisuals.get().getNametagRepository();
fa.registerNumberMetric(new MetricSupplier<>("nametag_count_total", () -> {
double count = 0;
for (Context ctx : Context.values()) {
count += repo.getStore(ctx).getNametags().size();
}
return count;
}));
fa.registerNumberMetric(new MetricSupplier<>("nametag_count_player", () -> (double) repo.getStore(Context.PLAYER).getNametags().size()));
fa.registerNumberMetric(new MetricSupplier<>("nametag_count_group", () -> (double) repo.getStore(Context.GROUP).getNametags().size()));
fa.registerNumberMetric(new MetricSupplier<>("nametag_count_world", () -> (double) repo.getStore(Context.WORLD).getNametags().size()));
fa.registerNumberMetric(new MetricSupplier<>("nametag_count_server", () -> (double) repo.getStore(Context.SERVER).getNametags().size()));
}
}

View File

@@ -0,0 +1,19 @@
package de.oliver.fancyvisuals.config;
public class FancyVisualsConfig {
private int amountWorkerThreads;
public FancyVisualsConfig() {
this.amountWorkerThreads = 4;
}
public void load() {
}
public int getAmountWorkerThreads() {
return amountWorkerThreads;
}
}

View File

@@ -0,0 +1,23 @@
package de.oliver.fancyvisuals.config;
public class NametagConfig {
private int distributionBucketSize;
public NametagConfig() {
distributionBucketSize = 10;
}
public void load() {
}
/**
* Retrieves the size of the distribution bucket configured.
*
* @return The size of the distribution bucket.
*/
public int getDistributionBucketSize() {
return distributionBucketSize;
}
}

View File

@@ -0,0 +1,14 @@
package de.oliver.fancyvisuals.loaders;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import org.jetbrains.annotations.NotNull;
public class FancyVisualsBootstrapper implements PluginBootstrap {
@Override
public void bootstrap(@NotNull BootstrapContext context) {
}
}

View File

@@ -0,0 +1,27 @@
package de.oliver.fancyvisuals.loaders;
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
import io.papermc.paper.plugin.loader.PluginLoader;
import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.repository.RemoteRepository;
import org.jetbrains.annotations.NotNull;
public class FancyVisualsLoader implements PluginLoader {
@Override
public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) {
MavenLibraryResolver resolver = new MavenLibraryResolver();
resolver.addRepository(new RemoteRepository.Builder("fancyplugins", "default", "https://repo.fancyplugins.de/releases").build());
// resolver.addRepository(new RemoteRepository.Builder("mavencentral", "default", "https://repo1.maven.org/maven2/").build());
resolver.addDependency(new Dependency(new DefaultArtifact("de.oliver:FancyLib:33"), "compile"));
resolver.addDependency(new Dependency(new DefaultArtifact("de.oliver:FancySitula:0.0.13"), "compile"));
resolver.addDependency(new Dependency(new DefaultArtifact("de.oliver.FancyAnalytics:api:0.0.5"), "compile"));
resolver.addDependency(new Dependency(new DefaultArtifact("de.oliver.FancyAnalytics:logger:0.0.5"), "compile"));
classpathBuilder.addLibrary(resolver);
}
}

View File

@@ -0,0 +1,58 @@
package de.oliver.fancyvisuals.nametags.fake;
import de.oliver.fancyvisuals.api.Context;
import de.oliver.fancyvisuals.api.nametags.Nametag;
import de.oliver.fancyvisuals.api.nametags.NametagRepository;
import de.oliver.fancyvisuals.api.nametags.NametagStore;
import de.oliver.fancyvisuals.utils.VaultHelper;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class FakeNametagRepository implements NametagRepository {
private final Map<Context, NametagStore> stores;
public FakeNametagRepository() {
this.stores = new ConcurrentHashMap<>();
for (Context ctx : Context.values()) {
stores.put(ctx, new FakeNametagStore());
}
}
@Override
public @NotNull NametagStore getStore(@NotNull Context context) {
return stores.get(context);
}
@Override
@NotNull
public Nametag getNametagForPlayer(@NotNull Player player) {
Nametag nametag = getNametag(Context.PLAYER, player.getUniqueId().toString());
if (nametag != null) {
return nametag;
}
if (VaultHelper.isVaultLoaded()) {
nametag = getNametag(Context.GROUP, VaultHelper.getPermission().getPrimaryGroup(player));
if (nametag != null) {
return nametag;
}
}
nametag = getNametag(Context.WORLD, player.getWorld().getName());
if (nametag != null) {
return nametag;
}
nametag = getNametag(Context.SERVER, "global");
if (nametag != null) {
return nametag;
}
return DEFAULT_NAMETAG;
}
}

View File

@@ -0,0 +1,39 @@
package de.oliver.fancyvisuals.nametags.fake;
import de.oliver.fancyvisuals.api.nametags.Nametag;
import de.oliver.fancyvisuals.api.nametags.NametagStore;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class FakeNametagStore implements NametagStore {
private final Map<String, Nametag> nametags;
public FakeNametagStore() {
this.nametags = new ConcurrentHashMap<>();
}
@Override
public void setNametag(@NotNull String id, @NotNull Nametag nametag) {
nametags.put(id, nametag);
}
@Override
public @Nullable Nametag getNametag(@NotNull String id) {
return nametags.getOrDefault(id, null);
}
@Override
public void removeNametag(@NotNull String id) {
nametags.remove(id);
}
@Override
public @NotNull List<Nametag> getNametags() {
return List.copyOf(nametags.values());
}
}

View File

@@ -0,0 +1,29 @@
package de.oliver.fancyvisuals.nametags.listeners;
import de.oliver.fancyvisuals.FancyVisuals;
import de.oliver.fancyvisuals.api.nametags.Nametag;
import de.oliver.fancyvisuals.nametags.visibility.PlayerNametag;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
public class NametagListeners implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Nametag nametag = FancyVisuals.get().getNametagRepository().getNametagForPlayer(player);
PlayerNametag playerNametag = new PlayerNametag(nametag, player);
FancyVisuals.get().getNametagScheduler().add(playerNametag);
}
@EventHandler
public void onPlayerWorldChange(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
Nametag nametag = FancyVisuals.get().getNametagRepository().getNametagForPlayer(player);
PlayerNametag playerNametag = new PlayerNametag(nametag, player);
FancyVisuals.get().getNametagScheduler().add(playerNametag);
}
}

View File

@@ -0,0 +1,119 @@
package de.oliver.fancyvisuals.nametags.store;
import de.oliver.fancylib.jdb.JDB;
import de.oliver.fancyvisuals.api.Context;
import de.oliver.fancyvisuals.api.nametags.Nametag;
import de.oliver.fancyvisuals.api.nametags.NametagRepository;
import de.oliver.fancyvisuals.api.nametags.NametagStore;
import de.oliver.fancyvisuals.utils.VaultHelper;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class JsonNametagRepository implements NametagRepository {
private static final String BASE_PATH = "plugins/FancyVisuals/data/nametags/";
private final JDB jdb;
private final Map<Context, NametagStore> stores;
public JsonNametagRepository() {
this.jdb = new JDB(BASE_PATH);
stores = new ConcurrentHashMap<>();
for (Context ctx : Context.values()) {
stores.put(ctx, new JsonNametagStore(jdb, ctx));
}
initialConfig();
}
@Override
public @NotNull NametagStore getStore(@NotNull Context context) {
return stores.get(context);
}
@Override
@NotNull
public Nametag getNametagForPlayer(@NotNull Player player) {
Nametag nametag = getNametag(Context.PLAYER, player.getUniqueId().toString());
if (nametag != null) {
return nametag;
}
if (VaultHelper.isVaultLoaded()) {
nametag = getNametag(Context.GROUP, VaultHelper.getPermission().getPrimaryGroup(player));
if (nametag != null) {
return nametag;
}
}
nametag = getNametag(Context.WORLD, player.getWorld().getName());
if (nametag != null) {
return nametag;
}
nametag = getNametag(Context.SERVER, "global");
if (nametag != null) {
return nametag;
}
return DEFAULT_NAMETAG;
}
private void initialConfig() {
File baseDir = new File(BASE_PATH);
if (baseDir.exists()) {
return;
}
NametagStore serverStore = getStore(Context.SERVER);
serverStore.setNametag("global", DEFAULT_NAMETAG);
NametagStore worldStore = getStore(Context.WORLD);
worldStore.setNametag("world", new Nametag(
List.of("Overworld", "%player%"),
"#C800AA00",
true,
Nametag.TextAlignment.CENTER
));
worldStore.setNametag("world_nether", new Nametag(
List.of("Nether", "%player%"),
"#C8AA0000",
true,
Nametag.TextAlignment.CENTER
));
worldStore.setNametag("world_the_end", new Nametag(
List.of("The End", "%player%"),
"#C80000AA",
true,
Nametag.TextAlignment.CENTER
));
NametagStore groupStore = getStore(Context.GROUP);
groupStore.setNametag("admin", new Nametag(
List.of("Admin", "%player%"),
"#C8FF0000",
true,
Nametag.TextAlignment.CENTER
));
groupStore.setNametag("moderator", new Nametag(
List.of("Mod", "%player%"),
"#C8FFAA00",
true,
Nametag.TextAlignment.CENTER
));
NametagStore playerStore = getStore(Context.PLAYER);
playerStore.setNametag(UUID.randomUUID().toString(), new Nametag(
List.of("Player", "%player%"),
"#C800FF00",
true,
Nametag.TextAlignment.CENTER
));
}
}

View File

@@ -0,0 +1,67 @@
package de.oliver.fancyvisuals.nametags.store;
import de.oliver.fancylib.jdb.JDB;
import de.oliver.fancyvisuals.FancyVisuals;
import de.oliver.fancyvisuals.api.Context;
import de.oliver.fancyvisuals.api.nametags.Nametag;
import de.oliver.fancyvisuals.api.nametags.NametagStore;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class JsonNametagStore implements NametagStore {
private final Context context;
private final JDB jdb;
public JsonNametagStore(JDB jdb, Context context) {
this.jdb = jdb;
this.context = context;
}
@Override
public void setNametag(@NotNull String id, @NotNull Nametag nametag) {
try {
jdb.set(context.getName() + "/" + id, nametag);
} catch (IOException e) {
FancyVisuals.getFancyLogger().error("Failed to set nametag for id " + id);
FancyVisuals.getFancyLogger().error(e);
}
}
@Override
public @Nullable Nametag getNametag(@NotNull String id) {
Nametag nametag = null;
try {
nametag = jdb.get(context.getName() + "/" + id, Nametag.class);
} catch (IOException e) {
FancyVisuals.getFancyLogger().error("Failed to get nametag for id " + id);
FancyVisuals.getFancyLogger().error(e);
}
return nametag;
}
@Override
public void removeNametag(@NotNull String id) {
jdb.delete(context.getName() + "/" + id);
}
@Override
public @NotNull List<Nametag> getNametags() {
List<Nametag> nametags = new ArrayList<>();
try {
jdb.getAll(context.getName(), Nametag.class);
} catch (IOException e) {
FancyVisuals.getFancyLogger().error("Failed to get all nametags");
FancyVisuals.getFancyLogger().error(e);
}
return nametags;
}
}

View File

@@ -0,0 +1,165 @@
package de.oliver.fancyvisuals.nametags.visibility;
import de.oliver.fancysitula.api.entities.FS_Display;
import de.oliver.fancysitula.api.entities.FS_RealPlayer;
import de.oliver.fancysitula.api.entities.FS_TextDisplay;
import de.oliver.fancysitula.factories.FancySitula;
import de.oliver.fancyvisuals.FancyVisuals;
import de.oliver.fancyvisuals.api.nametags.Nametag;
import de.oliver.fancyvisuals.playerConfig.PlayerConfig;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.joml.Vector3f;
import org.lushplugins.chatcolorhandler.ModernChatColorHandler;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class PlayerNametag {
private final Nametag nametag;
private final Player player;
private final Set<UUID> viewers;
private FS_TextDisplay fsTextDisplay;
public PlayerNametag(Nametag nametag, Player player) {
this.nametag = nametag;
this.player = player;
this.viewers = new HashSet<>();
this.fsTextDisplay = new FS_TextDisplay();
}
public void updateVisibilityForAll() {
cleanViewers();
for (Player viewer : Bukkit.getOnlinePlayers()) {
boolean should = shouldBeVisibleTo(viewer);
boolean is = isVisibleTo(viewer);
if (should && !is) {
showTo(viewer);
} else if (!should && is) {
hideFrom(viewer);
}
}
}
private boolean shouldBeVisibleTo(Player viewer) {
if (!player.isOnline()) {
return false;
}
if (!viewer.getLocation().getWorld().getName().equals(player.getLocation().getWorld().getName())) {
return false;
}
if (player.getUniqueId().equals(viewer.getUniqueId())) {
PlayerConfig playerConfig = FancyVisuals.get().getPlayerConfigStore().getPlayerConfig(player.getUniqueId());
if (!playerConfig.showOwnNametag()) {
return false;
}
}
boolean dead = player.isDead();
if (dead) {
return false;
}
boolean inDistance = isInDistance(viewer.getLocation(), player.getLocation(), 24);
if (!inDistance) {
return false;
}
return true;
}
public void showTo(Player viewer) {
viewers.add(viewer.getUniqueId());
FS_RealPlayer fsViewer = new FS_RealPlayer(viewer);
FancySitula.ENTITY_FACTORY.spawnEntityFor(fsViewer, fsTextDisplay);
updateFor(viewer);
letDisplayRidePlayer(viewer);
}
public void hideFrom(Player viewer) {
viewers.remove(viewer.getUniqueId());
FS_RealPlayer fsViewer = new FS_RealPlayer(viewer);
FancySitula.ENTITY_FACTORY.despawnEntityFor(fsViewer, fsTextDisplay);
}
public void updateFor(Player viewer) {
fsTextDisplay.setTranslation(new Vector3f(0, 0.2f, 0));
fsTextDisplay.setBillboard(FS_Display.Billboard.CENTER);
Color bgColor = Color.fromARGB((int) Long.parseLong(nametag.backgroundColor().substring(1), 16));
fsTextDisplay.setBackground(bgColor.asARGB());
fsTextDisplay.setStyleFlags((byte) 0);
fsTextDisplay.setShadow(nametag.textShadow());
switch (nametag.textAlignment()) {
case LEFT -> fsTextDisplay.setAlignLeft(true);
case RIGHT -> fsTextDisplay.setAlignRight(true);
case CENTER -> {
fsTextDisplay.setAlignLeft(false);
fsTextDisplay.setAlignRight(false);
}
}
StringBuilder text = new StringBuilder();
for (String line : nametag.textLines()) {
text.append(line).append('\n');
}
text.deleteCharAt(text.length() - 1);
fsTextDisplay.setText(ModernChatColorHandler.translate(text.toString(), player));
FS_RealPlayer fsViewer = new FS_RealPlayer(viewer);
FancySitula.ENTITY_FACTORY.setEntityDataFor(fsViewer, fsTextDisplay);
}
public void letDisplayRidePlayer(Player viewer) {
FS_RealPlayer fsViewer = new FS_RealPlayer(viewer);
FancySitula.PACKET_FACTORY.createSetPassengersPacket(
viewer.getEntityId(),
List.of(fsTextDisplay.getId())
).send(fsViewer);
}
public boolean isVisibleTo(Player viewer) {
return viewers.contains(viewer.getUniqueId());
}
public void cleanViewers() {
viewers.removeIf(uuid -> {
Player player = Bukkit.getPlayer(uuid);
return player == null || !player.isOnline() || !player.getWorld().getName().equals(this.player.getWorld().getName());
});
}
public Nametag getNametag() {
return nametag;
}
public Player getPlayer() {
return player;
}
public Set<UUID> getViewers() {
return viewers;
}
private boolean isInDistance(Location loc1, Location loc2, double distance) {
return loc1.distanceSquared(loc2) <= distance * distance;
}
}

View File

@@ -0,0 +1,57 @@
package de.oliver.fancyvisuals.nametags.visibility;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import de.oliver.fancyvisuals.utils.distributedWorkload.DistributedWorkload;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class PlayerNametagScheduler {
/**
* ScheduledExecutorService instance responsible for scheduling periodic execution of
* the DistributedWorkload<PlayerNametag>. It manages the timing and frequency
* of workload distribution, ensuring that tasks are run at fixed intervals.
*/
private final ScheduledExecutorService schedulerExecutor;
/**
* DistributedWorkload instance responsible for managing and executing tasks related
* to PlayerNametag objects. It divides the tasks across multiple buckets and performs
* specified actions on each element. Actions include updating visibility and checking
* whether a PlayerNametag needs to be updated.
*/
private final DistributedWorkload<PlayerNametag> workload;
public PlayerNametagScheduler(ExecutorService workerExecutor, int bucketSize) {
this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder()
.setNameFormat("PlayerNametagScheduler")
.build()
);
this.workload = new DistributedWorkload<>(
"PlayerNametagWorkload",
PlayerNametag::updateVisibilityForAll,
(nt) -> !nt.getPlayer().isOnline(),
bucketSize,
workerExecutor
);
}
/**
* Initializes the PlayerNametagScheduler and starts the periodic execution
* of the DistributedWorkload<PlayerNametag>. The workload is scheduled to
* run at a fixed rate with an initial delay of 0 seconds and a period of
* 25 seconds between subsequent executions.
*/
public void init() {
schedulerExecutor.scheduleWithFixedDelay(workload, 1000, 250, TimeUnit.MILLISECONDS);
}
public void add(PlayerNametag nametag) {
workload.addValue(() -> nametag);
}
}

View File

@@ -0,0 +1,100 @@
package de.oliver.fancyvisuals.playerConfig;
import de.oliver.fancylib.jdb.JDB;
import de.oliver.fancyvisuals.FancyVisuals;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* The {@code JsonPlayerConfigStore} class is responsible for handling player configuration storage and retrieval using JSON.
* It interacts with an underlying JSON database to manage {@code PlayerConfig} instances for individual players.
*/
public class JsonPlayerConfigStore {
private static final String BASE_PATH = "plugins/FancyVisuals/data/player-configs/";
private static final PlayerConfig DEFAULT_PLAYER_CONFIG = new PlayerConfig(true);
private final JDB jdb;
public JsonPlayerConfigStore() {
jdb = new JDB(BASE_PATH);
// Generate default player config if not present
getDefaultPlayerConfig();
}
/**
* Retrieves the PlayerConfig for a specific player identified by their UUID.
* If the PlayerConfig is not found, the default PlayerConfig is returned.
*
* @param uuid the unique identifier of the player whose configuration is being retrieved.
* @return the PlayerConfig associated with the given UUID, or the default PlayerConfig if none is found.
*/
public @NotNull PlayerConfig getPlayerConfig(@NotNull UUID uuid) {
PlayerConfig playerConfig = null;
try {
playerConfig = jdb.get(uuid.toString(), PlayerConfig.class);
} catch (Exception e) {
FancyVisuals.getFancyLogger().error("Failed to get player config for uuid " + uuid);
FancyVisuals.getFancyLogger().error(e);
}
return playerConfig != null ? playerConfig : getDefaultPlayerConfig();
}
/**
* Sets the configuration for a specific player identified by the UUID.
*
* @param uuid the unique identifier of the player for whom the configuration is to be set
* @param playerConfig the PlayerConfig object containing the new configuration settings for the player
*/
public void setPlayerConfig(@NotNull UUID uuid, @NotNull PlayerConfig playerConfig) {
try {
jdb.set(uuid.toString(), playerConfig);
} catch (Exception e) {
FancyVisuals.getFancyLogger().error("Failed to set player config for uuid " + uuid);
FancyVisuals.getFancyLogger().error(e);
}
}
/**
* Deletes the player configuration associated with the specified UUID.
*
* @param uuid the unique identifier of the player whose configuration is to be deleted
*/
public void deletePlayerConfig(@NotNull UUID uuid) {
jdb.delete(uuid.toString());
}
/**
* Retrieves the default PlayerConfig.
* If the default PlayerConfig is not found in the database, it sets and returns the predefined default configuration.
*
* @return the default PlayerConfig. If not present, the predefined default PlayerConfig is set and returned.
*/
public PlayerConfig getDefaultPlayerConfig() {
PlayerConfig playerConfig = null;
try {
playerConfig = jdb.get("default", PlayerConfig.class);
} catch (Exception e) {
FancyVisuals.getFancyLogger().error("Failed to get default player config");
FancyVisuals.getFancyLogger().error(e);
}
if (playerConfig == null) {
playerConfig = DEFAULT_PLAYER_CONFIG;
try {
jdb.set("default", DEFAULT_PLAYER_CONFIG);
} catch (Exception e) {
FancyVisuals.getFancyLogger().error("Failed to set default player config");
FancyVisuals.getFancyLogger().error(e);
}
}
return playerConfig;
}
}

View File

@@ -0,0 +1,14 @@
package de.oliver.fancyvisuals.playerConfig;
import com.google.gson.annotations.SerializedName;
/**
* Represents the configuration settings for a player.
*
* @param showOwnNametag indicates whether the player should see their own nametag.
*/
public record PlayerConfig(
@SerializedName("show_own_nametag")
boolean showOwnNametag
) {
}

View File

@@ -0,0 +1,30 @@
package de.oliver.fancyvisuals.utils;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
public class VaultHelper {
private static boolean vaultLoaded = false;
private static Permission permission;
public static void loadVault() {
vaultLoaded = Bukkit.getPluginManager().getPlugin("Vault") != null;
if (!vaultLoaded) {
return;
}
RegisteredServiceProvider<Permission> rsp = Bukkit.getServer().getServicesManager().getRegistration(Permission.class);
permission = rsp == null ? null : rsp.getProvider();
}
public static boolean isVaultLoaded() {
return vaultLoaded;
}
public static Permission getPermission() {
return permission;
}
}

View File

@@ -0,0 +1,64 @@
package de.oliver.fancyvisuals.utils.distributedWorkload;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* A Bucket provides storage for entries, which are suppliers that yield elements of the
* specified type. It allows adding entries and performing actions on them, with optional
* asynchronous execution.
*
* @param <T> The type of the elements in the bucket
*/
public class Bucket<T> {
private final String name;
private final LinkedList<Supplier<T>> entries;
/**
* Constructs an empty Bucket.
*/
public Bucket(String name) {
this.name = name;
this.entries = new LinkedList<>();
}
/**
* Adds a new entry to the bucket.
*
* @param entry the supplier providing the entry to be added to the bucket
*/
public void addEntry(Supplier<T> entry) {
entries.add(entry);
}
/**
* Executes an action for each entry in the bucket. If the action is set to be
* executed asynchronously, it will run in separate threads; otherwise, it will
* execute in the current thread.
*
* @param action the action to be performed on each entry
* @param escape a condition to determine which entries should be removed after the action is performed
* @param executor the executor service to be used for asynchronous execution
*/
public void executeAction(Consumer<T> action, Predicate<T> escape, ExecutorService executor) {
LinkedList<Supplier<T>> suppliers = new LinkedList<>(entries);
for (Supplier<T> supplier : suppliers) {
executor.submit(() -> action.accept(supplier.get()));
}
entries.removeIf(s -> escape.test(s.get()));
}
public int size() {
return entries.size();
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,100 @@
package de.oliver.fancyvisuals.utils.distributedWorkload;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* DistributedWorkload is a class that manages and executes a workload distributed across multiple buckets.
* Each bucket contains a subset of the workload and executes a specified action on its elements.
*
* @param <T> The type of the elements in the workload
*/
public class DistributedWorkload<T> implements Runnable {
private final String workloadName;
private final Consumer<T> action;
private final Predicate<T> escapeCondition;
private final int bucketSize;
private final ExecutorService executorService;
private final List<Bucket<T>> buckets;
private int currentBucket;
/**
* Creates a new DistributedWorkload instance.
*
* @param workloadName the name of the workload
* @param action the action to be performed on each element of the workload
* @param escapeCondition a condition to determine which elements should be removed after the action is performed
* @param bucketSize the number of buckets into which the workload will be split
* @param executorService the executor service to be used for asynchronous execution
*/
public DistributedWorkload(String workloadName, Consumer<T> action, Predicate<T> escapeCondition, int bucketSize, ExecutorService executorService) {
this.workloadName = workloadName;
this.action = action;
this.escapeCondition = escapeCondition;
this.bucketSize = bucketSize;
this.executorService = executorService;
this.currentBucket = 0;
this.buckets = new ArrayList<>(bucketSize);
for (int i = 0; i < bucketSize; i++) {
this.buckets.add(new Bucket<>("DWL-" + workloadName + "-" + i));
}
}
/**
* Executes the next bucket of workload by invoking the runNextBucket method.
* This method is called when this instance is run as a Runnable.
* Each bucket contains a portion of the workload and executes the specified action
* on each of its elements, either synchronously or asynchronously,
* depending on the configuration of the DistributedWorkload.
*/
@Override
public void run() {
runNextBucket();
}
/**
* Adds a new value to the smallest bucket within the distributed workload.
* The value is supplied by the specified Supplier.
*
* @param valueSupplier the supplier providing the value to be added to the bucket
*/
public void addValue(Supplier<T> valueSupplier) {
Bucket<T> smallestBucket = buckets.getFirst();
for (int i = 1; i < bucketSize; i++) {
if (smallestBucket.size() == 0) {
break;
}
if (buckets.get(i).size() < smallestBucket.size()) {
smallestBucket = buckets.get(i);
}
}
smallestBucket.addEntry(valueSupplier);
}
/**
* Advances to the next bucket in the list and executes its action.
* If the current bucket is the last one in the list, it wraps around to the first bucket.
* The action is executed on each element of the current bucket according to the configured
* conditions and can be run asynchronously if specified.
*/
private void runNextBucket() {
currentBucket++;
if (currentBucket >= buckets.size()) {
currentBucket = 0;
}
Bucket<T> bucket = buckets.get(currentBucket);
bucket.executeAction(action, escapeCondition, executorService);
}
}

View File

@@ -7,6 +7,9 @@ include(":plugins:fancyholograms:implementation_1_20_2")
include(":plugins:fancyholograms:implementation_1_20_1")
include(":plugins:fancyholograms:implementation_1_19_4")
include(":plugins:fancyvisuals")
include(":plugins:fancyvisuals:api")
include(":libraries:common")
include(":libraries:jdb")
include(":libraries:plugin-tests")