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
This commit is contained in:
2026-06-01 13:22:48 -05:00
parent 0a06223f7c
commit b7bc25e168
6 changed files with 201 additions and 5 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
} }
} }

View File

@@ -16,6 +16,7 @@ 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 { tasks.jar {

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;
@@ -102,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();
@@ -301,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);

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

@@ -2,6 +2,7 @@ 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;
@@ -19,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>
* *
@@ -27,6 +32,7 @@ import java.util.concurrent.ConcurrentHashMap;
public final class NameOverrideService { public final class NameOverrideService {
private final ProfileCache profileCache; private final ProfileCache profileCache;
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);
@@ -38,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);
@@ -50,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);
@@ -69,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.
*/ */
@@ -80,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

@@ -12,6 +12,7 @@ softdepend:
- LuckPerms - LuckPerms
- TAB - TAB
- Essentials - Essentials
- AtlantisDisguises
commands: commands:
nick: nick: