Compare commits

..

5 Commits

Author SHA1 Message Date
b7bc25e168 feat: integrate Atlantis Disguises API for packet-level nick/skin support
- Added net.atlantismc.disguises:api:1.0 as compileOnly dependency
- Created AtlantisDisguiseIntegration bridge class
- NameOverrideService now delegates to Atlantis when available
- Falls back to built-in Bukkit display name overrides if absent
- Added AtlantisDisguises to plugin.yml softdepend
2026-06-01 13:22:48 -05:00
0a06223f7c fix: proper JAR naming (NickCore-Paper/Velocity), disable default jar task 2026-06-01 12:55:48 -05:00
578a1d0d55 fix: resolve all IDE warnings - explicit imports, remove unused code, update branding to BaileyCodes/balls.studio, sync Velocity SKIN_UPDATE handler 2026-06-01 12:51:11 -05:00
0e18a64054 fix: remove all unused imports, fields, and variables across codebase 2026-06-01 12:43:20 -05:00
062d398d71 feat: v1.0.4 - cross-server nick persistence, DB-backed rank changes, Velocity auto-sync, author BaileyCodes 2026-06-01 12:30:06 -05:00
23 changed files with 378 additions and 87 deletions

View File

@@ -15,6 +15,7 @@ allprojects {
mavenCentral() mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.extendedclip.com/releases/") // PlaceholderAPI maven("https://repo.extendedclip.com/releases/") // PlaceholderAPI
maven("https://repo.selixe.com/repository/api/") // Atlantis Disguises
} }
} }
@@ -22,9 +23,8 @@ subprojects {
apply(plugin = "java") apply(plugin = "java")
java { java {
toolchain { sourceCompatibility = JavaVersion.toVersion(javaVersion.toInt())
languageVersion.set(JavaLanguageVersion.of(javaVersion.toInt())) targetCompatibility = JavaVersion.toVersion(javaVersion.toInt())
}
} }
tasks.withType<JavaCompile> { tasks.withType<JavaCompile> {

View File

@@ -1,5 +1,5 @@
# Project # Project
projectVersion=1.0.0 projectVersion=1.0.4
projectGroup=com.nickcore projectGroup=com.nickcore
# Minecraft / Paper # Minecraft / Paper

View File

@@ -16,11 +16,15 @@ dependencies {
compileOnly("net.luckperms:api:$luckpermsVersion") compileOnly("net.luckperms:api:$luckpermsVersion")
compileOnly("me.clip:placeholderapi:$placeholderApiVersion") compileOnly("me.clip:placeholderapi:$placeholderApiVersion")
compileOnly("net.atlantismc.disguises:api:1.0")
}
tasks.jar {
enabled = false
} }
tasks.shadowJar { tasks.shadowJar {
archiveClassifier.set("") archiveFileName.set("NickCore-Paper-${project.version}.jar")
archiveBaseName.set("NickCore-Paper")
relocate("com.zaxxer.hikari", "com.nickcore.lib.hikari") relocate("com.zaxxer.hikari", "com.nickcore.lib.hikari")

View File

@@ -7,6 +7,7 @@ import com.nickcore.common.validation.NicknameValidator;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.cache.ProfileCache;
import com.nickcore.paper.command.NickCommand; import com.nickcore.paper.command.NickCommand;
import com.nickcore.paper.gui.GuiManager; import com.nickcore.paper.gui.GuiManager;
import com.nickcore.paper.integration.AtlantisDisguiseIntegration;
import com.nickcore.paper.integration.NickCoreExpansion; import com.nickcore.paper.integration.NickCoreExpansion;
import com.nickcore.paper.integration.VelocityMessaging; import com.nickcore.paper.integration.VelocityMessaging;
import com.nickcore.paper.listener.CommandPreprocessListener; import com.nickcore.paper.listener.CommandPreprocessListener;
@@ -15,7 +16,13 @@ import com.nickcore.paper.listener.TabCompleteListener;
import com.nickcore.paper.repository.DatabaseManager; import com.nickcore.paper.repository.DatabaseManager;
import com.nickcore.paper.repository.SqlNickRepository; import com.nickcore.paper.repository.SqlNickRepository;
import com.nickcore.paper.scheduler.SchedulerAdapter; import com.nickcore.paper.scheduler.SchedulerAdapter;
import com.nickcore.paper.service.*; import com.nickcore.paper.service.ChatService;
import com.nickcore.paper.service.NameOverrideService;
import com.nickcore.paper.service.NametagService;
import com.nickcore.paper.service.NickService;
import com.nickcore.paper.service.PlaceholderService;
import com.nickcore.paper.service.RankService;
import com.nickcore.paper.service.SkinService;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
@@ -24,7 +31,11 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.io.File; import java.io.File;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
/** /**
@@ -92,6 +103,9 @@ public final class NickCorePlugin extends JavaPlugin {
chatService = new ChatService(profileCache, rankService, getLogger()); chatService = new ChatService(profileCache, rankService, getLogger());
nameOverrideService = new NameOverrideService(profileCache); nameOverrideService = new NameOverrideService(profileCache);
// Hook into Atlantis Disguises for packet-level nick/skin support
hookAtlantisDisguises();
// Load data files // Load data files
loadNames(); loadNames();
loadRanks(); loadRanks();
@@ -125,7 +139,7 @@ public final class NickCorePlugin extends JavaPlugin {
Bukkit.getPluginManager().registerEvents(chatService, this); Bukkit.getPluginManager().registerEvents(chatService, this);
Bukkit.getPluginManager().registerEvents(guiManager, this); Bukkit.getPluginManager().registerEvents(guiManager, this);
Bukkit.getPluginManager().registerEvents( Bukkit.getPluginManager().registerEvents(
new CommandPreprocessListener(nameOverrideService, getLogger()), this); new CommandPreprocessListener(nameOverrideService), this);
Bukkit.getPluginManager().registerEvents( Bukkit.getPluginManager().registerEvents(
new TabCompleteListener(nameOverrideService), this); new TabCompleteListener(nameOverrideService), this);
@@ -291,6 +305,18 @@ public final class NickCorePlugin extends JavaPlugin {
skinService.loadConfiguredSkins(skins); skinService.loadConfiguredSkins(skins);
} }
private void hookAtlantisDisguises() {
if (Bukkit.getPluginManager().getPlugin("AtlantisDisguises") != null) {
AtlantisDisguiseIntegration atlantis = new AtlantisDisguiseIntegration(getLogger());
if (atlantis.hook()) {
nameOverrideService.setAtlantis(atlantis);
getLogger().info("Atlantis Disguises integration enabled — using packet-level disguises");
}
} else {
getLogger().info("Atlantis Disguises not found — using built-in name overrides");
}
}
private void registerPlaceholderAPI() { private void registerPlaceholderAPI() {
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
placeholderExpansion = new NickCoreExpansion(this, placeholderService); placeholderExpansion = new NickCoreExpansion(this, placeholderService);
@@ -302,9 +328,11 @@ public final class NickCorePlugin extends JavaPlugin {
private void registerVelocityMessaging(NickCoreConfig config) { private void registerVelocityMessaging(NickCoreConfig config) {
if (config.velocity().enabled()) { if (config.velocity().enabled()) {
velocityMessaging = new VelocityMessaging(this, profileCache, velocityMessaging = new VelocityMessaging(this, profileCache,
placeholderService, nametagService, scheduler, placeholderService, nametagService, nameOverrideService,
getLogger(), config.velocity().serverName()); scheduler, getLogger(), config.velocity().serverName());
velocityMessaging.register(); velocityMessaging.register();
// Let NickService auto-sync changes to other servers
nickService.setVelocityMessaging(velocityMessaging);
getLogger().info("Velocity messaging enabled (server: " + config.velocity().serverName() + ")"); getLogger().info("Velocity messaging enabled (server: " + config.velocity().serverName() + ")");
} }
} }

View File

@@ -1,6 +1,5 @@
package com.nickcore.paper.command; package com.nickcore.paper.command;
import com.nickcore.common.model.NickHistory;
import com.nickcore.common.model.NickProfile; import com.nickcore.common.model.NickProfile;
import com.nickcore.common.validation.InputSanitizer; import com.nickcore.common.validation.InputSanitizer;
import com.nickcore.paper.gui.GuiManager; import com.nickcore.paper.gui.GuiManager;
@@ -23,7 +22,8 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter; import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -39,7 +39,6 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
private static final String PERM_RESET = "nickcore.reset"; private static final String PERM_RESET = "nickcore.reset";
private static final String PERM_RANK = "nickcore.rank.select"; private static final String PERM_RANK = "nickcore.rank.select";
private static final String PERM_SKIN = "nickcore.skin.select"; private static final String PERM_SKIN = "nickcore.skin.select";
private static final String PERM_ADMIN = "nickcore.admin";
private static final String PERM_RELOAD = "nickcore.reload"; private static final String PERM_RELOAD = "nickcore.reload";
private static final String PERM_FORCE = "nickcore.force"; private static final String PERM_FORCE = "nickcore.force";
private static final String PERM_INFO = "nickcore.info"; private static final String PERM_INFO = "nickcore.info";
@@ -172,13 +171,12 @@ public final class NickCommand implements CommandExecutor, TabCompleter {
sendMessage(player, "<red>You don't have permission for that rank."); sendMessage(player, "<red>You don't have permission for that rank.");
return; return;
} }
NickProfile profile = profileCache.get(player.getUniqueId()) // Save rank to DB and sync to Velocity
.orElse(NickProfile.empty(player.getUniqueId())); nickService.saveRankChange(player.getUniqueId(), rankId).thenRun(() -> {
NickProfile updated = profile.withRank(rankId);
profileCache.put(updated);
sendMessage(player, "<green>Rank set to <white>" + sendMessage(player, "<green>Rank set to <white>" +
rankService.getRank(rankId).map(r -> r.displayName()).orElse(rankId)); rankService.getRank(rankId).map(r -> r.displayName()).orElse(rankId));
applyDisplay(player); applyDisplay(player);
});
} }
private void handleSkin(Player player, String[] args) { private void handleSkin(Player player, String[] args) {

View File

@@ -10,7 +10,9 @@ import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
import java.util.*; import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**

View File

@@ -1,7 +1,11 @@
package com.nickcore.paper.gui; package com.nickcore.paper.gui;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.cache.ProfileCache;
import com.nickcore.paper.service.*; import com.nickcore.paper.service.NickService;
import com.nickcore.paper.service.NametagService;
import com.nickcore.paper.service.PlaceholderService;
import com.nickcore.paper.service.RankService;
import com.nickcore.paper.service.SkinService;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -10,7 +14,7 @@ import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import java.util.*; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
@@ -53,7 +57,7 @@ public final class GuiManager implements Listener {
} }
public void openRankMenu(Player player) { public void openRankMenu(Player player) {
RankMenuGui gui = new RankMenuGui(player, this, rankService, profileCache, RankMenuGui gui = new RankMenuGui(player, this, nickService, rankService,
placeholderService, nametagService); placeholderService, nametagService);
openGuis.put(player.getUniqueId(), gui); openGuis.put(player.getUniqueId(), gui);
gui.open(); gui.open();
@@ -67,7 +71,7 @@ public final class GuiManager implements Listener {
@EventHandler @EventHandler
public void onInventoryClick(InventoryClickEvent event) { public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return; if (!(event.getWhoClicked() instanceof Player)) return;
if (!(event.getInventory().getHolder() instanceof AbstractGui gui)) return; if (!(event.getInventory().getHolder() instanceof AbstractGui gui)) return;
gui.handleClick(event); gui.handleClick(event);
} }

View File

@@ -1,6 +1,5 @@
package com.nickcore.paper.gui; package com.nickcore.paper.gui;
import com.nickcore.common.validation.InputSanitizer;
import com.nickcore.paper.service.NickService; import com.nickcore.paper.service.NickService;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;

View File

@@ -1,8 +1,7 @@
package com.nickcore.paper.gui; package com.nickcore.paper.gui;
import com.nickcore.common.model.NickProfile;
import com.nickcore.common.model.RankDefinition; import com.nickcore.common.model.RankDefinition;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.service.NickService;
import com.nickcore.paper.service.NametagService; import com.nickcore.paper.service.NametagService;
import com.nickcore.paper.service.PlaceholderService; import com.nickcore.paper.service.PlaceholderService;
import com.nickcore.paper.service.RankService; import com.nickcore.paper.service.RankService;
@@ -21,20 +20,20 @@ import java.util.List;
public final class RankMenuGui extends AbstractGui { public final class RankMenuGui extends AbstractGui {
private final GuiManager guiManager; private final GuiManager guiManager;
private final NickService nickService;
private final RankService rankService; private final RankService rankService;
private final ProfileCache profileCache;
private final PlaceholderService placeholderService; private final PlaceholderService placeholderService;
private final NametagService nametagService; private final NametagService nametagService;
private final MiniMessage mm = MiniMessage.miniMessage(); private final MiniMessage mm = MiniMessage.miniMessage();
private int page = 0; private int page = 0;
public RankMenuGui(Player viewer, GuiManager guiManager, RankService rankService, public RankMenuGui(Player viewer, GuiManager guiManager, NickService nickService,
ProfileCache profileCache, PlaceholderService placeholderService, RankService rankService, PlaceholderService placeholderService,
NametagService nametagService) { NametagService nametagService) {
super(viewer, 54, Component.text("Select Rank", NamedTextColor.GOLD, TextDecoration.BOLD)); super(viewer, 54, Component.text("Select Rank", NamedTextColor.GOLD, TextDecoration.BOLD));
this.guiManager = guiManager; this.guiManager = guiManager;
this.nickService = nickService;
this.rankService = rankService; this.rankService = rankService;
this.profileCache = profileCache;
this.placeholderService = placeholderService; this.placeholderService = placeholderService;
this.nametagService = nametagService; this.nametagService = nametagService;
} }
@@ -57,15 +56,15 @@ public final class RankMenuGui extends AbstractGui {
Component.text("Click to select", NamedTextColor.YELLOW) Component.text("Click to select", NamedTextColor.YELLOW)
)), )),
e -> { e -> {
NickProfile profile = profileCache.get(viewer.getUniqueId()) // Save to DB and sync via Velocity
.orElse(NickProfile.empty(viewer.getUniqueId())); nickService.saveRankChange(viewer.getUniqueId(), rank.id()).thenRun(() -> {
profileCache.put(profile.withRank(rank.id()));
viewer.closeInventory(); viewer.closeInventory();
viewer.sendMessage(Component.text("Rank set to ", NamedTextColor.GREEN) viewer.sendMessage(Component.text("Rank set to ", NamedTextColor.GREEN)
.append(mm.deserialize(rank.displayName()))); .append(mm.deserialize(rank.displayName())));
placeholderService.invalidate(viewer.getUniqueId(), viewer.getName()); placeholderService.invalidate(viewer.getUniqueId(), viewer.getName());
nametagService.updateNametag(viewer); nametagService.updateNametag(viewer);
}); });
});
} }
// Back button // Back button

View File

@@ -0,0 +1,129 @@
package com.nickcore.paper.integration;
import net.atlantismc.disguises.api.API;
import net.atlantismc.disguises.api.DisguiseProvider;
import org.bukkit.entity.Player;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Integration bridge for the Atlantis Disguises plugin.
*
* <p>When Atlantis Disguises is present on the server, NickCore delegates
* all name-change and skin operations to it. Atlantis handles packet-level
* name injection, skin application, and GameProfile rewriting — producing
* a much more seamless disguise than NickCore's built-in display-name approach.</p>
*
* <p>All methods are safe to call even if Atlantis is not loaded; they will
* return false/null gracefully.</p>
*/
public final class AtlantisDisguiseIntegration {
private final Logger logger;
private volatile boolean available;
public AtlantisDisguiseIntegration(Logger logger) {
this.logger = Objects.requireNonNull(logger);
this.available = false;
}
/**
* Attempts to hook into the Atlantis Disguises API.
* Call this during plugin enable after soft-depend loads.
*
* @return true if the API was successfully hooked
*/
public boolean hook() {
try {
API api = DisguiseProvider.get();
if (api != null) {
available = true;
logger.info("[NickCore] Hooked into Atlantis Disguises API");
return true;
}
} catch (NoClassDefFoundError | Exception e) {
logger.log(Level.FINE, "Atlantis Disguises not available, using built-in name overrides", e);
}
available = false;
return false;
}
/**
* Checks if Atlantis Disguises is available and hooked.
*/
public boolean isAvailable() {
return available;
}
/**
* Disguises a player with the given name via Atlantis.
* This handles packet-level GameProfile rewriting, skin changes, and
* nametag updates — all at the protocol level.
*
* @param player the player to disguise
* @param name the disguise name
* @return true if the disguise was applied successfully
*/
public boolean disguise(Player player, String name) {
if (!available || player == null || name == null) return false;
try {
API api = DisguiseProvider.get();
if (api != null) {
return api.disguise(player, name);
}
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to apply Atlantis disguise for " + player.getName(), e);
}
return false;
}
/**
* Removes a player's disguise via Atlantis.
*
* @param player the player to undisguise
* @return true if the disguise was removed
*/
public boolean undisguise(Player player) {
if (!available || player == null) return false;
try {
API api = DisguiseProvider.get();
if (api != null) {
return api.undisguise(player);
}
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to remove Atlantis disguise for " + player.getName(), e);
}
return false;
}
/**
* Checks if a player is currently disguised via Atlantis.
*/
public boolean isDisguised(Player player) {
if (!available || player == null) return false;
try {
API api = DisguiseProvider.get();
return api != null && api.isDisguised(player);
} catch (Exception e) {
return false;
}
}
/**
* Gets the current disguise name for a player via Atlantis.
*
* @return the disguise name, or null if not disguised
*/
public String getDisguiseName(Player player) {
if (!available || player == null) return null;
try {
API api = DisguiseProvider.get();
return api != null ? api.getDisguise(player) : null;
} catch (Exception e) {
return null;
}
}
}

View File

@@ -25,7 +25,7 @@ public final class NickCoreExpansion extends PlaceholderExpansion {
public @NotNull String getIdentifier() { return "nickcore"; } public @NotNull String getIdentifier() { return "nickcore"; }
@Override @Override
public @NotNull String getAuthor() { return "NickCore Team"; } public @NotNull String getAuthor() { return "BaileyCodes"; }
@Override @Override
public @NotNull String getVersion() { return plugin.getPluginMeta().getVersion(); } public @NotNull String getVersion() { return plugin.getPluginMeta().getVersion(); }

View File

@@ -3,6 +3,7 @@ package com.nickcore.paper.integration;
import com.nickcore.common.dto.NickSyncMessage; import com.nickcore.common.dto.NickSyncMessage;
import com.nickcore.common.model.NickProfile; import com.nickcore.common.model.NickProfile;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.cache.ProfileCache;
import com.nickcore.paper.service.NameOverrideService;
import com.nickcore.paper.service.PlaceholderService; import com.nickcore.paper.service.PlaceholderService;
import com.nickcore.paper.service.NametagService; import com.nickcore.paper.service.NametagService;
import com.nickcore.paper.scheduler.SchedulerAdapter; import com.nickcore.paper.scheduler.SchedulerAdapter;
@@ -19,6 +20,10 @@ import java.util.logging.Logger;
/** /**
* Velocity plugin messaging integration. * Velocity plugin messaging integration.
* Sends/receives NickSyncMessages for cross-server nick synchronization. * Sends/receives NickSyncMessages for cross-server nick synchronization.
*
* <p>When a player switches servers (hub → survival), the Velocity proxy forwards
* their profile to the new server. This handler receives that sync and applies
* the full name override so the nick persists across servers.</p>
*/ */
public final class VelocityMessaging implements PluginMessageListener { public final class VelocityMessaging implements PluginMessageListener {
@@ -26,17 +31,20 @@ public final class VelocityMessaging implements PluginMessageListener {
private final ProfileCache profileCache; private final ProfileCache profileCache;
private final PlaceholderService placeholderService; private final PlaceholderService placeholderService;
private final NametagService nametagService; private final NametagService nametagService;
private final NameOverrideService nameOverrideService;
private final SchedulerAdapter scheduler; private final SchedulerAdapter scheduler;
private final Logger logger; private final Logger logger;
private final String serverName; private final String serverName;
public VelocityMessaging(Plugin plugin, ProfileCache profileCache, public VelocityMessaging(Plugin plugin, ProfileCache profileCache,
PlaceholderService placeholderService, NametagService nametagService, PlaceholderService placeholderService, NametagService nametagService,
SchedulerAdapter scheduler, Logger logger, String serverName) { NameOverrideService nameOverrideService, SchedulerAdapter scheduler,
Logger logger, String serverName) {
this.plugin = plugin; this.plugin = plugin;
this.profileCache = profileCache; this.profileCache = profileCache;
this.placeholderService = placeholderService; this.placeholderService = placeholderService;
this.nametagService = nametagService; this.nametagService = nametagService;
this.nameOverrideService = nameOverrideService;
this.scheduler = scheduler; this.scheduler = scheduler;
this.logger = logger; this.logger = logger;
this.serverName = serverName; this.serverName = serverName;
@@ -71,12 +79,34 @@ public final class VelocityMessaging implements PluginMessageListener {
} }
/** /**
* Sends a full profile sync for a player. * Sends a full profile sync to Velocity for cross-server propagation.
* Called every time a nick/rank/skin changes on this server.
*/ */
public void syncProfile(NickProfile profile) { public void syncProfile(NickProfile profile) {
sendSync(NickSyncMessage.fullSync(profile, serverName)); sendSync(NickSyncMessage.fullSync(profile, serverName));
} }
/**
* Sends a nick update to Velocity.
*/
public void syncNickUpdate(java.util.UUID uuid, String nickname) {
sendSync(NickSyncMessage.nickUpdate(uuid, nickname, serverName));
}
/**
* Sends a rank update to Velocity.
*/
public void syncRankUpdate(java.util.UUID uuid, String rankId) {
sendSync(NickSyncMessage.rankUpdate(uuid, rankId, serverName));
}
/**
* Sends a reset to Velocity.
*/
public void syncReset(java.util.UUID uuid) {
sendSync(NickSyncMessage.reset(uuid, serverName));
}
@Override @Override
public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte @NotNull [] data) { public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte @NotNull [] data) {
if (!NickSyncMessage.CHANNEL.equals(channel)) return; if (!NickSyncMessage.CHANNEL.equals(channel)) return;
@@ -109,7 +139,11 @@ public final class VelocityMessaging implements PluginMessageListener {
Player player = Bukkit.getPlayer(msg.playerUuid()); Player player = Bukkit.getPlayer(msg.playerUuid());
if (player != null) { if (player != null) {
placeholderService.invalidate(msg.playerUuid(), player.getName()); placeholderService.invalidate(msg.playerUuid(), player.getName());
scheduler.runAtEntity(plugin, player, () -> nametagService.updateNametag(player)); scheduler.runAtEntity(plugin, player, () -> {
// Apply full name override so the nick appears everywhere on this server
nameOverrideService.applyFullOverride(player);
nametagService.updateNametag(player);
});
} }
} }
@@ -137,13 +171,24 @@ public final class VelocityMessaging implements PluginMessageListener {
private void handleCacheInvalidate(NickSyncMessage msg) { private void handleCacheInvalidate(NickSyncMessage msg) {
profileCache.remove(msg.playerUuid()); profileCache.remove(msg.playerUuid());
placeholderService.remove(msg.playerUuid()); placeholderService.remove(msg.playerUuid());
Player player = Bukkit.getPlayer(msg.playerUuid());
if (player != null) {
scheduler.runAtEntity(plugin, player, () -> {
nameOverrideService.clearMapping(player);
nameOverrideService.applyFullOverride(player);
nametagService.updateNametag(player);
});
}
} }
private void refreshPlayer(java.util.UUID uuid) { private void refreshPlayer(java.util.UUID uuid) {
Player player = Bukkit.getPlayer(uuid); Player player = Bukkit.getPlayer(uuid);
if (player != null) { if (player != null) {
placeholderService.invalidate(uuid, player.getName()); placeholderService.invalidate(uuid, player.getName());
scheduler.runAtEntity(plugin, player, () -> nametagService.updateNametag(player)); scheduler.runAtEntity(plugin, player, () -> {
nameOverrideService.applyFullOverride(player);
nametagService.updateNametag(player);
});
} }
} }
} }

View File

@@ -9,9 +9,6 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Intercepts player commands and replaces nick names with real names before dispatch. * Intercepts player commands and replaces nick names with real names before dispatch.
@@ -25,14 +22,9 @@ import java.util.regex.Pattern;
public final class CommandPreprocessListener implements Listener { public final class CommandPreprocessListener implements Listener {
private final NameOverrideService nameOverrideService; private final NameOverrideService nameOverrideService;
private final Logger logger;
/** Pattern to split command into parts while preserving structure. */ public CommandPreprocessListener(NameOverrideService nameOverrideService) {
private static final Pattern WORD_BOUNDARY = Pattern.compile("\\s+");
public CommandPreprocessListener(NameOverrideService nameOverrideService, Logger logger) {
this.nameOverrideService = Objects.requireNonNull(nameOverrideService); this.nameOverrideService = Objects.requireNonNull(nameOverrideService);
this.logger = Objects.requireNonNull(logger);
} }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)

View File

@@ -1,11 +1,12 @@
package com.nickcore.paper.listener; package com.nickcore.paper.listener;
import com.nickcore.common.model.NickProfile; import com.nickcore.common.model.NickProfile;
import com.nickcore.common.validation.InputSanitizer; import com.nickcore.paper.service.NameOverrideService;
import com.nickcore.paper.service.*; import com.nickcore.paper.service.NametagService;
import com.nickcore.paper.service.NickService;
import com.nickcore.paper.service.PlaceholderService;
import com.nickcore.paper.service.SkinService;
import com.nickcore.paper.scheduler.SchedulerAdapter; import com.nickcore.paper.scheduler.SchedulerAdapter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@@ -29,7 +30,6 @@ public final class PlayerConnectionListener implements Listener {
private final NameOverrideService nameOverrideService; private final NameOverrideService nameOverrideService;
private final SchedulerAdapter scheduler; private final SchedulerAdapter scheduler;
private final Logger logger; private final Logger logger;
private final MiniMessage mm = MiniMessage.miniMessage();
public PlayerConnectionListener(Plugin plugin, NickService nickService, public PlayerConnectionListener(Plugin plugin, NickService nickService,
SkinService skinService, PlaceholderService placeholderService, SkinService skinService, PlaceholderService placeholderService,

View File

@@ -5,8 +5,6 @@ import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;

View File

@@ -2,9 +2,9 @@ package com.nickcore.paper.service;
import com.nickcore.common.model.NickProfile; import com.nickcore.common.model.NickProfile;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.cache.ProfileCache;
import com.nickcore.paper.integration.AtlantisDisguiseIntegration;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -20,6 +20,10 @@ import java.util.concurrent.ConcurrentHashMap;
* tab completion, /tpa, /msg, player list, nametags, and any plugin that resolves * tab completion, /tpa, /msg, player list, nametags, and any plugin that resolves
* players by display name. * players by display name.
* *
* <p>When Atlantis Disguises is available, delegates to it for packet-level
* GameProfile rewriting (the most thorough approach). Otherwise, falls back to
* Bukkit display name + custom name overrides.</p>
*
* <p>Maintains a bidirectional mapping between nick names and real names so that * <p>Maintains a bidirectional mapping between nick names and real names so that
* commands referencing a nick name can be resolved back to the actual player.</p> * commands referencing a nick name can be resolved back to the actual player.</p>
* *
@@ -28,7 +32,7 @@ import java.util.concurrent.ConcurrentHashMap;
public final class NameOverrideService { public final class NameOverrideService {
private final ProfileCache profileCache; private final ProfileCache profileCache;
private final MiniMessage miniMessage = MiniMessage.miniMessage(); private volatile AtlantisDisguiseIntegration atlantis;
/** nick (lowercase) -> real player UUID */ /** nick (lowercase) -> real player UUID */
private final ConcurrentHashMap<String, UUID> nickToUuid = new ConcurrentHashMap<>(512); private final ConcurrentHashMap<String, UUID> nickToUuid = new ConcurrentHashMap<>(512);
@@ -40,9 +44,21 @@ public final class NameOverrideService {
this.profileCache = Objects.requireNonNull(profileCache); this.profileCache = Objects.requireNonNull(profileCache);
} }
/**
* Sets the Atlantis Disguises integration. When set and available,
* disguise/undisguise operations are delegated to Atlantis for
* packet-level name rewriting.
*/
public void setAtlantis(AtlantisDisguiseIntegration atlantis) {
this.atlantis = atlantis;
}
/** /**
* Applies a full name override to a player: display name, player list name, * Applies a full name override to a player: display name, player list name,
* custom name, and registers the nick in lookup maps. * custom name, and registers the nick in lookup maps.
*
* <p>If Atlantis Disguises is available, uses its packet-level disguise system
* which rewrites the GameProfile and handles skin + nametag automatically.</p>
*/ */
public void applyFullOverride(Player player) { public void applyFullOverride(Player player) {
NickProfile profile = profileCache.get(player.getUniqueId()).orElse(null); NickProfile profile = profileCache.get(player.getUniqueId()).orElse(null);
@@ -52,16 +68,21 @@ public final class NameOverrideService {
if (profile == null || !profile.nicked() || profile.nickname() == null) { if (profile == null || !profile.nicked() || profile.nickname() == null) {
// Not nicked — restore real identity // Not nicked — restore real identity
player.displayName(Component.text(player.getName())); undisguisePlayer(player);
player.playerListName(Component.text(player.getName()));
player.customName(null);
return; return;
} }
String nick = profile.nickname(); String nick = profile.nickname();
Component nickComponent = Component.text(nick);
// Set all name surfaces // Try Atlantis Disguises first (packet-level, most thorough)
boolean atlantisApplied = false;
AtlantisDisguiseIntegration atl = this.atlantis;
if (atl != null && atl.isAvailable()) {
atlantisApplied = atl.disguise(player, nick);
}
// Always set Bukkit-level names as fallback/supplement
Component nickComponent = Component.text(nick);
player.displayName(nickComponent); player.displayName(nickComponent);
player.playerListName(nickComponent); player.playerListName(nickComponent);
player.customName(nickComponent); player.customName(nickComponent);
@@ -71,6 +92,22 @@ public final class NameOverrideService {
realToNick.put(player.getName().toLowerCase(), nick); realToNick.put(player.getName().toLowerCase(), nick);
} }
/**
* Removes all name overrides from a player.
*/
private void undisguisePlayer(Player player) {
// Undo Atlantis disguise if active
AtlantisDisguiseIntegration atl = this.atlantis;
if (atl != null && atl.isAvailable() && atl.isDisguised(player)) {
atl.undisguise(player);
}
// Reset Bukkit-level names
player.displayName(Component.text(player.getName()));
player.playerListName(Component.text(player.getName()));
player.customName(null);
}
/** /**
* Clears all name mappings for a player. Called on quit or reset. * Clears all name mappings for a player. Called on quit or reset.
*/ */
@@ -82,6 +119,15 @@ public final class NameOverrideService {
} }
} }
/**
* Fully resets a player's identity — clears mappings and undisguises.
* Called on /nick reset or when a player quits.
*/
public void resetPlayer(Player player) {
clearMapping(player);
undisguisePlayer(player);
}
/** /**
* Resolves a player by either their nick name or real name. * Resolves a player by either their nick name or real name.
* This is the key method that allows /tpa, /msg, etc. to work with nicks. * This is the key method that allows /tpa, /msg, etc. to work with nicks.

View File

@@ -5,10 +5,10 @@ import com.nickcore.common.model.NickProfile;
import com.nickcore.common.validation.InputSanitizer; import com.nickcore.common.validation.InputSanitizer;
import com.nickcore.common.validation.NicknameValidator; import com.nickcore.common.validation.NicknameValidator;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.cache.ProfileCache;
import com.nickcore.paper.integration.VelocityMessaging;
import com.nickcore.paper.repository.NickRepository; import com.nickcore.paper.repository.NickRepository;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -36,7 +36,6 @@ public final class NickService {
private final ProfileCache cache; private final ProfileCache cache;
private final NicknameValidator validator; private final NicknameValidator validator;
private final Logger logger; private final Logger logger;
private final MiniMessage miniMessage;
// Configurable name pool — loaded from names.yml, swapped atomically on reload // Configurable name pool — loaded from names.yml, swapped atomically on reload
private volatile List<String> namePool = List.of(); private volatile List<String> namePool = List.of();
@@ -45,13 +44,15 @@ public final class NickService {
private volatile boolean allowDuplicateNicks = false; private volatile boolean allowDuplicateNicks = false;
private volatile boolean allowCustomNames = false; private volatile boolean allowCustomNames = false;
// Optional: set when Velocity is enabled for cross-server sync
private volatile VelocityMessaging velocityMessaging;
public NickService(NickRepository repository, ProfileCache cache, public NickService(NickRepository repository, ProfileCache cache,
NicknameValidator validator, Logger logger) { NicknameValidator validator, Logger logger) {
this.repository = Objects.requireNonNull(repository); this.repository = Objects.requireNonNull(repository);
this.cache = Objects.requireNonNull(cache); this.cache = Objects.requireNonNull(cache);
this.validator = Objects.requireNonNull(validator); this.validator = Objects.requireNonNull(validator);
this.logger = Objects.requireNonNull(logger); this.logger = Objects.requireNonNull(logger);
this.miniMessage = MiniMessage.miniMessage();
} }
/** /**
@@ -138,6 +139,11 @@ public final class NickService {
return repository.save(newProfile).thenCompose(v -> { return repository.save(newProfile).thenCompose(v -> {
cache.put(newProfile); cache.put(newProfile);
// Sync to other servers via Velocity
if (velocityMessaging != null) {
velocityMessaging.syncProfile(newProfile);
}
// Record history // Record history
NickHistory history = NickHistory.nickChange( NickHistory history = NickHistory.nickChange(
uuid, oldProfile.nickname(), sanitized, uuid, oldProfile.nickname(), sanitized,
@@ -189,6 +195,11 @@ public final class NickService {
return repository.save(resetProfile).thenCompose(v -> { return repository.save(resetProfile).thenCompose(v -> {
cache.put(resetProfile); cache.put(resetProfile);
// Sync reset to other servers
if (velocityMessaging != null) {
velocityMessaging.syncReset(uuid);
}
NickHistory history = NickHistory.reset( NickHistory history = NickHistory.reset(
uuid, oldProfile.nickname(), oldProfile.rankId(), changedBy uuid, oldProfile.nickname(), oldProfile.rankId(), changedBy
); );
@@ -239,6 +250,28 @@ public final class NickService {
return allowCustomNames; return allowCustomNames;
} }
/**
* Sets the VelocityMessaging instance for cross-server sync.
*/
public void setVelocityMessaging(VelocityMessaging velocityMessaging) {
this.velocityMessaging = velocityMessaging;
}
/**
* Saves a rank change to the database and syncs to Velocity.
*/
public CompletableFuture<Void> saveRankChange(java.util.UUID uuid, String rankId) {
NickProfile profile = cache.get(uuid).orElse(NickProfile.empty(uuid));
NickProfile updated = profile.withRank(rankId);
cache.put(updated);
if (velocityMessaging != null) {
velocityMessaging.syncRankUpdate(uuid, rankId);
}
return repository.save(updated);
}
// ── Result Container ──────────────────────────────────────────────── // ── Result Container ────────────────────────────────────────────────
/** /**

View File

@@ -3,7 +3,6 @@ package com.nickcore.paper.service;
import com.nickcore.common.model.NickProfile; import com.nickcore.common.model.NickProfile;
import com.nickcore.common.model.RankDefinition; import com.nickcore.common.model.RankDefinition;
import com.nickcore.paper.cache.ProfileCache; import com.nickcore.paper.cache.ProfileCache;
import org.bukkit.entity.Player;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;

View File

@@ -5,7 +5,13 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.*; import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**

View File

@@ -6,15 +6,19 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.List;
import java.util.concurrent.*; import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;

View File

@@ -1,10 +1,10 @@
name: NickCore name: NickCore
version: '1.0.0' version: '1.0.4'
main: com.nickcore.paper.NickCorePlugin main: com.nickcore.paper.NickCorePlugin
api-version: '1.21' api-version: '1.21'
description: Enterprise Minecraft Nickname Plugin description: Enterprise Minecraft Nickname Plugin
author: NickCore Team author: BaileyCodes
website: https://nickcore.com website: https://balls.studio
folia-supported: true folia-supported: true
softdepend: softdepend:
@@ -12,6 +12,7 @@ softdepend:
- LuckPerms - LuckPerms
- TAB - TAB
- Essentials - Essentials
- AtlantisDisguises
commands: commands:
nick: nick:

View File

@@ -11,9 +11,12 @@ dependencies {
annotationProcessor("com.velocitypowered:velocity-api:$velocityApiVersion") annotationProcessor("com.velocitypowered:velocity-api:$velocityApiVersion")
} }
tasks.jar {
enabled = false
}
tasks.shadowJar { tasks.shadowJar {
archiveClassifier.set("") archiveFileName.set("NickCore-Velocity-${project.version}.jar")
archiveBaseName.set("NickCore-Velocity")
minimize() minimize()
} }

View File

@@ -19,7 +19,6 @@ import org.slf4j.Logger;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -27,8 +26,8 @@ import java.util.concurrent.ConcurrentHashMap;
* NickCore Velocity proxy plugin. * NickCore Velocity proxy plugin.
* Maintains a cross-server nick profile cache and routes sync messages. * Maintains a cross-server nick profile cache and routes sync messages.
*/ */
@Plugin(id = "nickcore", name = "NickCore", version = "1.0.0", @Plugin(id = "nickcore", name = "NickCore", version = "1.0.4",
authors = {"NickCore Team"}, description = "Enterprise nickname system - Velocity module") authors = {"BaileyCodes"}, description = "Enterprise nickname system - Velocity module")
public final class NickCoreVelocity { public final class NickCoreVelocity {
private static final MinecraftChannelIdentifier CHANNEL = private static final MinecraftChannelIdentifier CHANNEL =
@@ -108,10 +107,12 @@ public final class NickCoreVelocity {
(uuid, p) -> p.withNickname(message.nickname())); (uuid, p) -> p.withNickname(message.nickname()));
case RANK_UPDATE -> proxyCache.computeIfPresent(message.playerUuid(), case RANK_UPDATE -> proxyCache.computeIfPresent(message.playerUuid(),
(uuid, p) -> p.withRank(message.rankId())); (uuid, p) -> p.withRank(message.rankId()));
case SKIN_UPDATE -> proxyCache.computeIfPresent(message.playerUuid(),
(uuid, p) -> p.withSkin(message.skinId(), message.skinValue(), message.skinSignature()));
case RESET -> proxyCache.computeIfPresent(message.playerUuid(), case RESET -> proxyCache.computeIfPresent(message.playerUuid(),
(uuid, p) -> p.reset()); (uuid, p) -> p.reset());
case CACHE_INVALIDATE -> proxyCache.remove(message.playerUuid()); case CACHE_INVALIDATE -> proxyCache.remove(message.playerUuid());
default -> { /* ignore */ } default -> { /* PLAYER_JOIN, PLAYER_QUIT — no proxy-side action needed */ }
} }
// Forward to all OTHER servers // Forward to all OTHER servers