feat: NickCore v1.0.0 - enterprise nickname plugin for Paper/Folia/Velocity

This commit is contained in:
2026-05-30 21:05:02 -05:00
parent 8a42740875
commit f0b35e89ac
54 changed files with 5393 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
plugins {
id("com.gradleup.shadow")
}
dependencies {
val velocityApiVersion: String by project
implementation(project(":nickcore-common"))
compileOnly("com.velocitypowered:velocity-api:$velocityApiVersion")
annotationProcessor("com.velocitypowered:velocity-api:$velocityApiVersion")
}
tasks.shadowJar {
archiveClassifier.set("")
archiveBaseName.set("NickCore-Velocity")
minimize()
}
tasks.build {
dependsOn(tasks.shadowJar)
}

View File

@@ -0,0 +1,155 @@
package com.nickcore.velocity;
import com.nickcore.common.dto.NickSyncMessage;
import com.nickcore.common.model.NickProfile;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import org.slf4j.Logger;
import com.google.inject.Inject;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* NickCore Velocity proxy plugin.
* Maintains a cross-server nick profile cache and routes sync messages.
*/
@Plugin(id = "nickcore", name = "NickCore", version = "1.0.0",
authors = {"NickCore Team"}, description = "Enterprise nickname system - Velocity module")
public final class NickCoreVelocity {
private static final MinecraftChannelIdentifier CHANNEL =
MinecraftChannelIdentifier.from(NickSyncMessage.CHANNEL);
private final ProxyServer proxy;
private final Logger logger;
/** Cross-server nick profile cache. */
private final ConcurrentHashMap<UUID, NickProfile> proxyCache = new ConcurrentHashMap<>(512);
@Inject
public NickCoreVelocity(ProxyServer proxy, Logger logger) {
this.proxy = proxy;
this.logger = logger;
}
@Subscribe
public void onProxyInit(ProxyInitializeEvent event) {
proxy.getChannelRegistrar().register(CHANNEL);
logger.info("NickCore Velocity module initialized");
logger.info("Sync channel registered: " + NickSyncMessage.CHANNEL);
}
@Subscribe
public void onProxyShutdown(ProxyShutdownEvent event) {
proxyCache.clear();
proxy.getChannelRegistrar().unregister(CHANNEL);
logger.info("NickCore Velocity module disabled");
}
@Subscribe
public void onServerConnected(ServerConnectedEvent event) {
Player player = event.getPlayer();
NickProfile cached = proxyCache.get(player.getUniqueId());
if (cached != null && cached.nicked()) {
// Forward profile to the new server
NickSyncMessage msg = NickSyncMessage.fullSync(cached, "velocity-proxy");
event.getServer().sendPluginMessage(CHANNEL, msg.encode());
logger.info("Forwarded nick profile for " + player.getUsername() +
" to " + event.getServer().getServerInfo().getName());
}
}
@Subscribe
public void onDisconnect(DisconnectEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
proxyCache.remove(uuid);
// Broadcast cache invalidation to all servers
NickSyncMessage invalidate = NickSyncMessage.cacheInvalidate(uuid, "velocity-proxy");
broadcastToAllServers(invalidate);
}
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
if (!CHANNEL.equals(event.getIdentifier())) return;
// Consume the message so it doesn't get forwarded raw
event.setResult(PluginMessageEvent.ForwardResult.handled());
try {
NickSyncMessage message = NickSyncMessage.decode(event.getData());
// Update proxy cache
switch (message.action()) {
case FULL_SYNC -> {
NickProfile profile = new NickProfile(
message.playerUuid(), message.nickname(), message.rankId(),
message.skinId(), message.skinValue(), message.skinSignature(),
message.nickname() != null, null, null
);
proxyCache.put(message.playerUuid(), profile);
}
case NICK_UPDATE -> proxyCache.computeIfPresent(message.playerUuid(),
(uuid, p) -> p.withNickname(message.nickname()));
case RANK_UPDATE -> proxyCache.computeIfPresent(message.playerUuid(),
(uuid, p) -> p.withRank(message.rankId()));
case RESET -> proxyCache.computeIfPresent(message.playerUuid(),
(uuid, p) -> p.reset());
case CACHE_INVALIDATE -> proxyCache.remove(message.playerUuid());
default -> { /* ignore */ }
}
// Forward to all OTHER servers
broadcastToOtherServers(message, event);
} catch (Exception e) {
logger.warn("Failed to process sync message: " + e.getMessage());
}
}
private void broadcastToAllServers(NickSyncMessage message) {
byte[] data = message.encode();
for (var server : proxy.getAllServers()) {
if (!server.getPlayersConnected().isEmpty()) {
server.sendPluginMessage(CHANNEL, data);
}
}
}
private void broadcastToOtherServers(NickSyncMessage message, PluginMessageEvent sourceEvent) {
byte[] data = message.encode();
ServerConnection sourceConn = sourceEvent.getSource() instanceof ServerConnection sc ? sc : null;
for (var server : proxy.getAllServers()) {
// Skip the source server
if (sourceConn != null &&
server.getServerInfo().equals(sourceConn.getServerInfo())) continue;
if (!server.getPlayersConnected().isEmpty()) {
server.sendPluginMessage(CHANNEL, data);
}
}
}
/**
* Returns the proxy-level cache for external access.
*/
public Map<UUID, NickProfile> getProxyCache() {
return Map.copyOf(proxyCache);
}
}