diff --git a/Citizens-2.0.41-b4138.jar b/Citizens-2.0.41-b4138.jar new file mode 100644 index 0000000..29ab357 Binary files /dev/null and b/Citizens-2.0.41-b4138.jar differ diff --git a/Freesia-Backend/build.gradle.kts b/Freesia-Backend/build.gradle.kts index ca51f8b..fb3d0f4 100644 --- a/Freesia-Backend/build.gradle.kts +++ b/Freesia-Backend/build.gradle.kts @@ -1,6 +1,13 @@ +repositories { + maven("https://maven.citizensnpcs.co/repo") +} + dependencies { compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") compileOnly("io.netty:netty-all:4.1.118.Final") + compileOnly("net.citizensnpcs:citizens-main:2.0.41-SNAPSHOT") { + exclude(group = "*", module = "*") + } } tasks.build { diff --git a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/FreesiaBackend.java b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/FreesiaBackend.java index 80f61a0..219fe53 100644 --- a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/FreesiaBackend.java +++ b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/FreesiaBackend.java @@ -41,4 +41,3 @@ public void onDisable() { "&5[&dFreesia&5] &cFreesia Backend plugin disabled!")); } } - diff --git a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinRealPlayerTrackerUpdateEvent.java b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinRealPlayerTrackerUpdateEvent.java index f6ce3b5..162fb54 100644 --- a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinRealPlayerTrackerUpdateEvent.java +++ b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinRealPlayerTrackerUpdateEvent.java @@ -40,4 +40,3 @@ public void setCancelled(boolean b) { return HANDLERS; } } - diff --git a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinTrackerScanEvent.java b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinTrackerScanEvent.java index e8fb319..d384be6 100644 --- a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinTrackerScanEvent.java +++ b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/event/CyanidinTrackerScanEvent.java @@ -35,4 +35,3 @@ public Set getResultsUnmodifiable() { return HANDLERS; } } - diff --git a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/misc/VirtualPlayerManager.java b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/misc/VirtualPlayerManager.java index 5b389ad..66842f8 100644 --- a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/misc/VirtualPlayerManager.java +++ b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/misc/VirtualPlayerManager.java @@ -114,4 +114,3 @@ public CompletableFuture addVirtualPlayer(UUID playerUUID, int entityId return future; } } - diff --git a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/tracker/TrackerProcessor.java b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/tracker/TrackerProcessor.java index c10e475..18918ed 100644 --- a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/tracker/TrackerProcessor.java +++ b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/tracker/TrackerProcessor.java @@ -131,4 +131,4 @@ public void onPluginMessageReceived(@NotNull String channel, @NotNull Player sen } } -} +} \ No newline at end of file diff --git a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/utils/FriendlyByteBuf.java b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/utils/FriendlyByteBuf.java index 4b2f9e4..a85ccf6 100644 --- a/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/utils/FriendlyByteBuf.java +++ b/Freesia-Backend/src/main/java/com/nguyendevs/freesia/backend/utils/FriendlyByteBuf.java @@ -886,4 +886,4 @@ public byte[] getBytes() { this.source.readBytes(bytes); return bytes; } -} +} \ No newline at end of file diff --git a/Freesia-Backend/src/main/resources/plugin.yml b/Freesia-Backend/src/main/resources/plugin.yml index f04d6e9..eb28a59 100644 --- a/Freesia-Backend/src/main/resources/plugin.yml +++ b/Freesia-Backend/src/main/resources/plugin.yml @@ -3,3 +3,4 @@ version: '1.0.3-YSM-2.6.4' main: com.nguyendevs.freesia.backend.FreesiaBackend api-version: '1.16' folia-supported: true +softdepend: [Citizens] diff --git a/Freesia-NPC/build.gradle.kts b/Freesia-NPC/build.gradle.kts new file mode 100644 index 0000000..cf700fd --- /dev/null +++ b/Freesia-NPC/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("java") + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +group = "com.nguyendevs.freesia" +version = "1.0.0" + +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.citizensnpcs.co/") +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") + compileOnly("net.citizensnpcs:citizens-main:2.0.33-SNAPSHOT") { + exclude(group = "*", module = "*") + } +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} + +tasks.shadowJar { + archiveClassifier.set("") + archiveFileName.set("Freesia-NPC-${project.version}.jar") +} diff --git a/Freesia-NPC/src/main/java/com/nguyendevs/freesia/npc/FreesiaNPCPlugin.java b/Freesia-NPC/src/main/java/com/nguyendevs/freesia/npc/FreesiaNPCPlugin.java new file mode 100644 index 0000000..5d23a4d --- /dev/null +++ b/Freesia-NPC/src/main/java/com/nguyendevs/freesia/npc/FreesiaNPCPlugin.java @@ -0,0 +1,35 @@ +package com.nguyendevs.freesia.npc; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +public class FreesiaNPCPlugin extends JavaPlugin { + + public static final String CHANNEL_NAME = "freesia:npc"; + public static FreesiaNPCPlugin INSTANCE; + + @Override + public void onEnable() { + INSTANCE = this; + + Bukkit.getMessenger().registerOutgoingPluginChannel(this, CHANNEL_NAME); + Bukkit.getMessenger().registerIncomingPluginChannel(this, CHANNEL_NAME, new TrackerListener()); + + getServer().getPluginManager().registerEvents(new TrackerListener(), this); + Bukkit.getConsoleSender().sendMessage(ChatColor.translateAlternateColorCodes('&', + "&5[&dFreesia-NPC&5] &a Enabled successfully. Hooked Citizens for proxy model injection!")); + } + + @Override + public void onDisable() { + Bukkit.getMessenger().unregisterIncomingPluginChannel(this, CHANNEL_NAME); + Bukkit.getMessenger().unregisterOutgoingPluginChannel(this, CHANNEL_NAME); + getLogger().info(ChatColor.RED + "[Freesia-NPC] Disabled!"); + } + + public void sendProxyPayload(Player player, byte[] payload) { + player.sendPluginMessage(this, CHANNEL_NAME, payload); + } +} diff --git a/Freesia-NPC/src/main/java/com/nguyendevs/freesia/npc/TrackerListener.java b/Freesia-NPC/src/main/java/com/nguyendevs/freesia/npc/TrackerListener.java new file mode 100644 index 0000000..af9dfa5 --- /dev/null +++ b/Freesia-NPC/src/main/java/com/nguyendevs/freesia/npc/TrackerListener.java @@ -0,0 +1,74 @@ +package com.nguyendevs.freesia.npc; + +import io.papermc.paper.event.player.PlayerTrackEntityEvent; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.util.Collection; + +public class TrackerListener implements Listener, PluginMessageListener { + + private static final byte OP_TRACK_SYNC = 0; + private static final byte OP_REQ_LIST = 1; + private static final byte OP_RES_LIST = 2; + + @EventHandler + public void onTrackEntity(PlayerTrackEntityEvent event) { + final Entity beingWatched = event.getEntity(); + final Player watcher = event.getPlayer(); + + if (beingWatched.hasMetadata("NPC")) { + NPC npc = CitizensAPI.getNPCRegistry().getNPC(beingWatched); + if (npc != null) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(OP_TRACK_SYNC); + dos.writeLong(watcher.getUniqueId().getMostSignificantBits()); + dos.writeLong(watcher.getUniqueId().getLeastSignificantBits()); + dos.writeInt(npc.getId()); + dos.writeInt(beingWatched.getEntityId()); + dos.flush(); + + FreesiaNPCPlugin.INSTANCE.sendProxyPayload(watcher, bos.toByteArray()); + } catch (Exception e) { + FreesiaNPCPlugin.INSTANCE.getLogger().warning("Failed to send track sync: " + e.getMessage()); + } + } + } + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte[] message) { + if (!channel.equals(FreesiaNPCPlugin.CHANNEL_NAME)) return; + + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(message))) { + byte opcode = in.readByte(); + if (opcode == OP_REQ_LIST) { + Collection npcs = (Collection) CitizensAPI.getNPCRegistry().sorted(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(OP_RES_LIST); + dos.writeInt(npcs.size()); + for (NPC npc : npcs) { + dos.writeInt(npc.getId()); + dos.writeUTF(npc.getName()); + } + dos.flush(); + FreesiaNPCPlugin.INSTANCE.sendProxyPayload(player, bos.toByteArray()); + } + } catch (Exception e) { + FreesiaNPCPlugin.INSTANCE.getLogger().warning("Failed to parse plugin messaging: " + e.getMessage()); + } + } +} diff --git a/Freesia-NPC/src/main/resources/plugin.yml b/Freesia-NPC/src/main/resources/plugin.yml new file mode 100644 index 0000000..017e8ec --- /dev/null +++ b/Freesia-NPC/src/main/resources/plugin.yml @@ -0,0 +1,7 @@ +name: Freesia-NPC +version: 1.0.0 +main: com.nguyendevs.freesia.npc.FreesiaNPCPlugin +api-version: '1.20' +depend: [Citizens] +description: Freesia expansion for mapping Citizens NPCs to YesSteveModel environments via Velocity/Waterfall arrays. +author: NguyenDevs diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/Freesia.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/Freesia.java index 60f58bb..f7c49ef 100644 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/Freesia.java +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/Freesia.java @@ -23,8 +23,7 @@ import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.nguyendevs.freesia.common.EntryPoint; import com.nguyendevs.freesia.common.communicating.NettySocketServer; -import com.nguyendevs.freesia.velocity.command.ListYsmPlayersCommand; -import com.nguyendevs.freesia.velocity.command.DispatchWorkerCommandCommand; +import com.nguyendevs.freesia.velocity.command.FreesiaCommand; import com.nguyendevs.freesia.velocity.i18n.I18NManager; import com.nguyendevs.freesia.velocity.network.backend.MasterServerMessageHandler; import com.nguyendevs.freesia.velocity.network.mc.FreesiaPlayerTracker; @@ -59,6 +58,7 @@ public class Freesia implements PacketListener { public static YsmClientKickingDetector kickChecker; public static YsmMapperPayloadManager mapperManager; public static NettySocketServer masterServer; + public static com.nguyendevs.freesia.velocity.network.misc.NpcMessageReceiver npcMessageReceiver; @Inject private Logger logger; @@ -114,6 +114,9 @@ public void onProxyStart(ProxyInitializeEvent event) { tracker.addVirtualPlayerTrackerEventListener(mapperManager::onVirtualPlayerTrackerUpdate); virtualPlayerManager.init(); + + npcMessageReceiver = new com.nguyendevs.freesia.velocity.network.misc.NpcMessageReceiver(); + this.proxyServer.getEventManager().register(this, npcMessageReceiver); io.netty.handler.ssl.SslContext sslContext = null; try { @@ -140,8 +143,8 @@ public void onProxyStart(ProxyInitializeEvent event) { kickChecker.bootstrap(); LOGGER.info("Registering commands"); - DispatchWorkerCommandCommand.register(); - ListYsmPlayersCommand.register(); + FreesiaCommand.register(); + } @Subscribe diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/FreesiaConstants.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/FreesiaConstants.java index 0c53f1a..8f0efbd 100644 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/FreesiaConstants.java +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/FreesiaConstants.java @@ -23,7 +23,8 @@ public static final class FileConstants { public static final class PermissionConstants { public static final String LIST_PLAYER_COMMAND = "freesia.commands.listysmplayers", - DISPATCH_WORKER_COMMAND = "freesia.commands.dworkerc"; + DISPATCH_WORKER_COMMAND = "freesia.commands.dworkerc", + SET_SKIN_COMMAND = "freesia.commands.setskin"; } public static final class LanguageConstants { @@ -33,6 +34,10 @@ public static final class LanguageConstants { PLAYER_LIST_HEADER = "freesia.list_player_command_header", PLAYER_LIST_ENTRY = "freesia.list_player_command_body", + SETSKIN_SUCCESS = "freesia.setskin.success", + SETSKIN_NO_PLAYERS = "freesia.setskin.no_players", + SETSKIN_CITIZENS_DISABLED = "freesia.setskin.citizens_disabled", + HANDSHAKE_TIMED_OUT = "freesia.mod_handshake_time_outed", WORKER_TERMINATED_CONNECTION = "freesia.backend.disconnected", WORKER_NOT_CONNECTED = "freesia.backend.not_connected"; diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/DispatchWorkerCommandCommand.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/DispatchWorkerCommandCommand.java deleted file mode 100644 index 0622e19..0000000 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/DispatchWorkerCommandCommand.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.nguyendevs.freesia.velocity.command; - -import com.mojang.brigadier.Command; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.tree.LiteralCommandNode; -import com.velocitypowered.api.command.BrigadierCommand; -import com.velocitypowered.api.command.CommandMeta; -import com.velocitypowered.api.command.CommandSource; -import com.nguyendevs.freesia.velocity.FreesiaConstants; -import com.nguyendevs.freesia.velocity.Freesia; -import com.nguyendevs.freesia.velocity.network.backend.MasterServerMessageHandler; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class DispatchWorkerCommandCommand { - public static void register() { - final CommandMeta meta = Freesia.PROXY_SERVER.getCommandManager() - .metaBuilder("dworkerc") - .plugin(Freesia.INSTANCE) - .build(); - - Freesia.PROXY_SERVER.getCommandManager().register(meta, create()); - } - - public static @NotNull BrigadierCommand create() { - LiteralCommandNode registed = BrigadierCommand.literalArgumentBuilder("dworkerc") - .requires(source -> source.hasPermission(FreesiaConstants.PermissionConstants.DISPATCH_WORKER_COMMAND)) - .then( - BrigadierCommand.requiredArgumentBuilder("workerName", StringArgumentType.word()).suggests((ctx, builder) -> { - for (MasterServerMessageHandler connection : Freesia.registedWorkers.values()) { - builder.suggest(connection.getWorkerName()); - } - return builder.buildFuture(); - }) - .then( - BrigadierCommand.requiredArgumentBuilder("mcCommand", StringArgumentType.word()) - .executes(context -> { - final CommandSource source = context.getSource(); - final String workerName = StringArgumentType.getString(context, "workerName"); - final String command = StringArgumentType.getString(context, "mcCommand"); - - MasterServerMessageHandler targetWorkerConnection = null; - for (MasterServerMessageHandler connection : Freesia.registedWorkers.values()) { - if (workerName.equals(connection.getWorkerName())) { - targetWorkerConnection = connection; - break; - } - } - - if (targetWorkerConnection == null) { - source.sendMessage(Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.WORKER_NOT_FOUND, List.of(), List.of())); - return -1; - } - - targetWorkerConnection.dispatchCommandToWorker(command, feedback -> { - if (feedback == null) { - return; - } - - source.sendMessage(Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.WORKER_COMMAND_FEEDBACK, List.of("worker_name", "feedback"), List.of(workerName, feedback))); - }); - return Command.SINGLE_SUCCESS; - }) - ) - - - ).build(); - return new BrigadierCommand(registed); - } - -} diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/FreesiaCommand.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/FreesiaCommand.java new file mode 100644 index 0000000..b5eab42 --- /dev/null +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/FreesiaCommand.java @@ -0,0 +1,140 @@ +package com.nguyendevs.freesia.velocity.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import com.nguyendevs.freesia.velocity.FreesiaConstants; +import com.nguyendevs.freesia.velocity.Freesia; +import com.nguyendevs.freesia.velocity.network.backend.MasterServerMessageHandler; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class FreesiaCommand { + + public static void register() { + final CommandMeta meta = Freesia.PROXY_SERVER.getCommandManager() + .metaBuilder("freesia") + .plugin(Freesia.INSTANCE) + .build(); + + Freesia.PROXY_SERVER.getCommandManager().register(meta, create()); + } + + public static @NotNull BrigadierCommand create() { + LiteralCommandNode root = BrigadierCommand.literalArgumentBuilder("freesia") + + .then(BrigadierCommand.literalArgumentBuilder("listplayers") + .requires(source -> source.hasPermission(FreesiaConstants.PermissionConstants.LIST_PLAYER_COMMAND)) + .executes(context -> { + final Collection ysmPlayers = Freesia.PROXY_SERVER + .getAllPlayers() + .stream() + .filter(player -> Freesia.mapperManager.isPlayerInstalledYsm(player)) + .toList(); + + Component msg = Freesia.languageManager + .i18n(FreesiaConstants.LanguageConstants.PLAYER_LIST_HEADER, List.of(), List.of()) + .appendNewline(); + for (Player player : ysmPlayers) { + msg = msg.append(Freesia.languageManager.i18n( + FreesiaConstants.LanguageConstants.PLAYER_LIST_ENTRY, + List.of("name"), List.of(player.getUsername()))) + .appendNewline(); + } + + context.getSource().sendMessage(msg); + return Command.SINGLE_SUCCESS; + })) + + .then(BrigadierCommand.literalArgumentBuilder("dworkerc") + .requires(source -> source.hasPermission(FreesiaConstants.PermissionConstants.DISPATCH_WORKER_COMMAND)) + .then(BrigadierCommand.requiredArgumentBuilder("workerName", StringArgumentType.word()) + .suggests((ctx, builder) -> { + for (MasterServerMessageHandler conn : Freesia.registedWorkers.values()) { + if (conn.getWorkerName() != null) { + builder.suggest(conn.getWorkerName()); + } + } + return builder.buildFuture(); + }) + .then(BrigadierCommand.requiredArgumentBuilder("mcCommand", StringArgumentType.greedyString()) + .executes(context -> { + final CommandSource source = context.getSource(); + final String workerName = StringArgumentType.getString(context, "workerName"); + final String command = StringArgumentType.getString(context, "mcCommand"); + + MasterServerMessageHandler targetWorker = null; + for (MasterServerMessageHandler conn : Freesia.registedWorkers.values()) { + if (workerName.equals(conn.getWorkerName())) { + targetWorker = conn; + break; + } + } + + if (targetWorker == null) { + source.sendMessage(Freesia.languageManager.i18n( + FreesiaConstants.LanguageConstants.WORKER_NOT_FOUND, + List.of(), List.of())); + return -1; + } + + targetWorker.dispatchCommandToWorker(command, feedback -> { + if (feedback == null) { + return; + } + source.sendMessage(Freesia.languageManager.i18n( + FreesiaConstants.LanguageConstants.WORKER_COMMAND_FEEDBACK, + List.of("worker_name", "feedback"), + List.of(workerName, feedback.toString()))); + }); + return Command.SINGLE_SUCCESS; + })))) + + .then(BrigadierCommand.literalArgumentBuilder("setskin") + .requires(source -> source.hasPermission(FreesiaConstants.PermissionConstants.SET_SKIN_COMMAND)) + .then(BrigadierCommand.requiredArgumentBuilder("npcId", IntegerArgumentType.integer(0)) + .suggests((ctx, builder) -> { + for (Map.Entry entry : Freesia.npcMessageReceiver.getCachedNpcNames().entrySet()) { + builder.suggest(String.valueOf(entry.getKey())); + } + return builder.buildFuture(); + }) + .then(BrigadierCommand.requiredArgumentBuilder("modelId", StringArgumentType.word()) + .suggests((ctx, builder) -> { + for (String model : Freesia.mapperManager.getNpcModelBinaryCache().keySet()) { + if (model.toLowerCase().endsWith(".ysm")) { + builder.suggest(model.substring(0, model.length() - 4)); + } else { + builder.suggest(model); + } + } + return builder.buildFuture(); + }) + .executes(context -> { + final CommandSource source = context.getSource(); + final int npcId = IntegerArgumentType.getInteger(context, "npcId"); + final String modelId = StringArgumentType.getString(context, "modelId"); + + Freesia.mapperManager.npcPersistenceManager.saveAssignment(npcId, java.util.UUID.randomUUID(), modelId); + + source.sendMessage(Freesia.languageManager.i18n( + FreesiaConstants.LanguageConstants.SETSKIN_SUCCESS, + List.of("npc_id", "model_id"), + List.of(String.valueOf(npcId), modelId))); + return Command.SINGLE_SUCCESS; + })))) + + .build(); + + return new BrigadierCommand(root); + } +} diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/ListYsmPlayersCommand.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/ListYsmPlayersCommand.java deleted file mode 100644 index ac43f0a..0000000 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/command/ListYsmPlayersCommand.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.nguyendevs.freesia.velocity.command; - -import com.mojang.brigadier.Command; -import com.mojang.brigadier.tree.LiteralCommandNode; -import com.velocitypowered.api.command.BrigadierCommand; -import com.velocitypowered.api.command.CommandMeta; -import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.proxy.Player; -import com.nguyendevs.freesia.velocity.FreesiaConstants; -import com.nguyendevs.freesia.velocity.Freesia; -import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; - -public class ListYsmPlayersCommand { - public static void register() { - final CommandMeta meta = Freesia.PROXY_SERVER.getCommandManager() - .metaBuilder("listysmplayers") - .plugin(Freesia.INSTANCE) - .build(); - - Freesia.PROXY_SERVER.getCommandManager().register(meta, create()); - } - - public static @NotNull BrigadierCommand create() { - LiteralCommandNode registed = BrigadierCommand.literalArgumentBuilder("listysmplayers") - .requires(source -> source.hasPermission(FreesiaConstants.PermissionConstants.LIST_PLAYER_COMMAND)) - .executes(context -> { - final Collection ysmPlayers = Freesia.PROXY_SERVER - .getAllPlayers() - .stream() - .filter(player -> Freesia.mapperManager.isPlayerInstalledYsm(player)) - .toList(); - - Component msg = Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.PLAYER_LIST_HEADER, List.of(), List.of()).appendNewline(); - for (Player player : ysmPlayers) { - msg = msg - .append(Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.PLAYER_LIST_ENTRY, List.of("name"), List.of(player.getUsername()))) - .appendNewline(); - } - - context.getSource().sendMessage(msg); - return Command.SINGLE_SUCCESS; - }).build(); - - return new BrigadierCommand(registed); - } -} - diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/NpcMessageReceiver.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/NpcMessageReceiver.java new file mode 100644 index 0000000..8d42543 --- /dev/null +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/NpcMessageReceiver.java @@ -0,0 +1,53 @@ +package com.nguyendevs.freesia.velocity.network.misc; + +import com.nguyendevs.freesia.velocity.Freesia; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.util.HashMap; +import java.util.Map; + +public class NpcMessageReceiver { + + public static final MinecraftChannelIdentifier CHANNEL = MinecraftChannelIdentifier.create("freesia", "npc"); + private final Map cachedNpcNames = new HashMap<>(); + + public NpcMessageReceiver() { + Freesia.PROXY_SERVER.getChannelRegistrar().register(CHANNEL); + } + + @Subscribe + public void onPluginMessage(PluginMessageEvent event) { + if (!event.getIdentifier().equals(CHANNEL)) return; + + if (!(event.getTarget() instanceof Player watcher)) return; + + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(event.getData()))) { + byte opcode = in.readByte(); + + if (opcode == 0) { // OP_TRACK_SYNC + in.readLong(); in.readLong(); // discard UUID, we only care about player which we already have + int npcId = in.readInt(); + int entityId = in.readInt(); + + Freesia.mapperManager.handleNpcTrackSync(watcher, npcId, entityId); + } else if (opcode == 2) { // OP_RES_LIST + int count = in.readInt(); + cachedNpcNames.clear(); + for (int i = 0; i < count; i++) { + cachedNpcNames.put(in.readInt(), in.readUTF()); + } + } + } catch (Exception e) { + Freesia.LOGGER.warn("[NPC] Error reading proxy payload: " + e.getMessage()); + } + } + + public Map getCachedNpcNames() { + return cachedNpcNames; + } +} diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/NpcPersistenceManager.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/NpcPersistenceManager.java new file mode 100644 index 0000000..60ca589 --- /dev/null +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/NpcPersistenceManager.java @@ -0,0 +1,89 @@ +package com.nguyendevs.freesia.velocity.network.misc; + +import com.nguyendevs.freesia.velocity.Freesia; +import com.nguyendevs.freesia.velocity.FreesiaConstants; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class NpcPersistenceManager { + + private static final File ASSIGNMENTS_FILE = new File(FreesiaConstants.FileConstants.PLUGIN_DIR, "npc_assignments.dat"); + private static final File MODEL_CACHE_FILE = new File(FreesiaConstants.FileConstants.PLUGIN_DIR, "npc_model_cache.dat"); + + private final Map byId = new ConcurrentHashMap<>(); + + public record NpcEntry(int npcId, String modelId) {} + + public void load() { + byId.clear(); + if (!ASSIGNMENTS_FILE.exists()) return; + try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(ASSIGNMENTS_FILE)))) { + final int count = in.readInt(); + for (int i = 0; i < count; i++) { + final int npcId = in.readInt(); + in.readLong(); in.readLong(); + final String model = in.readUTF(); + byId.put(npcId, new NpcEntry(npcId, model)); + } + Freesia.LOGGER.info("[NPC] Loaded {} NPC assignments from disk", byId.size()); + } catch (Exception e) { + Freesia.LOGGER.warn("[NPC] Failed to load npc_assignments.dat: {}", e.getMessage()); + } + } + + public void saveAssignment(int npcId, java.util.UUID ignored, String modelId) { + byId.put(npcId, new NpcEntry(npcId, modelId)); + flush(); + } + + public Map getIdAssignments() { return byId; } + + public Map loadModelBinaryCache() { + final Map cache = new HashMap<>(); + if (!MODEL_CACHE_FILE.exists()) return cache; + try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(MODEL_CACHE_FILE)))) { + final int count = in.readInt(); + for (int i = 0; i < count; i++) { + final String model = in.readUTF(); + final int len = in.readInt(); + final byte[] data = new byte[len]; + in.readFully(data); + cache.put(model, data); + } + Freesia.LOGGER.info("[NPC] Loaded {} model binary entries from disk", cache.size()); + } catch (Exception e) { + Freesia.LOGGER.warn("[NPC] Failed to load npc_model_cache.dat: {}", e.getMessage()); + } + return cache; + } + + public void saveModelBinaryCache(Map cache) { + try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(MODEL_CACHE_FILE)))) { + out.writeInt(cache.size()); + for (Map.Entry e : cache.entrySet()) { + out.writeUTF(e.getKey()); + out.writeInt(e.getValue().length); + out.write(e.getValue()); + } + } catch (Exception e) { + Freesia.LOGGER.warn("[NPC] Failed to save npc_model_cache.dat: {}", e.getMessage()); + } + } + + private void flush() { + try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(ASSIGNMENTS_FILE)))) { + out.writeInt(byId.size()); + for (NpcEntry e : byId.values()) { + out.writeInt(e.npcId()); + out.writeLong(0); + out.writeLong(0); + out.writeUTF(e.modelId()); + } + } catch (Exception e) { + Freesia.LOGGER.warn("[NPC] Failed to save npc_assignments.dat: {}", e.getMessage()); + } + } +} diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/VirtualPlayerManager.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/VirtualPlayerManager.java index 7c7e9a1..14ca025 100644 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/VirtualPlayerManager.java +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/misc/VirtualPlayerManager.java @@ -6,6 +6,7 @@ import com.velocitypowered.api.event.EventTask; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import io.netty.buffer.Unpooled; @@ -21,10 +22,15 @@ public class VirtualPlayerManager { private static final MinecraftChannelIdentifier MANAGEMENT_CHANNEL_KEY = MinecraftChannelIdentifier.create("freesia", "virtual_player_management"); + public static final MinecraftChannelIdentifier CITIZENS_SETSKIN_CHANNEL = MinecraftChannelIdentifier.create("freesia", "citizens_setskin"); + public static final MinecraftChannelIdentifier CITIZENS_UUID_RESP_CHANNEL = MinecraftChannelIdentifier.create("freesia", "citizens_uuid_resp"); public void init() { Freesia.PROXY_SERVER.getChannelRegistrar().register(MANAGEMENT_CHANNEL_KEY); + Freesia.PROXY_SERVER.getChannelRegistrar().register(CITIZENS_SETSKIN_CHANNEL); + Freesia.PROXY_SERVER.getChannelRegistrar().register(CITIZENS_UUID_RESP_CHANNEL); Freesia.PROXY_SERVER.getEventManager().register(Freesia.INSTANCE, this); + // Model binary cache + NPC preload already done inside YsmMapperPayloadManager constructor } @Subscribe @@ -34,7 +40,15 @@ public EventTask onPluginMessage(@NotNull PluginMessageEvent event) { return; } - if (!event.getIdentifier().getId().equals(MANAGEMENT_CHANNEL_KEY.getId())) { + final String channelId = event.getIdentifier().getId(); + + if (channelId.equals(CITIZENS_UUID_RESP_CHANNEL.getId())) { + event.setResult(PluginMessageEvent.ForwardResult.handled()); + handleCitizensUuidResponse(event.getData()); + return; + } + + if (!channelId.equals(MANAGEMENT_CHANNEL_KEY.getId())) { return; } @@ -43,7 +57,7 @@ public EventTask onPluginMessage(@NotNull PluginMessageEvent event) { final FriendlyByteBuf packetData = new FriendlyByteBuf(Unpooled.wrappedBuffer(event.getData())); switch (packetData.readByte()) { - case 0 -> { // Create virtual player packet + case 0 -> { final int eventId = packetData.readVarInt(); final int entityId = packetData.readVarInt(); final UUID virtualPlayerUUID = packetData.readUUID(); @@ -53,16 +67,11 @@ public EventTask onPluginMessage(@NotNull PluginMessageEvent event) { response.writeByte(2); response.writeVarInt(eventId); response.writeBoolean(result); - ((ServerConnection) event.getSource()).sendPluginMessage(MANAGEMENT_CHANNEL_KEY, response.getBytes()); }; Freesia.mapperManager.addVirtualPlayer(virtualPlayerUUID, entityId).whenComplete((result, ex) -> { - if (ex != null) { - operationCallback.accept(false); - return; - } - + if (ex != null) { operationCallback.accept(false); return; } operationCallback.accept(result); }); } @@ -76,20 +85,27 @@ public EventTask onPluginMessage(@NotNull PluginMessageEvent event) { response.writeByte(2); response.writeVarInt(eventId); response.writeBoolean(result); - ((ServerConnection) event.getSource()).sendPluginMessage(MANAGEMENT_CHANNEL_KEY, response.getBytes()); }; Freesia.mapperManager.removeVirtualPlayer(virtualPlayerUUID).whenComplete((result, ex) -> { - if (ex != null) { - operationCallback.accept(false); - return; - } - + if (ex != null) { operationCallback.accept(false); return; } operationCallback.accept(result); }); } + // Virtual NPC tracker update — now includes entityId and optionally npcId (if Citizens) + case 3 -> { + final UUID virtualEntityUUID = packetData.readUUID(); + final int npcEntityId = packetData.readVarInt(); + final UUID watcherUUID = packetData.readUUID(); + + Freesia.mapperManager.updateVirtualPlayerEntityId(virtualEntityUUID, npcEntityId); + + Freesia.PROXY_SERVER.getPlayer(watcherUUID).ifPresent(watcher -> + Freesia.mapperManager.onVirtualPlayerTrackerUpdate(virtualEntityUUID, watcher)); + } + case 4 -> { final int eventId = packetData.readVarInt(); final UUID virtualPlayerUUID = packetData.readUUID(); @@ -99,9 +115,10 @@ public EventTask onPluginMessage(@NotNull PluginMessageEvent event) { Freesia.PROXY_SERVER.getScheduler().buildTask(Freesia.INSTANCE, () -> { final DefaultNBTSerializer serializer = new DefaultNBTSerializer(); final NBTCompound deserializedTag; - try { - deserializedTag = (NBTCompound) serializer.deserializeTag(NBTLimiter.forBuffer(null, Integer.MAX_VALUE), new DataInputStream(new ByteArrayInputStream(serializedNbt))); + deserializedTag = (NBTCompound) serializer.deserializeTag( + NBTLimiter.forBuffer(null, Integer.MAX_VALUE), + new DataInputStream(new ByteArrayInputStream(serializedNbt))); } catch (IOException e) { throw new RuntimeException(e); } @@ -111,16 +128,11 @@ public EventTask onPluginMessage(@NotNull PluginMessageEvent event) { response.writeByte(2); response.writeVarInt(eventId); response.writeBoolean(result); - ((ServerConnection) event.getSource()).sendPluginMessage(MANAGEMENT_CHANNEL_KEY, response.getBytes()); }; - Freesia.mapperManager.setVirtualPlayerEntityState(virtualPlayerUUID, deserializedTag).whenComplete((result ,ex) -> { - if (ex != null) { - operationCallback.accept(false); - return; - } - + Freesia.mapperManager.setVirtualPlayerEntityState(virtualPlayerUUID, deserializedTag).whenComplete((result, ex) -> { + if (ex != null) { operationCallback.accept(false); return; } operationCallback.accept(result); }); }).schedule(); @@ -128,5 +140,54 @@ public EventTask onPluginMessage(@NotNull PluginMessageEvent event) { } }); } -} + private void handleCitizensUuidResponse(byte[] data) { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(data)); + final int npcId = buf.readVarInt(); + final UUID npcUUID = buf.readUUID(); + final int npcEntityId = buf.readVarInt(); + final String modelId = buf.readUtf(); + + // Persist assignment (npcId + npcUUID + modelId) so proxy survives restarts + Freesia.mapperManager.npcPersistenceManager.saveAssignment(npcId, npcUUID, modelId); + + Freesia.mapperManager.addVirtualPlayer(npcUUID, npcEntityId).whenComplete((addResult, addEx) -> { + if (addEx != null) { + Freesia.LOGGER.error("[Citizens] Failed to register NPC {} as virtual player", npcUUID, addEx); + } + + final byte[] cachedBinary = Freesia.mapperManager.getCachedNpcModelBinary(modelId); + if (cachedBinary == null) { + Freesia.LOGGER.warn("[Citizens] No cached binary for model '{}'. A real player using this model must be online first, then run /freesia setskin again.", modelId); + return; + } + + Freesia.mapperManager.setVirtualPlayerEntityStateBinary(npcUUID, cachedBinary) + .whenComplete((result, ex) -> { + if (ex != null) { + Freesia.LOGGER.error("[Citizens] Failed to set model for NPC {}", npcUUID, ex); + return; + } + if (Boolean.TRUE.equals(result)) { + Freesia.LOGGER.info("[Citizens] Model '{}' applied to NPC {} (cached binary {} bytes)", + modelId, npcUUID, cachedBinary.length); + } else { + Freesia.LOGGER.warn("[Citizens] setVirtualPlayerEntityStateBinary returned false for NPC {}", npcUUID); + } + }); + }); + } + + public boolean sendSetskinToBackendViaAnyPlayer(int npcId, String modelId) { + final Player carrier = Freesia.PROXY_SERVER.getAllPlayers().stream().findFirst().orElse(null); + if (carrier == null) return false; + + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeVarInt(npcId); + buf.writeUtf(modelId); + + carrier.getCurrentServer().ifPresent(serverConn -> + serverConn.sendPluginMessage(CITIZENS_SETSKIN_CHANNEL, buf.getBytes())); + return true; + } +} diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/MapperSessionProcessor.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/MapperSessionProcessor.java index f733488..0785ff1 100644 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/MapperSessionProcessor.java +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/MapperSessionProcessor.java @@ -34,6 +34,9 @@ public class MapperSessionProcessor implements SessionListener { private final MultiThreadedQueue pendingYsmPacketsInbound = new MultiThreadedQueue<>(); private final MultiThreadedQueue pendingTrackerUpdatesTo = new MultiThreadedQueue<>(); private final MultiThreadedQueue pendingYsmPacketsOutbound = new MultiThreadedQueue<>(); + private final MultiThreadedQueue pendingNpcTrackerUpdates = new MultiThreadedQueue<>(); + + public record NpcTrackerUpdate(int entityId, byte[] binary) {} private volatile Session session; private boolean kickMasterWhenDisconnect = true; @@ -58,6 +61,10 @@ protected boolean queueTrackerUpdate(UUID target) { return this.pendingTrackerUpdatesTo.offer(target); } + protected boolean queueNpcTrackerUpdate(int entityId, byte[] binary) { + return this.pendingNpcTrackerUpdates.offer(new NpcTrackerUpdate(entityId, binary)); + } + protected void retireTrackerCallbacks() { UUID toSend; while ((toSend = this.pendingTrackerUpdatesTo.pollOrBlockAdds()) != null) { @@ -71,6 +78,11 @@ protected void retireTrackerCallbacks() { this.packetProxy.sendEntityStateTo(targetPlayer); } + + NpcTrackerUpdate npcUpdate; + while ((npcUpdate = this.pendingNpcTrackerUpdates.pollOrBlockAdds()) != null) { + this.mapperPayloadManager.sendEntityStateToRaw(this.bindPlayer.getUniqueId(), npcUpdate.entityId(), YsmState.ofBinary(npcUpdate.binary())); + } } public boolean sendPacket(Packet packet) { diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/RealPlayerYsmPacketProxyImpl.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/RealPlayerYsmPacketProxyImpl.java index 7cc3b77..1295f74 100644 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/RealPlayerYsmPacketProxyImpl.java +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/RealPlayerYsmPacketProxyImpl.java @@ -50,6 +50,21 @@ public ProxyComputeResult processS2C(Key key, ByteBuf copiedPacketData) { byte[] binaryData = new byte[mcBuffer.readableBytes()]; mcBuffer.readBytes(binaryData); state = YsmState.ofBinary(binaryData); + if (FreesiaConfig.debug) { + final StringBuilder hex = new StringBuilder(); + for (int i = 0; i < Math.min(binaryData.length, 32); i++) { + hex.append(String.format("%02X ", binaryData[i])); + } + Freesia.LOGGER.info("[DEBUG] YSM2.6 binary state for {} (len={}): {}", + this.player.getUsername(), binaryData.length, hex); + } + try { + final FriendlyByteBuf probe = new FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(binaryData)); + final String modelPath = probe.readUtf(); + if (!modelPath.isEmpty()) { + Freesia.mapperManager.cacheNpcModelBinary(modelPath, binaryData); + } + } catch (Exception ignored) {} } else { state = YsmState.ofNbt(this.nbtRemapper.readBound(mcBuffer)); } diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/VirtualYsmPacketProxyImpl.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/VirtualYsmPacketProxyImpl.java index 1e6b19a..302335e 100644 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/VirtualYsmPacketProxyImpl.java +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/VirtualYsmPacketProxyImpl.java @@ -1,5 +1,6 @@ package com.nguyendevs.freesia.velocity.network.ysm; +import com.velocitypowered.api.proxy.Player; import io.netty.buffer.ByteBuf; import com.nguyendevs.freesia.velocity.Freesia; import net.kyori.adventure.key.Key; @@ -7,6 +8,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; public class VirtualYsmPacketProxyImpl extends YsmPacketProxyLayer { public VirtualYsmPacketProxyImpl(UUID virtualPlayerUUID) { @@ -15,7 +17,14 @@ public VirtualYsmPacketProxyImpl(UUID virtualPlayerUUID) { @Override public CompletableFuture> fetchTrackerList(UUID observer) { - return Freesia.tracker.getCanSee(observer); + if (Freesia.PROXY_SERVER.getPlayer(observer).isPresent()) { + return Freesia.tracker.getCanSee(observer); + } + final Set allYsm = Freesia.PROXY_SERVER.getAllPlayers().stream() + .filter(p -> Freesia.mapperManager.isPlayerInstalledYsm(p)) + .map(Player::getUniqueId) + .collect(Collectors.toSet()); + return CompletableFuture.completedFuture(allYsm); } @Override diff --git a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/YsmMapperPayloadManager.java b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/YsmMapperPayloadManager.java index ef5e459..d0ab9a2 100644 --- a/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/YsmMapperPayloadManager.java +++ b/Freesia-Velocity/src/main/java/com/nguyendevs/freesia/velocity/network/ysm/YsmMapperPayloadManager.java @@ -24,6 +24,7 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.IOException; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -48,12 +49,23 @@ public class YsmMapperPayloadManager { private final Set ysmInstalledPlayers = Sets.newConcurrentHashSet(); private final Map> worker2PlayerEntityIdCache = Maps.newConcurrentMap(); + private final Map npcModelBinaryCache = Maps.newConcurrentMap(); + private final Map> playerTrackedNpcs = Maps.newConcurrentMap(); + + public final com.nguyendevs.freesia.velocity.network.misc.NpcPersistenceManager npcPersistenceManager + = new com.nguyendevs.freesia.velocity.network.misc.NpcPersistenceManager(); + + public Map getNpcModelBinaryCache() { + return npcModelBinaryCache; + } public YsmMapperPayloadManager(Function packetProxyCreator, Function packetProxyCreatorVirtual) { this.packetProxyCreator = packetProxyCreator; this.packetProxyCreatorVirtual = packetProxyCreatorVirtual; this.backend2Players.put(FreesiaConfig.workerMSessionAddress, 1); + this.npcModelBinaryCache.putAll(this.npcPersistenceManager.loadModelBinaryCache()); + this.npcPersistenceManager.load(); } @Nullable @@ -151,6 +163,69 @@ public CompletableFuture setVirtualPlayerEntityState(UUID playerUUID, N return callback; } + public CompletableFuture setVirtualPlayerEntityStateBinary(UUID playerUUID, byte[] binary) { + final YsmPacketProxy virtualProxy; + + synchronized (this.virtualProxies) { + virtualProxy = this.virtualProxies.get(playerUUID); + } + + if (virtualProxy == null) { + return CompletableFuture.completedFuture(false); + } + + virtualProxy.setEntityDataRaw(YsmState.ofBinary(binary)); + virtualProxy.notifyFullTrackerUpdates(); + return CompletableFuture.completedFuture(true); + } + + public void cacheNpcModelBinary(String modelPath, byte[] binary) { + final String key = modelPath.toLowerCase(); + if (!npcModelBinaryCache.containsKey(key)) { + npcModelBinaryCache.put(key, binary); + Freesia.LOGGER.info("[YSM] Cached model binary for '{}' ({} bytes)", modelPath, binary.length); + npcPersistenceManager.saveModelBinaryCache(npcModelBinaryCache); + } + } + + public byte[] getCachedNpcModelBinary(String modelId) { + byte[] result = npcModelBinaryCache.get(modelId); + if (result != null) return result; + result = npcModelBinaryCache.get(modelId.toLowerCase()); + if (result != null) return result; + result = npcModelBinaryCache.get(modelId + ".ysm"); + if (result != null) return result; + return npcModelBinaryCache.get(modelId.toLowerCase() + ".ysm"); + } + + public void handleNpcTrackSync(Player watcher, int npcId, int entityId) { + com.nguyendevs.freesia.velocity.network.misc.NpcPersistenceManager.NpcEntry entry = npcPersistenceManager.getIdAssignments().get(npcId); + if (entry != null) { + byte[] binary = getCachedNpcModelBinary(entry.modelId()); + if (binary != null) { + final MapperSessionProcessor mapperSession = this.mapperSessions.get(watcher); + if (mapperSession != null) { + if (this.isPlayerInstalledYsm(watcher)) { + this.sendEntityStateToRaw(watcher.getUniqueId(), entityId, YsmState.ofBinary(binary)); + } else { + mapperSession.queueNpcTrackerUpdate(entityId, binary); + } + } + } else { + Freesia.LOGGER.warn("[NPC] Binary missing for model '{}' on NPC ID {}", entry.modelId(), npcId); + } + } + } + + public void updateVirtualPlayerEntityId(java.util.UUID playerUUID, int entityId) { + synchronized (this.virtualProxies) { + final YsmPacketProxy proxy = this.virtualProxies.get(playerUUID); + if (proxy != null) { + proxy.setPlayerEntityId(entityId); + } + } + } + public CompletableFuture addVirtualPlayer(UUID playerUUID, int playerEntityId) { if (Freesia.PROXY_SERVER.getPlayer(playerUUID).isPresent()) { return CompletableFuture.completedFuture(false); @@ -158,7 +233,9 @@ public CompletableFuture addVirtualPlayer(UUID playerUUID, int playerEn synchronized (this.virtualProxies) { if (this.virtualProxies.containsKey(playerUUID)) { - return CompletableFuture.completedFuture(false); + final YsmPacketProxy existing = this.virtualProxies.get(playerUUID); + existing.setPlayerEntityId(playerEntityId); + return CompletableFuture.completedFuture(true); } final CompletableFuture callback = new CompletableFuture<>(); @@ -188,8 +265,10 @@ public CompletableFuture addVirtualPlayer(UUID playerUUID, int playerEn createdVirtualProxy.setPlayerEntityId(playerEntityId); createdVirtualProxy.setPlayerWorkerEntityId(0); } catch (Exception ex1) { - callback.completeExceptionally(ex1); - return; + Freesia.LOGGER.warn("[addVirtualPlayer] Corrupt stored data for {}, starting clean: {}", + playerUUID, ex1.getMessage()); + createdVirtualProxy.setPlayerEntityId(playerEntityId); + createdVirtualProxy.setPlayerWorkerEntityId(0); } callback.complete(true); @@ -337,6 +416,46 @@ public void onBackendReady(Player player) { mapperSession.onBackendReady(); } + public void sendEntityStateToRaw(@NotNull UUID receiverUUID, int entityId, YsmState data) { + final Optional queryResult = Freesia.PROXY_SERVER.getPlayer(receiverUUID); + + if (queryResult.isEmpty()) { + return; + } + + final Player receiver = queryResult.get(); + final Object targetChannel = com.github.retrooper.packetevents.PacketEvents.getAPI().getProtocolManager().getChannel(receiver.getUniqueId()); + + if (targetChannel == null) { + return; + } + + final com.github.retrooper.packetevents.protocol.player.ClientVersion clientVersion = com.github.retrooper.packetevents.PacketEvents.getAPI().getProtocolManager() + .getClientVersion(targetChannel); + + final int targetProtocolVer = clientVersion.getProtocolVersion(); + final com.nguyendevs.freesia.velocity.utils.FriendlyByteBuf wrappedPacketData = new com.nguyendevs.freesia.velocity.utils.FriendlyByteBuf(io.netty.buffer.Unpooled.buffer()); + + wrappedPacketData.writeByte(4); + wrappedPacketData.writeVarInt(entityId); + + if (data.isBinary()) { + wrappedPacketData.writeBytes(data.getBinary()); + } else { + try { + com.nguyendevs.freesia.velocity.network.mc.NbtRemapper remapper = new com.nguyendevs.freesia.velocity.network.mc.impl.StandardNbtRemapperImpl(); + wrappedPacketData.writeBytes(remapper.shouldRemap(targetProtocolVer) + ? remapper.remapToMasterVer(data.getNbt()) + : remapper.remapToWorkerVer(data.getNbt())); + } catch (IOException e) { + Freesia.LOGGER.error("Failed to remap NBT for raw entity state: {}", e.getMessage()); + return; + } + } + + receiver.sendPluginMessage(YSM_CHANNEL_KEY_VELOCITY, wrappedPacketData.getBytes()); + } + public static class SavedProxyState { public final YsmState state; public final String version; @@ -443,8 +562,17 @@ public void onVirtualPlayerTrackerUpdate(UUID owner, Player watcher) { return; } + final MapperSessionProcessor mapperSession = this.mapperSessions.get(watcher); + + if (mapperSession == null) { + return; + } + if (this.isPlayerInstalledYsm(watcher)) { - virtualProxy.sendEntityStateTo(watcher); + // Queue it properly through the mapper session so we don't send to unprepared clients + if (!mapperSession.queueTrackerUpdate(owner)) { + virtualProxy.sendEntityStateTo(watcher); + } } } diff --git a/Freesia-Velocity/src/main/resources/lang/en_US.lang b/Freesia-Velocity/src/main/resources/lang/en_US.lang index 5cbfec2..2445afc 100644 --- a/Freesia-Velocity/src/main/resources/lang/en_US.lang +++ b/Freesia-Velocity/src/main/resources/lang/en_US.lang @@ -1,7 +1,10 @@ -freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm backend has been disconnected, reason: -freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> There is not a valid connection to ysm backend! If you still meet this issue. please report to the administrators -freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Cannot found specified worker -freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] -freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Handshake time outed, please ensure you have installed the corrected version of ysm -freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: -freesia.list_player_command_body=<#cdd6f4> \ No newline at end of file +freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm backend has been disconnected, reason: +freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> There is not a valid connection to ysm backend! If you still meet this issue. please report to the administrators +freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Cannot found specified worker +freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] +freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Handshake time outed, please ensure you have installed the corrected version of ysm +freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: +freesia.list_player_command_body=<#cdd6f4> +freesia.setskin.success=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#a6e3a1> Sent setskin request for NPC with model . Check backend logs for result. +freesia.setskin.no_players=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> No online players to relay the command to Backend. +freesia.setskin.citizens_disabled=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Citizens is not enabled on the Backend server. \ No newline at end of file diff --git a/Freesia-Velocity/src/main/resources/lang/vi_VN.lang b/Freesia-Velocity/src/main/resources/lang/vi_VN.lang index c9a0e25..98ec4cb 100644 --- a/Freesia-Velocity/src/main/resources/lang/vi_VN.lang +++ b/Freesia-Velocity/src/main/resources/lang/vi_VN.lang @@ -1,7 +1,10 @@ -freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Backend Ysm đã ngắt kết nối, lý do: -freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không có kết nối hợp lệ tới backend Ysm! Nếu bạn vẫn gặp sự cố này, vui lòng báo cáo cho quản trị viên -freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không tìm thấy worker được chỉ định -freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] -freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Bắt tay kết nối đã hết thời gian chờ, vui lòng đảm bảo bạn đã cài đúng phiên bản Ysm -freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Danh sách người chơi Ysm: -freesia.list_player_command_body=<#cdd6f4> \ No newline at end of file +freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Backend Ysm đã ngắt kết nối, lý do: +freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không có kết nối hợp lệ tới backend Ysm! Nếu bạn vẫn gặp sự cố này, vui lòng báo cáo cho quản trị viên +freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không tìm thấy worker được chỉ định +freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] +freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Bắt tay kết nối đã hết thời gian chờ, vui lòng đảm bảo bạn đã cài đúng phiên bản Ysm +freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Danh sách người chơi Ysm: +freesia.list_player_command_body=<#cdd6f4> +freesia.setskin.success=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#a6e3a1> Đã gửi yêu cầu setskin cho NPC với model . Kiểm tra log backend để xem kết quả. +freesia.setskin.no_players=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không có người chơi nào online để chuyển tiếp lệnh tới Backend. +freesia.setskin.citizens_disabled=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Citizens chưa được bật trên server Backend. \ No newline at end of file diff --git a/Freesia-Velocity/src/main/resources/lang/zh_CN.lang b/Freesia-Velocity/src/main/resources/lang/zh_CN.lang index 3fcd8f1..5b30857 100644 --- a/Freesia-Velocity/src/main/resources/lang/zh_CN.lang +++ b/Freesia-Velocity/src/main/resources/lang/zh_CN.lang @@ -1,7 +1,10 @@ -freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm后端worker断开了连接,原因: -freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 你没有连接到Ysm后端worker.如果反复遇到该问题,请汇报管理员! -freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 找不到该worker! -freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] -freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm握手超时,请确认您安装了正确版本的Ysm -freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: -freesia.list_player_command_body=<#cdd6f4> \ No newline at end of file +freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm后端worker断开了连接,原因: +freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 你没有连接到Ysm后端worker.如果反复遇到该问题,请汇报管理员! +freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 找不到该worker! +freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] +freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm握手超时,请确认您安装了正确版本的Ysm +freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: +freesia.list_player_command_body=<#cdd6f4> +freesia.setskin.success=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#a6e3a1> 已发送NPC 的setskin请求,模型为 。请查看后端日志确认结果。 +freesia.setskin.no_players=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 没有在线玩家可用于转发命令到后端。 +freesia.setskin.citizens_disabled=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Citizens未在后端服务器上启用。 \ No newline at end of file diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/Freesia.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/Freesia.java index b65ea5f..a5b2137 100644 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/Freesia.java +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/Freesia.java @@ -9,8 +9,7 @@ import com.google.common.collect.Maps; import com.nguyendevs.freesia.common.EntryPoint; import com.nguyendevs.freesia.common.communicating.NettySocketServer; -import com.nguyendevs.freesia.waterfall.command.ListYsmPlayersCommand; -import com.nguyendevs.freesia.waterfall.command.DispatchWorkerCommandCommand; +import com.nguyendevs.freesia.waterfall.command.FreesiaCommand; import com.nguyendevs.freesia.waterfall.i18n.I18NManager; import com.nguyendevs.freesia.waterfall.network.backend.MasterServerMessageHandler; import com.nguyendevs.freesia.waterfall.network.mc.FreesiaPlayerTracker; @@ -52,6 +51,7 @@ public class Freesia extends Plugin implements PacketListener, Listener { public static YsmClientKickingDetector kickChecker; public static YsmMapperPayloadManager mapperManager; public static NettySocketServer masterServer; + public static com.nguyendevs.freesia.waterfall.network.misc.NpcMessageReceiver npcMessageReceiver; private static void printLogo() { String RESET = "\u001B[0m"; @@ -109,6 +109,9 @@ public void onEnable() { virtualPlayerManager.init(); + npcMessageReceiver = new com.nguyendevs.freesia.waterfall.network.misc.NpcMessageReceiver(); + getProxy().getPluginManager().registerListener(this, npcMessageReceiver); + io.netty.handler.ssl.SslContext sslContext = null; try { if (FreesiaSecurityConfig.enableTls) { @@ -142,8 +145,8 @@ public void onEnable() { kickChecker.bootstrap(); LOGGER.info("Registering commands"); - this.getProxy().getPluginManager().registerCommand(this, new DispatchWorkerCommandCommand()); - this.getProxy().getPluginManager().registerCommand(this, new ListYsmPlayersCommand()); + FreesiaCommand.register(); + } @EventHandler diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/FreesiaConstants.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/FreesiaConstants.java index aa627b3..e622f95 100644 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/FreesiaConstants.java +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/FreesiaConstants.java @@ -22,7 +22,8 @@ public static final class FileConstants { public static final class PermissionConstants { public static final String LIST_PLAYER_COMMAND = "freesia.commands.listysmplayers", - DISPATCH_WORKER_COMMAND = "freesia.commands.dworkerc"; + DISPATCH_WORKER_COMMAND = "freesia.commands.dworkerc", + SET_SKIN_COMMAND = "freesia.commands.setskin"; } public static final class LanguageConstants { @@ -32,6 +33,10 @@ public static final class LanguageConstants { PLAYER_LIST_HEADER = "freesia.list_player_command_header", PLAYER_LIST_ENTRY = "freesia.list_player_command_body", + SETSKIN_SUCCESS = "freesia.setskin.success", + SETSKIN_NO_PLAYERS = "freesia.setskin.no_players", + SETSKIN_CITIZENS_DISABLED = "freesia.setskin.citizens_disabled", + HANDSHAKE_TIMED_OUT = "freesia.mod_handshake_time_outed", WORKER_TERMINATED_CONNECTION = "freesia.backend.disconnected", WORKER_NOT_CONNECTED = "freesia.backend.not_connected"; diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/DispatchWorkerCommandCommand.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/DispatchWorkerCommandCommand.java deleted file mode 100644 index 89dcd03..0000000 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/DispatchWorkerCommandCommand.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.nguyendevs.freesia.waterfall.command; - -import com.nguyendevs.freesia.waterfall.FreesiaConstants; -import com.nguyendevs.freesia.waterfall.Freesia; -import com.nguyendevs.freesia.waterfall.network.backend.MasterServerMessageHandler; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.plugin.Command; -import net.md_5.bungee.api.plugin.TabExecutor; - -import java.util.ArrayList; -import java.util.List; - -public class DispatchWorkerCommandCommand extends Command implements TabExecutor { - - public DispatchWorkerCommandCommand() { - super("dworkerc", FreesiaConstants.PermissionConstants.DISPATCH_WORKER_COMMAND); - } - - public static void register() { - Freesia.PROXY_SERVER.getPluginManager().registerCommand(Freesia.INSTANCE, new DispatchWorkerCommandCommand()); - } - - @Override - public void execute(CommandSender sender, String[] args) { - if (args.length < 2) { - sender.sendMessage(TextComponent.fromLegacyText("Usage: /dworkerc ")); - return; - } - - String workerName = args[0]; - StringBuilder commandBuilder = new StringBuilder(); - for (int i = 1; i < args.length; i++) { - commandBuilder.append(args[i]).append(" "); - } - String command = commandBuilder.toString().trim(); - - MasterServerMessageHandler targetWorkerConnection = null; - for (MasterServerMessageHandler connection : Freesia.registedWorkers.values()) { - if (workerName.equals(connection.getWorkerName())) { - targetWorkerConnection = connection; - break; - } - } - - if (targetWorkerConnection == null) { - sender.sendMessage(TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize( - Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.WORKER_NOT_FOUND, List.of(), - List.of())))); - return; - } - - targetWorkerConnection.dispatchCommandToWorker(command, feedback -> { - if (feedback == null) { - return; - } - - sender.sendMessage(TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize( - Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.WORKER_COMMAND_FEEDBACK, - List.of("worker_name", "feedback"), List.of(workerName, feedback))))); - }); - } - - @Override - public Iterable onTabComplete(CommandSender sender, String[] args) { - List completions = new ArrayList<>(); - if (args.length == 1) { - for (MasterServerMessageHandler connection : Freesia.registedWorkers.values()) { - if (connection.getWorkerName().toLowerCase().startsWith(args[0].toLowerCase())) { - completions.add(connection.getWorkerName()); - } - } - } - return completions; - } -} - diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/FreesiaCommand.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/FreesiaCommand.java new file mode 100644 index 0000000..c331a5f --- /dev/null +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/FreesiaCommand.java @@ -0,0 +1,192 @@ +package com.nguyendevs.freesia.waterfall.command; + +import com.nguyendevs.freesia.waterfall.FreesiaConstants; +import com.nguyendevs.freesia.waterfall.Freesia; +import com.nguyendevs.freesia.waterfall.network.backend.MasterServerMessageHandler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.TabExecutor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class FreesiaCommand extends Command implements TabExecutor { + + public FreesiaCommand() { + super("freesia", null); + } + + public static void register() { + Freesia.PROXY_SERVER.getPluginManager().registerCommand(Freesia.INSTANCE, new FreesiaCommand()); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length == 0) { + sendUsage(sender); + return; + } + + switch (args[0].toLowerCase()) { + case "listplayers" -> handleListPlayers(sender); + case "dworkerc" -> handleDispatchWorker(sender, args); + case "setskin" -> handleSetSkin(sender, args); + default -> sendUsage(sender); + } + } + + private void handleListPlayers(CommandSender sender) { + if (!sender.hasPermission(FreesiaConstants.PermissionConstants.LIST_PLAYER_COMMAND)) { + sender.sendMessage(TextComponent.fromLegacyText("§cNo permission.")); + return; + } + + final Collection ysmPlayers = Freesia.PROXY_SERVER + .getPlayers() + .stream() + .filter(player -> Freesia.mapperManager.isPlayerInstalledYsm(player)) + .collect(Collectors.toList()); + + Component msg = Freesia.languageManager + .i18n(FreesiaConstants.LanguageConstants.PLAYER_LIST_HEADER, List.of(), List.of()).appendNewline(); + for (ProxiedPlayer player : ysmPlayers) { + msg = msg + .append(Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.PLAYER_LIST_ENTRY, + List.of("name"), List.of(player.getName()))) + .appendNewline(); + } + + sender.sendMessage(TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize(msg))); + } + + private void handleDispatchWorker(CommandSender sender, String[] args) { + if (!sender.hasPermission(FreesiaConstants.PermissionConstants.DISPATCH_WORKER_COMMAND)) { + sender.sendMessage(TextComponent.fromLegacyText("§cNo permission.")); + return; + } + + if (args.length < 3) { + sender.sendMessage(TextComponent.fromLegacyText("Usage: /freesia dworkerc ")); + return; + } + + final String workerName = args[1]; + final StringBuilder commandBuilder = new StringBuilder(); + for (int i = 2; i < args.length; i++) { + commandBuilder.append(args[i]).append(" "); + } + final String command = commandBuilder.toString().trim(); + + MasterServerMessageHandler targetWorker = null; + for (MasterServerMessageHandler conn : Freesia.registedWorkers.values()) { + if (workerName.equals(conn.getWorkerName())) { + targetWorker = conn; + break; + } + } + + if (targetWorker == null) { + sender.sendMessage(TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize( + Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.WORKER_NOT_FOUND, + List.of(), List.of())))); + return; + } + + targetWorker.dispatchCommandToWorker(command, feedback -> { + if (feedback == null) { + return; + } + sender.sendMessage(TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize( + Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.WORKER_COMMAND_FEEDBACK, + List.of("worker_name", "feedback"), List.of(workerName, feedback.toString()))))); + }); + } + + private void handleSetSkin(CommandSender sender, String[] args) { + if (!sender.hasPermission(FreesiaConstants.PermissionConstants.SET_SKIN_COMMAND)) { + sender.sendMessage(TextComponent.fromLegacyText("§cNo permission.")); + return; + } + + if (args.length < 3) { + sender.sendMessage(TextComponent.fromLegacyText("Usage: /freesia setskin ")); + return; + } + + final int npcId; + try { + npcId = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + sender.sendMessage(TextComponent.fromLegacyText("§c must be an integer.")); + return; + } + + final String modelId = args[2]; + + Freesia.mapperManager.npcPersistenceManager.saveAssignment(npcId, UUID.randomUUID(), modelId); + + sender.sendMessage(TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize( + Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.SETSKIN_SUCCESS, + List.of("npc_id", "model_id"), List.of(String.valueOf(npcId), modelId))))); + } + + private void sendUsage(CommandSender sender) { + sender.sendMessage(TextComponent.fromLegacyText("§6/freesia §elistplayers")); + sender.sendMessage(TextComponent.fromLegacyText("§6/freesia §edworkerc §7 ")); + sender.sendMessage(TextComponent.fromLegacyText("§6/freesia §esetskin §7 ")); + } + + @Override + public Iterable onTabComplete(CommandSender sender, String[] args) { + final List completions = new ArrayList<>(); + + if (args.length == 1) { + for (String sub : List.of("listplayers", "dworkerc", "setskin")) { + if (sub.startsWith(args[0].toLowerCase())) { + completions.add(sub); + } + } + return completions; + } + + if (args.length == 2) { + if (args[0].equalsIgnoreCase("dworkerc")) { + for (MasterServerMessageHandler conn : Freesia.registedWorkers.values()) { + if (conn.getWorkerName() != null && conn.getWorkerName().toLowerCase().startsWith(args[1].toLowerCase())) { + completions.add(conn.getWorkerName()); + } + } + } else if (args[0].equalsIgnoreCase("setskin")) { + Freesia.npcMessageReceiver.getCachedNpcNames().forEach((id, name) -> { + String sid = String.valueOf(id); + if (sid.startsWith(args[1])) { + completions.add(sid); + } + }); + } + } + + if (args.length == 3 && args[0].equalsIgnoreCase("setskin")) { + Freesia.mapperManager.getNpcModelBinaryCache().keySet().forEach(model -> { + String suggestion; + if (model.toLowerCase().endsWith(".ysm")) { + suggestion = model.substring(0, model.length() - 4); + } else { + suggestion = model; + } + if (suggestion.toLowerCase().startsWith(args[2].toLowerCase())) { + completions.add(suggestion); + } + }); + } + + return completions; + } +} diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/ListYsmPlayersCommand.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/ListYsmPlayersCommand.java deleted file mode 100644 index 68ba5ac..0000000 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/command/ListYsmPlayersCommand.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.nguyendevs.freesia.waterfall.command; - -import com.nguyendevs.freesia.waterfall.FreesiaConstants; -import com.nguyendevs.freesia.waterfall.Freesia; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.plugin.Command; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -public class ListYsmPlayersCommand extends Command { - - public ListYsmPlayersCommand() { - super("listysmplayers", FreesiaConstants.PermissionConstants.LIST_PLAYER_COMMAND); - } - - public static void register() { - Freesia.PROXY_SERVER.getPluginManager().registerCommand(Freesia.INSTANCE, new ListYsmPlayersCommand()); - } - - @Override - public void execute(CommandSender sender, String[] args) { - final Collection ysmPlayers = Freesia.PROXY_SERVER - .getPlayers() - .stream() - .filter(player -> Freesia.mapperManager.isPlayerInstalledYsm(player)) - .collect(Collectors.toList()); - - Component msg = Freesia.languageManager - .i18n(FreesiaConstants.LanguageConstants.PLAYER_LIST_HEADER, List.of(), List.of()).appendNewline(); - for (ProxiedPlayer player : ysmPlayers) { - msg = msg - .append(Freesia.languageManager.i18n(FreesiaConstants.LanguageConstants.PLAYER_LIST_ENTRY, - List.of("name"), List.of(player.getName()))) - .appendNewline(); - } - - sender.sendMessage(TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize(msg))); - } -} - diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/NpcMessageReceiver.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/NpcMessageReceiver.java new file mode 100644 index 0000000..d9b9473 --- /dev/null +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/NpcMessageReceiver.java @@ -0,0 +1,52 @@ +package com.nguyendevs.freesia.waterfall.network.misc; + +import com.nguyendevs.freesia.waterfall.Freesia; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PluginMessageEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.util.HashMap; +import java.util.Map; + +public class NpcMessageReceiver implements Listener { + + public static final String CHANNEL = "freesia:npc"; + private final Map cachedNpcNames = new HashMap<>(); + + public NpcMessageReceiver() { + Freesia.PROXY_SERVER.registerChannel(CHANNEL); + } + + @EventHandler + public void onPluginMessage(PluginMessageEvent event) { + if (!event.getTag().equals(CHANNEL)) return; + + if (!(event.getReceiver() instanceof ProxiedPlayer watcher)) return; + + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(event.getData()))) { + byte opcode = in.readByte(); + + if (opcode == 0) { // OP_TRACK_SYNC + in.readLong(); in.readLong(); // discard UUID + int npcId = in.readInt(); + int entityId = in.readInt(); + + Freesia.mapperManager.handleNpcTrackSync(watcher, npcId, entityId); + } else if (opcode == 2) { // OP_RES_LIST + int count = in.readInt(); + cachedNpcNames.clear(); + for (int i = 0; i < count; i++) { + cachedNpcNames.put(in.readInt(), in.readUTF()); + } + } + } catch (Exception e) { + Freesia.LOGGER.warning("[NPC] Error reading proxy payload: " + e.getMessage()); + } + } + + public Map getCachedNpcNames() { + return cachedNpcNames; + } +} diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/NpcPersistenceManager.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/NpcPersistenceManager.java new file mode 100644 index 0000000..97021ee --- /dev/null +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/NpcPersistenceManager.java @@ -0,0 +1,89 @@ +package com.nguyendevs.freesia.waterfall.network.misc; + +import com.nguyendevs.freesia.waterfall.Freesia; +import com.nguyendevs.freesia.waterfall.FreesiaConstants; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class NpcPersistenceManager { + + private static final File ASSIGNMENTS_FILE = new File(FreesiaConstants.FileConstants.PLUGIN_DIR, "npc_assignments.dat"); + private static final File MODEL_CACHE_FILE = new File(FreesiaConstants.FileConstants.PLUGIN_DIR, "npc_model_cache.dat"); + + private final Map byId = new ConcurrentHashMap<>(); + + public record NpcEntry(int npcId, String modelId) {} + + public void load() { + byId.clear(); + if (!ASSIGNMENTS_FILE.exists()) return; + try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(ASSIGNMENTS_FILE)))) { + final int count = in.readInt(); + for (int i = 0; i < count; i++) { + final int npcId = in.readInt(); + in.readLong(); in.readLong(); + final String model = in.readUTF(); + byId.put(npcId, new NpcEntry(npcId, model)); + } + Freesia.LOGGER.info("[NPC] Loaded " + byId.size() + " NPC assignments from disk"); + } catch (Exception e) { + Freesia.LOGGER.warning("[NPC] Failed to load npc_assignments.dat: " + e.getMessage()); + } + } + + public void saveAssignment(int npcId, java.util.UUID ignored, String modelId) { + byId.put(npcId, new NpcEntry(npcId, modelId)); + flush(); + } + + public Map getIdAssignments() { return byId; } + + public Map loadModelBinaryCache() { + final Map cache = new HashMap<>(); + if (!MODEL_CACHE_FILE.exists()) return cache; + try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(MODEL_CACHE_FILE)))) { + final int count = in.readInt(); + for (int i = 0; i < count; i++) { + final String model = in.readUTF(); + final int len = in.readInt(); + final byte[] data = new byte[len]; + in.readFully(data); + cache.put(model, data); + } + Freesia.LOGGER.info("[NPC] Loaded " + cache.size() + " model binary entries from disk"); + } catch (Exception e) { + Freesia.LOGGER.warning("[NPC] Failed to load npc_model_cache.dat: " + e.getMessage()); + } + return cache; + } + + public void saveModelBinaryCache(Map cache) { + try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(MODEL_CACHE_FILE)))) { + out.writeInt(cache.size()); + for (Map.Entry e : cache.entrySet()) { + out.writeUTF(e.getKey()); + out.writeInt(e.getValue().length); + out.write(e.getValue()); + } + } catch (Exception e) { + Freesia.LOGGER.warning("[NPC] Failed to save npc_model_cache.dat: " + e.getMessage()); + } + } + + private void flush() { + try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(ASSIGNMENTS_FILE)))) { + out.writeInt(byId.size()); + for (NpcEntry e : byId.values()) { + out.writeInt(e.npcId()); + out.writeLong(0); + out.writeLong(0); + out.writeUTF(e.modelId()); + } + } catch (Exception e) { + Freesia.LOGGER.warning("[NPC] Failed to save npc_assignments.dat: " + e.getMessage()); + } + } +} diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/VirtualPlayerManager.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/VirtualPlayerManager.java index 992dea7..71eda0f 100644 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/VirtualPlayerManager.java +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/misc/VirtualPlayerManager.java @@ -3,6 +3,7 @@ import com.github.retrooper.packetevents.protocol.nbt.NBTCompound; import com.github.retrooper.packetevents.protocol.nbt.NBTLimiter; import com.github.retrooper.packetevents.protocol.nbt.serializer.DefaultNBTSerializer; +import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.Server; import net.md_5.bungee.api.event.PluginMessageEvent; import net.md_5.bungee.api.plugin.Listener; @@ -13,17 +14,25 @@ import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.util.UUID; import java.util.function.Consumer; public class VirtualPlayerManager implements Listener { private static final String MANAGEMENT_CHANNEL_KEY = "freesia:virtual_player_management"; + private static final String TRACKER_CHANNEL_KEY = "freesia:tracker_sync"; + public static final String CITIZENS_SETSKIN_CHANNEL = "freesia:citizens_setskin"; + public static final String CITIZENS_UUID_RESP_CHANNEL = "freesia:citizens_uuid_resp"; public void init() { Freesia.PROXY_SERVER.registerChannel(MANAGEMENT_CHANNEL_KEY); + Freesia.PROXY_SERVER.registerChannel(CITIZENS_SETSKIN_CHANNEL); + Freesia.PROXY_SERVER.registerChannel(CITIZENS_UUID_RESP_CHANNEL); Freesia.PROXY_SERVER.getPluginManager().registerListener(Freesia.INSTANCE, this); + // Persistence load & preload logic is already in YsmMapperPayloadManager constructor } @EventHandler @@ -33,12 +42,19 @@ public void onPluginMessage(@NotNull PluginMessageEvent event) { return; } - if (!event.getTag().equals(MANAGEMENT_CHANNEL_KEY)) { + final String tag = event.getTag(); + + if (tag.equals(CITIZENS_UUID_RESP_CHANNEL)) { + event.setCancelled(true); + handleCitizensUuidResponse(event.getData()); return; } - event.setCancelled(true); + if (!tag.equals(MANAGEMENT_CHANNEL_KEY)) { + return; + } + event.setCancelled(true); final FriendlyByteBuf packetData = new FriendlyByteBuf(Unpooled.wrappedBuffer(event.getData())); switch (packetData.readByte()) { @@ -52,16 +68,11 @@ public void onPluginMessage(@NotNull PluginMessageEvent event) { response.writeByte(2); response.writeVarInt(eventId); response.writeBoolean(result); - ((Server) event.getSender()).sendData(MANAGEMENT_CHANNEL_KEY, response.getBytes()); }; Freesia.mapperManager.addVirtualPlayer(virtualPlayerUUID, entityId).whenComplete((result, ex) -> { - if (ex != null) { - operationCallback.accept(false); - return; - } - + if (ex != null) { operationCallback.accept(false); return; } operationCallback.accept(result); }); } @@ -75,20 +86,29 @@ public void onPluginMessage(@NotNull PluginMessageEvent event) { response.writeByte(2); response.writeVarInt(eventId); response.writeBoolean(result); - ((Server) event.getSender()).sendData(MANAGEMENT_CHANNEL_KEY, response.getBytes()); }; Freesia.mapperManager.removeVirtualPlayer(virtualPlayerUUID).whenComplete((result, ex) -> { - if (ex != null) { - operationCallback.accept(false); - return; - } - + if (ex != null) { operationCallback.accept(false); return; } operationCallback.accept(result); }); } + // Virtual NPC tracker update — now includes entityId and optionally npcId (if Citizens) + case 3 -> { + final UUID virtualEntityUUID = packetData.readUUID(); + final int npcEntityId = packetData.readVarInt(); + final UUID watcherUUID = packetData.readUUID(); + + Freesia.mapperManager.updateVirtualPlayerEntityId(virtualEntityUUID, npcEntityId); + + final ProxiedPlayer watcher = Freesia.PROXY_SERVER.getPlayer(watcherUUID); + if (watcher != null) { + Freesia.mapperManager.onVirtualPlayerTrackerUpdate(virtualEntityUUID, watcher); + } + } + case 4 -> { final int eventId = packetData.readVarInt(); final UUID virtualPlayerUUID = packetData.readUUID(); @@ -98,7 +118,6 @@ public void onPluginMessage(@NotNull PluginMessageEvent event) { Freesia.PROXY_SERVER.getScheduler().runAsync(Freesia.INSTANCE, () -> { final DefaultNBTSerializer serializer = new DefaultNBTSerializer(); final NBTCompound deserializedTag; - try { deserializedTag = (NBTCompound) serializer.deserializeTag( NBTLimiter.forBuffer(null, Integer.MAX_VALUE), @@ -112,23 +131,81 @@ public void onPluginMessage(@NotNull PluginMessageEvent event) { response.writeByte(2); response.writeVarInt(eventId); response.writeBoolean(result); - ((Server) event.getSender()).sendData(MANAGEMENT_CHANNEL_KEY, response.getBytes()); }; - Freesia.mapperManager.setVirtualPlayerEntityState(virtualPlayerUUID, deserializedTag) - .whenComplete((result, ex) -> { - if (ex != null) { - operationCallback.accept(false); - return; - } - - operationCallback.accept(result); - }); + Freesia.mapperManager.setVirtualPlayerEntityState(virtualPlayerUUID, deserializedTag).whenComplete((result, ex) -> { + if (ex != null) { operationCallback.accept(false); return; } + operationCallback.accept(result); + }); }); } } }); } -} + private void handleCitizensUuidResponse(byte[] data) { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(data)); + final int npcId = buf.readVarInt(); + final UUID npcUUID = buf.readUUID(); + final int npcEntityId = buf.readVarInt(); + final String modelId = buf.readUtf(); + + // Persist assignment (npcId + npcUUID + modelId) so proxy survives restarts + Freesia.mapperManager.npcPersistenceManager.saveAssignment(npcId, npcUUID, modelId); + + Freesia.mapperManager.addVirtualPlayer(npcUUID, npcEntityId).whenComplete((addResult, addEx) -> { + if (addEx != null) { + Freesia.LOGGER.warning("[Citizens] Failed to register NPC " + npcUUID + " as virtual player: " + addEx.getMessage()); + } + + final byte[] cachedBinary = Freesia.mapperManager.getCachedNpcModelBinary(modelId); + if (cachedBinary == null) { + Freesia.LOGGER.warning("[Citizens] No cached binary for model '" + modelId + "'. A real player using this model must be online first, then run /freesia setskin again."); + return; + } + + Freesia.mapperManager.setVirtualPlayerEntityStateBinary(npcUUID, cachedBinary) + .whenComplete((result, ex) -> { + if (ex != null) { + Freesia.LOGGER.warning("[Citizens] Failed to set model for NPC " + npcUUID + ": " + ex.getMessage()); + return; + } + if (Boolean.TRUE.equals(result)) { + Freesia.LOGGER.info("[Citizens] Model '" + modelId + "' applied to NPC " + npcUUID + " (cached binary " + cachedBinary.length + " bytes)"); + } else { + Freesia.LOGGER.warning("[Citizens] setVirtualPlayerEntityStateBinary returned false for NPC " + npcUUID); + } + }); + }); + } + + public void sendSetskinToBackend(net.md_5.bungee.api.connection.ProxiedPlayer carrier, int npcId, String modelId) { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeVarInt(npcId); + buf.writeUtf(modelId); + + carrier.sendData(CITIZENS_SETSKIN_CHANNEL, buf.getBytes()); + } + + public boolean sendSetskinToBackendViaAnyPlayer(int npcId, String modelId) { + final net.md_5.bungee.api.connection.ProxiedPlayer carrier = Freesia.PROXY_SERVER.getPlayers().stream().findFirst().orElse(null); + if (carrier == null) return false; + + sendSetskinToBackend(carrier, npcId, modelId); + return true; + } + + public static byte[] serializeNbt(NBTCompound compound) { + try { + final DefaultNBTSerializer serializer = new DefaultNBTSerializer(); + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + final DataOutputStream dos = new DataOutputStream(bos); + serializer.serializeTag(dos, compound, true); + dos.flush(); + return bos.toByteArray(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/MapperSessionProcessor.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/MapperSessionProcessor.java index 6158332..e975647 100644 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/MapperSessionProcessor.java +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/MapperSessionProcessor.java @@ -30,10 +30,12 @@ public class MapperSessionProcessor implements SessionListener { private final YsmPacketProxy packetProxy; private final YsmMapperPayloadManager mapperPayloadManager; - // Callbacks for packet processing and tracker updates private final MultiThreadedQueue pendingYsmPacketsInbound = new MultiThreadedQueue<>(); private final MultiThreadedQueue pendingTrackerUpdatesTo = new MultiThreadedQueue<>(); private final MultiThreadedQueue pendingYsmPacketsOutbound = new MultiThreadedQueue<>(); + private final MultiThreadedQueue pendingNpcTrackerUpdates = new MultiThreadedQueue<>(); + + public record NpcTrackerUpdate(int entityId, byte[] binary) {} // Controlled by the VarHandles following private volatile Session session; @@ -59,6 +61,10 @@ protected boolean queueTrackerUpdate(UUID target) { return this.pendingTrackerUpdatesTo.offer(target); } + protected boolean queueNpcTrackerUpdate(int entityId, byte[] binary) { + return this.pendingNpcTrackerUpdates.offer(new NpcTrackerUpdate(entityId, binary)); + } + protected void retireTrackerCallbacks() { UUID toSend; while ((toSend = this.pendingTrackerUpdatesTo.pollOrBlockAdds()) != null) { @@ -68,9 +74,12 @@ protected void retireTrackerCallbacks() { continue; } - final ProxiedPlayer targetPlayer = player; + this.packetProxy.sendEntityStateTo(player); + } - this.packetProxy.sendEntityStateTo(targetPlayer); + NpcTrackerUpdate npcUpdate; + while ((npcUpdate = this.pendingNpcTrackerUpdates.pollOrBlockAdds()) != null) { + this.mapperPayloadManager.sendEntityStateToRaw(this.bindPlayer.getUniqueId(), npcUpdate.entityId(), YsmState.ofBinary(npcUpdate.binary())); } } @@ -96,10 +105,6 @@ protected void setKickMasterWhenDisconnect(boolean kickMasterWhenDisconnect) { protected void processPlayerPluginMessage(byte[] packetData) { final Session sessionObject = (Session) SESSION_HANDLE.getVolatile(this); - // This case should never happen because player's ysm packet won't come in - // until we forward the handshake packet from the worker side - // And when the handshake packet is reached, the session was already set before - // see YsmMapperPayloadManager#createMapperSession if (sessionObject == null) { throw new IllegalStateException("Processing plugin message on non-connected mapper"); } @@ -137,10 +142,8 @@ public ProxiedPlayer getBindPlayer() { } protected void onBackendReady() { - // Process incoming packets that we had not ready to process before PendingPacket pendingYsmPacket; - while ((pendingYsmPacket = this.pendingYsmPacketsInbound.pollOrBlockAdds()) != null) { // Destroy(block add - // operations) the queue + while ((pendingYsmPacket = this.pendingYsmPacketsInbound.pollOrBlockAdds()) != null) { this.processInComingYsmPacket(pendingYsmPacket.channel(), pendingYsmPacket.data()); } } @@ -148,7 +151,6 @@ protected void onBackendReady() { @Override public void packetReceived(Session session, Packet packet) { if (packet instanceof ClientboundLoginPacket loginPacket) { - // Notify entity update to notify the tracker update of the player Freesia.mapperManager.updateWorkerPlayerEntityId(this.bindPlayer, loginPacket.getEntityId()); byte[] pendingData; @@ -170,20 +172,14 @@ public void packetReceived(Session session, Packet packet) { } } - // If the packet is of ysm if (channelKey.toString().equals(YsmMapperPayloadManager.YSM_CHANNEL_KEY_ADVENTURE.toString())) { - // Check if we are not ready for the backend side yet(We will block the add - // operations once the backend is ready for the player) final PendingPacket pendingPacket = new PendingPacket(channelKey, packetData); if (!this.pendingYsmPacketsInbound.offer(pendingPacket)) { - // Add is blocked, we'll process it directly this.processInComingYsmPacket(channelKey, packetData); } - // Otherwise, we push it into the callback queue } } - // Reply the fabric mod loader ping checks if (packet instanceof ClientboundPingPacket pingPacket) { session.send(new ServerboundPongPacket(pingPacket.getId())); } @@ -244,34 +240,27 @@ public void disconnected(DisconnectedEvent event) { this.detachFromManager(true, event); } - // Sometimes the callback would not be called when we destroy an non-connected - // mapper, - // so we separated the disconnect logics into here and manual call this in that - // cases protected void detachFromManager(boolean updateSession, @Nullable DisconnectedEvent disconnectedEvent) { Component reason = null; - // Log disconnects if we disconnected it non-manually if (disconnectedEvent != null) { reason = disconnectedEvent.getReason(); - Freesia.LOGGER.info("Mapper session has disconnected for reason(non-deserialized): " + reason); // Log - // disconnected + Freesia.LOGGER.info("Mapper session has disconnected for reason(non-deserialized): " + reason); final Throwable thr = disconnectedEvent.getCause(); if (thr != null) { Freesia.LOGGER.log(java.util.logging.Level.SEVERE, "Mapper session has disconnected for throwable", - thr); // Log errors + thr); } } - // Remove callback this.mapperPayloadManager.onWorkerSessionDisconnect(this, (boolean) KICK_MASTER_HANDLE.getVolatile(this), - reason); // Fire events + reason); if (updateSession) { - SESSION_HANDLE.setVolatile(this, null); // Set session to null to finalize the mapper connection + SESSION_HANDLE.setVolatile(this, null); } } @@ -283,26 +272,19 @@ protected void setSession(Session session) { } public void destroyAndAwaitDisconnected() { - // Prevent multiple disconnect calls if (!DESTROYED_HANDLE.compareAndSet(this, false, true)) { - // Wait for fully disconnected this.waitForDisconnected(); return; } final Session sessionObject = (Session) SESSION_HANDLE.getVolatile(this); - // Destroy the session if (sessionObject != null) { sessionObject.disconnect("DESTROYED"); } else { - // Disconnecting a non initialized session - // Manual call remove callbacks - // Remember: HERE SHOULDN'T BE ANY RACE CONDITION this.detachFromManager(false, null); } - // Wait for fully disconnected this.waitForDisconnected(); } diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/RealPlayerYsmPacketProxyImpl.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/RealPlayerYsmPacketProxyImpl.java index 5216e03..b5f7341 100644 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/RealPlayerYsmPacketProxyImpl.java +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/RealPlayerYsmPacketProxyImpl.java @@ -45,14 +45,25 @@ public ProxyComputeResult processS2C(Key key, ByteBuf copiedPacketData) { try { YsmState state; if (this.ysmVersion.startsWith("2.6")) { - // YSM 2.6.x uses binary format: Strings (ModelPath, ModelName, AnimationName) + - // raw bytes - // We just capture the whole thing as binary for now byte[] binaryData = new byte[mcBuffer.readableBytes()]; mcBuffer.readBytes(binaryData); state = YsmState.ofBinary(binaryData); + if (FreesiaConfig.debug) { + final StringBuilder hex = new StringBuilder(); + for (int i = 0; i < Math.min(binaryData.length, 32); i++) { + hex.append(String.format("%02X ", binaryData[i])); + } + Freesia.LOGGER.info("[DEBUG] YSM2.6 binary state for " + this.player.getName() + + " (len=" + binaryData.length + "): " + hex); + } + try { + final FriendlyByteBuf probe = new FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(binaryData)); + final String modelPath = probe.readUtf(); + if (!modelPath.isEmpty()) { + Freesia.mapperManager.cacheNpcModelBinary(modelPath, binaryData); + } + } catch (Exception ignored) {} } else { - // Legacy NBT format state = YsmState.ofNbt(this.nbtRemapper.readBound(mcBuffer)); } diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/VirtualYsmPacketProxyImpl.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/VirtualYsmPacketProxyImpl.java index c47251c..577efec 100644 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/VirtualYsmPacketProxyImpl.java +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/VirtualYsmPacketProxyImpl.java @@ -3,10 +3,12 @@ import io.netty.buffer.ByteBuf; import com.nguyendevs.freesia.waterfall.Freesia; import net.kyori.adventure.key.Key; +import net.md_5.bungee.api.connection.ProxiedPlayer; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; public class VirtualYsmPacketProxyImpl extends YsmPacketProxyLayer { public VirtualYsmPacketProxyImpl(UUID virtualPlayerUUID) { @@ -15,7 +17,14 @@ public VirtualYsmPacketProxyImpl(UUID virtualPlayerUUID) { @Override public CompletableFuture> fetchTrackerList(UUID observer) { - return Freesia.tracker.getCanSee(observer); + if (Freesia.PROXY_SERVER.getPlayer(observer) != null) { + return Freesia.tracker.getCanSee(observer); + } + final Set allYsm = Freesia.PROXY_SERVER.getPlayers().stream() + .filter(p -> Freesia.mapperManager.isPlayerInstalledYsm(p)) + .map(ProxiedPlayer::getUniqueId) + .collect(Collectors.toSet()); + return CompletableFuture.completedFuture(allYsm); } @Override diff --git a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/YsmMapperPayloadManager.java b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/YsmMapperPayloadManager.java index ec6ba1b..434268a 100644 --- a/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/YsmMapperPayloadManager.java +++ b/Freesia-Waterfall/src/main/java/com/nguyendevs/freesia/waterfall/network/ysm/YsmMapperPayloadManager.java @@ -54,12 +54,23 @@ public class YsmMapperPayloadManager { private final Set ysmInstalledPlayers = Sets.newConcurrentHashSet(); private final Map> worker2PlayerEntityIdCache = Maps.newConcurrentMap(); + private final Map npcModelBinaryCache = Maps.newConcurrentMap(); + public final com.nguyendevs.freesia.waterfall.network.misc.NpcPersistenceManager npcPersistenceManager + = new com.nguyendevs.freesia.waterfall.network.misc.NpcPersistenceManager(); + + public Map getNpcModelBinaryCache() { + return npcModelBinaryCache; + } public YsmMapperPayloadManager(Function packetProxyCreator, Function packetProxyCreatorVirtual) { this.packetProxyCreator = packetProxyCreator; this.packetProxyCreatorVirtual = packetProxyCreatorVirtual; this.backend2Players.put(FreesiaConfig.workerMSessionAddress, 1); // TODO Load balance + // Load persisted model binary cache + this.npcModelBinaryCache.putAll(this.npcPersistenceManager.loadModelBinaryCache()); + // Load persisted NPC assignments. Keyed purely by npcId now. + this.npcPersistenceManager.load(); } @Nullable @@ -163,6 +174,72 @@ public CompletableFuture setVirtualPlayerEntityState(UUID playerUUID, N return callback; } + public CompletableFuture setVirtualPlayerEntityStateBinary(UUID playerUUID, byte[] binary) { + final YsmPacketProxy virtualProxy; + + synchronized (this.virtualProxies) { + virtualProxy = this.virtualProxies.get(playerUUID); + } + + if (virtualProxy == null) { + return CompletableFuture.completedFuture(false); + } + + virtualProxy.setEntityDataRaw(YsmState.ofBinary(binary)); + virtualProxy.notifyFullTrackerUpdates(); + return CompletableFuture.completedFuture(true); + } + + public void cacheNpcModelBinary(String modelPath, byte[] binary) { + final String key = modelPath.toLowerCase(); + if (!npcModelBinaryCache.containsKey(key)) { + npcModelBinaryCache.put(key, binary); + Freesia.LOGGER.info("[YSM] Cached model binary for '" + modelPath + "' (" + binary.length + " bytes)"); + npcPersistenceManager.saveModelBinaryCache(npcModelBinaryCache); + } + } + + public byte[] getCachedNpcModelBinary(String modelId) { + byte[] result = npcModelBinaryCache.get(modelId); + if (result != null) return result; + result = npcModelBinaryCache.get(modelId.toLowerCase()); + if (result != null) return result; + result = npcModelBinaryCache.get(modelId + ".ysm"); + if (result != null) return result; + return npcModelBinaryCache.get(modelId.toLowerCase() + ".ysm"); + } + + /** + * Invariant track sync using npcId to apply cached models directly to entityId. + */ + public void handleNpcTrackSync(ProxiedPlayer watcher, int npcId, int entityId) { + com.nguyendevs.freesia.waterfall.network.misc.NpcPersistenceManager.NpcEntry entry = npcPersistenceManager.getIdAssignments().get(npcId); + if (entry != null) { + byte[] binary = getCachedNpcModelBinary(entry.modelId()); + if (binary != null) { + final MapperSessionProcessor mapperSession = this.mapperSessions.get(watcher); + if (mapperSession != null) { + if (this.isPlayerInstalledYsm(watcher)) { + this.sendEntityStateToRaw(watcher.getUniqueId(), entityId, YsmState.ofBinary(binary)); + } else { + mapperSession.queueNpcTrackerUpdate(entityId, binary); + } + } + } else { + Freesia.LOGGER.warning("[NPC] Binary missing for model '" + entry.modelId() + "' on NPC ID " + npcId); + } + } + } + + public void updateVirtualPlayerEntityId(java.util.UUID playerUUID, int entityId) { + synchronized (this.virtualProxies) { + final YsmPacketProxy proxy = this.virtualProxies.get(playerUUID); + if (proxy != null) { + proxy.setPlayerEntityId(entityId); + } + } + } + public CompletableFuture addVirtualPlayer(UUID playerUUID, int playerEntityId) { if (Freesia.PROXY_SERVER.getPlayer(playerUUID) != null) { return CompletableFuture.completedFuture(false); @@ -170,7 +247,9 @@ public CompletableFuture addVirtualPlayer(UUID playerUUID, int playerEn synchronized (this.virtualProxies) { if (this.virtualProxies.containsKey(playerUUID)) { - return CompletableFuture.completedFuture(false); + final YsmPacketProxy existing = this.virtualProxies.get(playerUUID); + existing.setPlayerEntityId(playerEntityId); + return CompletableFuture.completedFuture(true); } final CompletableFuture callback = new CompletableFuture<>(); @@ -178,7 +257,6 @@ public CompletableFuture addVirtualPlayer(UUID playerUUID, int playerEn final YsmPacketProxy createdVirtualProxy = this.virtualProxies.computeIfAbsent(playerUUID, this.packetProxyCreatorVirtual); - // Load from data storage Freesia.virtualPlayerDataStorageManager.loadPlayerData(playerUUID).whenComplete((data, ex) -> { if (ex != null) { callback.completeExceptionally(ex); @@ -201,8 +279,10 @@ public CompletableFuture addVirtualPlayer(UUID playerUUID, int playerEn createdVirtualProxy.setPlayerEntityId(playerEntityId); createdVirtualProxy.setPlayerWorkerEntityId(0); } catch (Exception ex1) { - callback.completeExceptionally(ex1); - return; + Freesia.LOGGER.log(java.util.logging.Level.WARNING, + "[addVirtualPlayer] Corrupt stored data for " + playerUUID + ", starting clean: " + ex1.getMessage()); + createdVirtualProxy.setPlayerEntityId(playerEntityId); + createdVirtualProxy.setPlayerWorkerEntityId(0); } callback.complete(true); @@ -379,6 +459,45 @@ public void onBackendReady(ProxiedPlayer player) { mapperSession.onBackendReady(); } + public void sendEntityStateToRaw(@NotNull UUID receiverUUID, int entityId, YsmState data) { + final ProxiedPlayer receiver = Freesia.PROXY_SERVER.getPlayer(receiverUUID); + + if (receiver == null) { + return; + } + + final Object targetChannel = com.github.retrooper.packetevents.PacketEvents.getAPI().getProtocolManager().getChannel(receiver.getUniqueId()); + + if (targetChannel == null) { + return; + } + + final com.github.retrooper.packetevents.protocol.player.ClientVersion clientVersion = com.github.retrooper.packetevents.PacketEvents.getAPI().getProtocolManager() + .getClientVersion(targetChannel); + + final int targetProtocolVer = clientVersion.getProtocolVersion(); + final com.nguyendevs.freesia.waterfall.utils.FriendlyByteBuf wrappedPacketData = new com.nguyendevs.freesia.waterfall.utils.FriendlyByteBuf(io.netty.buffer.Unpooled.buffer()); + + wrappedPacketData.writeByte(4); + wrappedPacketData.writeVarInt(entityId); + + if (data.isBinary()) { + wrappedPacketData.writeBytes(data.getBinary()); + } else { + try { + com.nguyendevs.freesia.waterfall.network.mc.NbtRemapper remapper = new com.nguyendevs.freesia.waterfall.network.mc.impl.StandardNbtRemapperImpl(); + wrappedPacketData.writeBytes(remapper.shouldRemap(targetProtocolVer) + ? remapper.remapToMasterVer(data.getNbt()) + : remapper.remapToWorkerVer(data.getNbt())); + } catch (java.io.IOException e) { + Freesia.LOGGER.warning("Failed to remap NBT for raw entity state: " + e.getMessage()); + return; + } + } + + receiver.sendData(YSM_CHANNEL_KEY_VELOCITY, wrappedPacketData.getBytes()); + } + public static class SavedProxyState { public final YsmState state; public final String version; @@ -488,13 +607,21 @@ public void createMapperSession(@NotNull ProxiedPlayer player, @NotNull InetSock public void onVirtualPlayerTrackerUpdate(UUID owner, ProxiedPlayer watcher) { final YsmPacketProxy virtualProxy = this.virtualProxies.get(owner); - // There is no specified virtual proxy for the owner if (virtualProxy == null) { return; } + final MapperSessionProcessor mapperSession = this.mapperSessions.get(watcher); + + if (mapperSession == null) { + return; + } + if (this.isPlayerInstalledYsm(watcher)) { - virtualProxy.sendEntityStateTo(watcher); + // Queue it properly through the mapper session so we don't send to unprepared clients + if (!mapperSession.queueTrackerUpdate(owner)) { + virtualProxy.sendEntityStateTo(watcher); + } } } diff --git a/Freesia-Waterfall/src/main/resources/lang/en_US.lang b/Freesia-Waterfall/src/main/resources/lang/en_US.lang index 5cbfec2..2445afc 100644 --- a/Freesia-Waterfall/src/main/resources/lang/en_US.lang +++ b/Freesia-Waterfall/src/main/resources/lang/en_US.lang @@ -1,7 +1,10 @@ -freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm backend has been disconnected, reason: -freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> There is not a valid connection to ysm backend! If you still meet this issue. please report to the administrators -freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Cannot found specified worker -freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] -freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Handshake time outed, please ensure you have installed the corrected version of ysm -freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: -freesia.list_player_command_body=<#cdd6f4> \ No newline at end of file +freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm backend has been disconnected, reason: +freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> There is not a valid connection to ysm backend! If you still meet this issue. please report to the administrators +freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Cannot found specified worker +freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] +freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Handshake time outed, please ensure you have installed the corrected version of ysm +freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: +freesia.list_player_command_body=<#cdd6f4> +freesia.setskin.success=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#a6e3a1> Sent setskin request for NPC with model . Check backend logs for result. +freesia.setskin.no_players=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> No online players to relay the command to Backend. +freesia.setskin.citizens_disabled=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Citizens is not enabled on the Backend server. \ No newline at end of file diff --git a/Freesia-Waterfall/src/main/resources/lang/vi_VN.lang b/Freesia-Waterfall/src/main/resources/lang/vi_VN.lang index c9a0e25..98ec4cb 100644 --- a/Freesia-Waterfall/src/main/resources/lang/vi_VN.lang +++ b/Freesia-Waterfall/src/main/resources/lang/vi_VN.lang @@ -1,7 +1,10 @@ -freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Backend Ysm đã ngắt kết nối, lý do: -freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không có kết nối hợp lệ tới backend Ysm! Nếu bạn vẫn gặp sự cố này, vui lòng báo cáo cho quản trị viên -freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không tìm thấy worker được chỉ định -freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] -freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Bắt tay kết nối đã hết thời gian chờ, vui lòng đảm bảo bạn đã cài đúng phiên bản Ysm -freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Danh sách người chơi Ysm: -freesia.list_player_command_body=<#cdd6f4> \ No newline at end of file +freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Backend Ysm đã ngắt kết nối, lý do: +freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không có kết nối hợp lệ tới backend Ysm! Nếu bạn vẫn gặp sự cố này, vui lòng báo cáo cho quản trị viên +freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không tìm thấy worker được chỉ định +freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] +freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Bắt tay kết nối đã hết thời gian chờ, vui lòng đảm bảo bạn đã cài đúng phiên bản Ysm +freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Danh sách người chơi Ysm: +freesia.list_player_command_body=<#cdd6f4> +freesia.setskin.success=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#a6e3a1> Đã gửi yêu cầu setskin cho NPC với model . Kiểm tra log backend để xem kết quả. +freesia.setskin.no_players=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Không có người chơi nào online để chuyển tiếp lệnh tới Backend. +freesia.setskin.citizens_disabled=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Citizens chưa được bật trên server Backend. \ No newline at end of file diff --git a/Freesia-Waterfall/src/main/resources/lang/zh_CN.lang b/Freesia-Waterfall/src/main/resources/lang/zh_CN.lang index 3fcd8f1..5b30857 100644 --- a/Freesia-Waterfall/src/main/resources/lang/zh_CN.lang +++ b/Freesia-Waterfall/src/main/resources/lang/zh_CN.lang @@ -1,7 +1,10 @@ -freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm后端worker断开了连接,原因: -freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 你没有连接到Ysm后端worker.如果反复遇到该问题,请汇报管理员! -freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 找不到该worker! -freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] -freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm握手超时,请确认您安装了正确版本的Ysm -freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: -freesia.list_player_command_body=<#cdd6f4> \ No newline at end of file +freesia.backend.disconnected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm后端worker断开了连接,原因: +freesia.backend.not_connected=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 你没有连接到Ysm后端worker.如果反复遇到该问题,请汇报管理员! +freesia.worker_command.worker_not_found=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 找不到该worker! +freesia.worker_command.command_feedback=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] [<#cdd6f4>] +freesia.mod_handshake_time_outed=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Ysm握手超时,请确认您安装了正确版本的Ysm +freesia.list_player_command_header=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>] Ysm Player List: +freesia.list_player_command_body=<#cdd6f4> +freesia.setskin.success=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#a6e3a1> 已发送NPC 的setskin请求,模型为 。请查看后端日志确认结果。 +freesia.setskin.no_players=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> 没有在线玩家可用于转发命令到后端。 +freesia.setskin.citizens_disabled=<#cdd6f4>[<#89dceb>Freesia<#cdd6f4>]<#f38ba8> Citizens未在后端服务器上启用。 \ No newline at end of file diff --git a/README.md b/README.md index 18bfa5d..e5d730b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Freesia is a hybrid between **MultiPaper** and **Geyser**. It uses MultiPaper's ### Quick Start (Recommended for Testing) -1. Download `template server` from the [latest release]([https://github.com/YesSteveModel/Freesia/releases](https://github.com/NguyenDevs/YSM_SERVER)) +1. Download `template server` from the [latest release](https://github.com/NguyenDevs/YSM_SERVER) 2. After download completes, run `start.bat` in the created folder ### Production Setup @@ -61,7 +61,9 @@ Freesia is a hybrid between **MultiPaper** and **Geyser**. It uses MultiPaper's ## Configuration -### Proxy — `plugin/Freesia/freesia_config.toml` +> 🔒 **Security Warning:** Never expose ports `19200` (master) or `19199` (msession) to the public internet! They are designed strictly for internal communication. + +### Proxy — `plugins/Freesia/freesia_config.toml` ```toml [functions] @@ -79,6 +81,21 @@ worker_msession_ip = "localhost" worker_msession_port = 19199 ``` +### Proxy Security — `plugins/Freesia/freesia_security.toml` +*Freesia comes with a hardened IP filtering and TLS encapsulation system to strictly authorize traffic between Master and Worker Nodes.* + +```toml +[security] +enable_tls = true # Set to false to disable network encryption +use_self_signed = true # Automatically generates a TLS certificate on boot +cert_path = "cert.pem" # Required if use_self_signed = false +key_path = "key.pem" + +[firewall] +enable_ip_filter = true # Enables strict IP Whitelisting +allowed_worker_ips = ["127.0.0.1", "192.168.1.5"] # Only IPs listed here are allowed to connect +``` + ### Worker — `config/freesia_config.toml` ```toml @@ -87,6 +104,11 @@ worker_master_ip = "localhost" worker_master_port = 19200 controller_reconnect_interval = 1 player_data_cache_invalidate_interval_seconds = 30 + +[security] +enable_tls = true # Must match proxy's security configuration +trust_all = true # Trusts any master certificates (useful for self-signed proxy setups) +trust_cert_path = "truststore.pem" ``` ### Worker — `server.properties` @@ -96,18 +118,18 @@ server-ip=127.0.0.1 server-port=19199 # Must match worker_msession_port in Velocity config ``` -> 🔒 **Security:** Never expose ports `19200` (master) or `19199` (msession) to the public internet. - --- ## Features - Full Yes Steve Model support on Velocity/Waterfall / plugin networks +- **Native TLS Encryption:** Data passing between Proxy and Workers is actively encrypted natively on Java 21+ environments. +- **Zero-Latency Processing:** Internal socket buffering is disabled (Nagle's Algorithm bypassed) ensuring rapid-fire YSM sync processing without latency spikes. - Asynchronous model synchronization and data generation - Worker nodes optimized exclusively for YSM packet handling - Optional kick for players without YSM mod installed - Multi-language kick messages (`zh_CN` / `en_US` / `vi_VN`) -- Secure design — control and msession ports are internal only +- Secure design — Configurable Built-in **IP Whitelist Firewall** against external intrusions. --- diff --git a/settings.gradle b/settings.gradle index 55af646..5a8059b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,4 +18,5 @@ include("Freesia-Velocity") include("Freesia-Waterfall") include("Freesia-Backend") include("Freesia-Common") -include("Freesia-Worker") \ No newline at end of file +include("Freesia-Worker") +include("Freesia-NPC") \ No newline at end of file