Full tested ChatSync Feature

This commit is contained in:
mlus 2025-06-05 12:43:41 +08:00
parent 74451eecff
commit 0562b01138
8 changed files with 230 additions and 136 deletions

View File

@ -34,7 +34,7 @@ mod_name=PlayerSync
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=GPL-3.0 license
# The mod version. See https://semver.org/
mod_version=2.1.0
mod_version=2.1.2
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -0,0 +1,28 @@
package vip.fubuki.playersync;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import vip.fubuki.playersync.sync.chat.ChatSyncClient;
@Mod.EventBusSubscriber()
public class CommandInit {
@SubscribeEvent
public static void registerCommand(RegisterCommandsEvent event){
CommandDispatcher<CommandSourceStack> dispatcher=event.getDispatcher();
dispatcher.register(Commands.literal("playersync")
.requires(cs->cs.hasPermission(2))
.then(Commands.literal("reconnect")
.executes(context -> {
new ChatSyncClient().run();
// context.getSource().sendSuccess(()->MutableComponent.create(new TranslatableContents("playersync.command.reconnect")),true);
return 0;
}
))
);
}
}

View File

@ -1,140 +1,38 @@
package vip.fubuki.playersync.sync;
import net.minecraft.network.chat.Component;
import net.minecraft.server.players.PlayerList;
import net.neoforged.bus.api.SubscribeEvent;
import com.mojang.logging.LogUtils;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import org.slf4j.Logger;
import vip.fubuki.playersync.config.JdbcConfig;
import vip.fubuki.playersync.sync.chat.ChatSyncClient;
import vip.fubuki.playersync.sync.chat.ChatSyncServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Objects;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import com.mojang.logging.LogUtils;
public class ChatSync {
private static final Logger LOGGER = LogUtils.getLogger();
static PlayerList playerList;
static ServerSocket serverSocket;
static Socket clientSocket;
static Set<Socket> SocketList = ConcurrentHashMap.newKeySet();
static ExecutorService executorService = Executors.newCachedThreadPool();
public static final Logger LOGGER = LogUtils.getLogger();
public static void register(){
if(JdbcConfig.IS_CHAT_SERVER.get()) {
LOGGER.info("Launching chat server thread.");
new Thread(ChatSync::ServerSocket).start();
}
ClientSocket();
NeoForge.EVENT_BUS.register(ChatSync.class);
}
private static void ServerSocket() {
try {
LOGGER.info("Trying to setup chat server at port " + JdbcConfig.CHAT_SERVER_PORT.get());
serverSocket = new ServerSocket(JdbcConfig.CHAT_SERVER_PORT.get());
while (true) {
Socket newSocket = serverSocket.accept();
SocketList.add(newSocket);
executorService.submit(() -> handleClient(newSocket));
}
} catch (IOException e) {
LOGGER.error("Unable to start chat server");
e.printStackTrace();
} finally {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void handleClient(Socket socket) {
try (InputStream inputStream = socket.getInputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
String message = new String(buffer, 0, bytesRead);
broadcastMessage(socket, message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
SocketList.remove(socket);
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void broadcastMessage(Socket sender, String message) {
for (Socket socket : SocketList) {
if (!socket.equals(sender)) {
new Thread(()->{
ChatSyncServer chatSyncServer = new ChatSyncServer();
try {
OutputStream outputStream = socket.getOutputStream();
outputStream.write(message.getBytes());
chatSyncServer.run();
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("Unable to start chat server", e);
}
}
}).start();
}
}
private static void ClientSocket() {
try {
new Thread(()->{
LOGGER.info("Trying to connect to chat server "
+ JdbcConfig.CHAT_SERVER_IP.get()
+ ":"
+ JdbcConfig.CHAT_SERVER_PORT.get());
clientSocket = new Socket(JdbcConfig.CHAT_SERVER_IP.get(), JdbcConfig.CHAT_SERVER_PORT.get());
Scanner scanner = new Scanner(clientSocket.getInputStream());
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
Component textComponents = Component.nullToEmpty(line);
playerList.broadcastSystemMessage(textComponents,true);
}
} catch (IOException e) {
e.printStackTrace();
reconnectClient();
}
}
private static void reconnectClient() {
LOGGER.warn("TODO: implement reconnectClient()");
//TODO
}
@SubscribeEvent
public static void onPlayerChat(net.neoforged.neoforge.event.ServerChatEvent event) throws IOException {
String message= event.getUsername()+":"+event.getMessage();
OutputStream outputStream = clientSocket.getOutputStream();
outputStream.write(message.getBytes());
}
@SubscribeEvent
public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event){
playerList= Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
@SubscribeEvent
public static void onPlayerLeave(PlayerEvent.PlayerLoggedOutEvent event){
playerList= Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
ChatSyncClient chatSyncClient = new ChatSyncClient();
chatSyncClient.run();
}).start();
NeoForge.EVENT_BUS.register(ChatSyncClient.class);
}
}

View File

@ -1,13 +1,9 @@
package vip.fubuki.playersync.sync;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.StringTag;
import net.minecraft.nbt.Tag;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.*;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.resources.ResourceLocation;
@ -29,7 +25,6 @@ import net.neoforged.neoforge.event.OnDatapackSyncEvent;
import net.neoforged.neoforge.event.TickEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.server.ServerStoppedEvent;
import net.minecraft.core.registries.BuiltInRegistries;
import net.neoforged.neoforge.server.ServerLifecycleHooks;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
@ -43,11 +38,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -184,10 +175,8 @@ public class VanillaSync {
// Restore basic attributes
serverPlayer.setHealth(rs2.getInt("health"));
serverPlayer.getFoodData().setFoodLevel(rs2.getInt("food_level"));
serverPlayer.totalExperience = 0;
serverPlayer.experienceLevel = 0;
serverPlayer.experienceProgress = 0;
serverPlayer.giveExperiencePoints(rs2.getInt("xp"));
setXpForPlayer(serverPlayer, rs2.getInt("xp"));
serverPlayer.setScore(rs2.getInt("score"));
// Restore left-hand item
@ -635,4 +624,52 @@ public class VanillaSync {
}
}
}
private static void setXpForPlayer(ServerPlayer serverPlayer, int databaseXp) {
// Don't use giveExperience() as it has several side-effects:
// triggers an event, sends network packets, increases the score, ...
serverPlayer.totalExperience = databaseXp;
serverPlayer.experienceLevel = 0;
serverPlayer.experienceProgress = 0;
int xpForLevel;
while (databaseXp >= (xpForLevel = serverPlayer.getXpNeededForNextLevel())) {
databaseXp -= xpForLevel;
serverPlayer.experienceLevel++;
}
serverPlayer.experienceProgress = serverPlayer.experienceLevel > 0
? (float) databaseXp / serverPlayer.getXpNeededForNextLevel()
: 0f;
PlayerSync.LOGGER.debug("Giving player "
+ serverPlayer.experienceLevel + " levels and "
+ serverPlayer.experienceProgress * 100 + "% experience progress, calculated from "
+ serverPlayer.totalExperience + " XP.");
}
private static int getTotalExperience(final Player player) {
int level = player.experienceLevel;
int totalXp = 0;
// Calculate total XP for completed levels
if (level > 30) {
totalXp = (int) (4.5 * Math.pow(level, 2) - 162.5 * level + 2220);
} else if (level > 15) {
totalXp = (int) (2.5 * Math.pow(level, 2) - 40.5 * level + 360);
} else {
totalXp = level * level + 6 * level;
}
// Add partial level progress
totalXp += Math.round(player.getXpNeededForNextLevel() * player.experienceProgress);
PlayerSync.LOGGER.debug("Experience calcuation for "
+ player.experienceLevel + " levels and "
+ player.experienceProgress * 100 + "% experience progress yields "
+ totalXp + " XP.");
return totalXp;
}
}

View File

@ -0,0 +1,66 @@
package vip.fubuki.playersync.sync.chat;
import net.minecraft.network.chat.Component;
import net.minecraft.server.players.PlayerList;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.ServerChatEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
import vip.fubuki.playersync.sync.ChatSync;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Objects;
public class ChatSyncClient {
static PlayerList playerList;
static Socket clientSocket;
static PrintWriter out;
public void run() {
try {
clientSocket = new Socket(JdbcConfig.CHAT_SERVER_IP.get(), JdbcConfig.CHAT_SERVER_PORT.get());
out = new PrintWriter(clientSocket.getOutputStream(),true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String serverMessage;
while ((serverMessage = in.readLine()) != null) {
PlayerSync.LOGGER.info("Received message from chat server: " + serverMessage);
Component textComponents = Component.nullToEmpty(serverMessage);
playerList.broadcastSystemMessage(textComponents,false);
}
} catch (IOException e) {
e.printStackTrace();
reconnectClient();
}
}
private void reconnectClient() {
ChatSync.LOGGER.warn("TODO: implement reconnectClient()");
//TODO
}
@SubscribeEvent
public static void onPlayerChat(ServerChatEvent event) {
String message= "<"+event.getUsername()+"> "+event.getMessage().getString();
if (out != null) {
out.println(message);
}
}
@SubscribeEvent
public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event){
playerList = Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
@SubscribeEvent
public static void onPlayerLeave(PlayerEvent.PlayerLoggedOutEvent event){
playerList = Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
}

View File

@ -0,0 +1,63 @@
package vip.fubuki.playersync.sync.chat;
import vip.fubuki.playersync.config.JdbcConfig;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ChatSyncServer {
static ServerSocket serverSocket;
static final Set<Socket> SocketList = ConcurrentHashMap.newKeySet();
static final ExecutorService executorService = Executors.newCachedThreadPool();
public void run() throws IOException {
serverSocket = new ServerSocket(JdbcConfig.CHAT_SERVER_PORT.get());
while (!Thread.currentThread().isInterrupted()) {
Socket newSocket = serverSocket.accept();
SocketList.add(newSocket);
executorService.submit(() -> handleClient(newSocket));
}
serverSocket.close();
}
private void handleClient(Socket socket) {
try (InputStream inputStream = socket.getInputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
String message = new String(buffer, 0, bytesRead);
broadcastMessage(socket, message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
SocketList.remove(socket);
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void broadcastMessage(Socket sender, String message) {
for (Socket socket : SocketList) {
if (!socket.equals(sender)) {
try {
OutputStream outputStream = socket.getOutputStream();
outputStream.write(message.getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

View File

@ -1,5 +1,5 @@
{
"playersync.item_placeholder_description": "Item is unknown on this server. This can either\nbe a modded item, an added, or a removed vanilla\nitem.\nThis voucher will automatically be replaced with\nthe corresponding item when joining a server\nwhere the item is known.","playersync.placeholder_titel_override": "Item Voucher",
"playersync.item_placeholder_description": "Item is unknown on this server. This can either\nbe a modded item, an added, or a removed vanilla\nitem.\nThis voucher will automatically be replaced with\nthe corresponding item when joining a server\nwhere the item is known.",
"playersync.item_placeholder_title": "Item Voucher",
"playersync.already_online": "You can't join more than one synchronization server at the same time."
}

View File

@ -1,3 +1,5 @@
{
"playersync.item_placeholder_description": "物品在此服务器未知。这可能是一个模组物品,或是不同版本的原版物品。\n这张券将会在加入可识别此物品的服务器后自动替换为对应物品。",
"playersync.item_placeholder_title": "物品券",
"playersync.already_online": "你不能同时加入多个同步的服务器。"
}
}