package dev.plutorocks; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class PlutoBridge implements ClientModInitializer { public static final String MOD_ID = "PlutoBridge"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); private static final Gson GSON = new Gson(); private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("bridge.json"); public static final int MAX_NICK_LEN = 15; public static final int MAX_MESSAGE_LEN = 400; // must match server public static String bridgeHost = "bridge.plutorocks.dev"; public static String bridgeToken = ""; public static String bridgeNick; // will be set from config/session public static BridgeClient BRIDGE_CLIENT; // false = normal chat default, true = bridge default public static boolean bridgeChatDefault = false; @Override public void onInitializeClient() { loadConfig(); // Auto-connect if we already have a token if (bridgeToken != null && !bridgeToken.isEmpty()) { BRIDGE_CLIENT = new BridgeClient(bridgeHost, bridgeToken, bridgeNick); BRIDGE_CLIENT.connect(); } ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { dispatcher.register( literal("bridge") .then(literal("connect").executes(this::handleConnect)) .then(literal("disconnect").executes(this::handleDisconnect)) .then(literal("status").executes(this::handleStatus)) .then(literal("host") .then(argument("host", StringArgumentType.word()) .executes(this::handleSetHost))) .then(literal("token") .then(argument("token", StringArgumentType.word()) .executes(this::handleSetToken))) .then(literal("nick") .then(argument("nick", StringArgumentType.word()) .executes(this::handleSetNick))) .then(literal("toggle") .executes(this::handleToggle)) ); }); LOGGER.info("Bridge client loaded!"); } private static void loadConfig() { if (!Files.exists(CONFIG_PATH)) { applyDefaultsFromSession(); saveConfig(); return; } try (Reader reader = Files.newBufferedReader(CONFIG_PATH)) { Config cfg = GSON.fromJson(reader, Config.class); if (cfg == null) { applyDefaultsFromSession(); saveConfig(); return; } if (cfg.host != null && !cfg.host.isEmpty()) { bridgeHost = cfg.host; } else { bridgeHost = "bridge.plutorocks.dev"; } if (cfg.token != null && !cfg.token.isEmpty()) { bridgeToken = cfg.token; } else { bridgeToken = ""; } if (cfg.nick != null && !cfg.nick.isEmpty()) { bridgeNick = cfg.nick; } else { bridgeNick = defaultNickFromSession(); } } catch (IOException | JsonSyntaxException e) { LOGGER.warn("Failed to read bridge config, using defaults", e); applyDefaultsFromSession(); } } private static void saveConfig() { Config cfg = new Config(); cfg.host = bridgeHost; cfg.token = bridgeToken; cfg.nick = bridgeNick; try (Writer writer = Files.newBufferedWriter(CONFIG_PATH)) { GSON.toJson(cfg, writer); } catch (IOException e) { LOGGER.warn("Failed to save bridge config", e); } } private static void applyDefaultsFromSession() { bridgeHost = "bridge.plutorocks.dev"; bridgeToken = ""; bridgeNick = defaultNickFromSession(); } private static String defaultNickFromSession() { MinecraftClient client = MinecraftClient.getInstance(); if (client != null && client.getSession() != null) { String name = client.getSession().getUsername(); if (name != null && !name.isEmpty()) { return name; } } return "Player"; } private int handleConnect(CommandContext ctx) { if (bridgeToken == null || bridgeToken.isEmpty()) { sendCmdFeedback(ctx, "Token is empty. Set it with /bridge token first."); return 1; } if (BRIDGE_CLIENT != null && BRIDGE_CLIENT.isConnected()) { sendCmdFeedback(ctx, "Already connected."); return 1; } if (BRIDGE_CLIENT != null) { BRIDGE_CLIENT.disconnect(); } BRIDGE_CLIENT = new BridgeClient(bridgeHost, bridgeToken, bridgeNick); sendCmdFeedback(ctx, "Connecting to " + bridgeHost + " as " + bridgeNick + "..."); BRIDGE_CLIENT.connect(); return 1; } private int handleDisconnect(CommandContext ctx) { if (BRIDGE_CLIENT == null || !BRIDGE_CLIENT.isConnected()) { sendCmdFeedback(ctx, "Already disconnected."); return 1; } BRIDGE_CLIENT.disconnect(); sendCmdFeedback(ctx, "Disconnected."); return 1; } private int handleStatus(CommandContext ctx) { String state = (BRIDGE_CLIENT != null && BRIDGE_CLIENT.isConnected()) ? "Connected" : "Not connected"; sendCmdFeedback(ctx, state + ". Host=" + bridgeHost + " Nick=" + bridgeNick + " Mode=" + (bridgeChatDefault ? "BRIDGE" : "CHAT")); return 1; } private int handleToggle(CommandContext ctx) { bridgeChatDefault = !bridgeChatDefault; if (bridgeChatDefault) { sendCmdFeedback(ctx, "Bridge mode ON. Messages go to the bridge by default; prefix with ! to send to normal chat."); } else { sendCmdFeedback(ctx, "Bridge mode OFF. Messages go to normal chat by default; prefix with ! to send to the bridge."); } return 1; } private int handleSetHost(CommandContext ctx) { bridgeHost = StringArgumentType.getString(ctx, "host"); saveConfig(); sendCmdFeedback(ctx, "Host set to " + bridgeHost + ". Use /bridge connect to apply."); return 1; } private int handleSetToken(CommandContext ctx) { bridgeToken = StringArgumentType.getString(ctx, "token"); saveConfig(); sendCmdFeedback(ctx, "Token set. Use /bridge connect to apply."); return 1; } private int handleSetNick(CommandContext ctx) { String requested = StringArgumentType.getString(ctx, "nick"); int nickLength = requested.codePointCount(0, requested.length()); if (nickLength > MAX_NICK_LEN) { sendCmdFeedback(ctx, "Nick too long (max " + MAX_NICK_LEN + " characters)."); return 1; } bridgeNick = requested; saveConfig(); if (PlutoBridge.BRIDGE_CLIENT != null && PlutoBridge.BRIDGE_CLIENT.isConnected()) { PlutoBridge.BRIDGE_CLIENT.updateNick(bridgeNick); sendCmdFeedback(ctx, "Nick set to " + bridgeNick + " (updated live)."); } else { sendCmdFeedback(ctx, "Nick set to " + bridgeNick + ". Use /bridge connect to apply."); } return 1; } private void sendCmdFeedback(CommandContext ctx, String grayMessage) { MutableText prefix = Text.literal("[Bridge] ").formatted(Formatting.AQUA); MutableText body = Text.literal(grayMessage).formatted(Formatting.GRAY); ctx.getSource().sendFeedback(prefix.append(body)); } private static class Config { String host; String token; String nick; } }