summaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/dev/plutorocks/IrcClient.java199
-rw-r--r--src/main/java/dev/plutorocks/MinecraftIRC.java233
-rw-r--r--src/main/java/dev/plutorocks/mixin/ChatScreenMixin.java61
-rw-r--r--src/main/java/dev/plutorocks/mixin/ExampleMixin.java15
4 files changed, 483 insertions, 25 deletions
diff --git a/src/main/java/dev/plutorocks/IrcClient.java b/src/main/java/dev/plutorocks/IrcClient.java
new file mode 100644
index 0000000..f3abd40
--- /dev/null
+++ b/src/main/java/dev/plutorocks/IrcClient.java
@@ -0,0 +1,199 @@
+package dev.plutorocks;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.io.*;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+public class IrcClient {
+
+ private final String host;
+ private final int port;
+ private final String channel;
+ private final String nick;
+
+ private volatile boolean running = false;
+ private volatile boolean connected = false;
+
+ private Socket socket;
+ private BufferedWriter writer;
+
+ public IrcClient(String host, int port, String channel, String nick) {
+ this.host = host;
+ this.port = port;
+ this.channel = channel.startsWith("#") ? channel : "#" + channel;
+ this.nick = nick;
+ }
+
+ /**
+ * start the IRC thread.
+ */
+ public void connect() {
+ if (running) return;
+ running = true;
+
+ Thread thread = new Thread(this::runLoop, "MinecraftIRC-Thread");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public void disconnect() {
+ running = false;
+ try {
+ if (socket != null && !socket.isClosed()) {
+ socket.close();
+ }
+ } catch (IOException ignored) {}
+ connected = false;
+ }
+
+ private void runLoop() {
+ try (Socket sock = new Socket(host, port);
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8));
+ BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8))) {
+
+ this.socket = sock;
+ this.writer = writer;
+ this.connected = true;
+
+ sendRaw("NICK " + nick);
+ sendRaw("USER " + nick + " 0 * :" + nick);
+
+ sendRaw("JOIN " + channel);
+
+ sendClientChat(
+ Text.literal("[IRC] ").formatted(Formatting.AQUA)
+ .append(Text.literal("Connected to " + host + " " + channel + " as " + nick)
+ .formatted(Formatting.GRAY))
+ );
+
+ String line;
+ while (running && (line = reader.readLine()) != null) {
+ handleLine(line);
+ }
+
+ if (running) {
+ sendClientChat(
+ Text.literal("[IRC] ").formatted(Formatting.AQUA)
+ .append(Text.literal("Disconnected from server. Use /irc connect to reconnect.")
+ .formatted(Formatting.GRAY))
+ );
+ }
+
+ } catch (IOException e) {
+ sendClientChat(
+ Text.literal("[IRC] ").formatted(Formatting.AQUA)
+ .append(Text.literal("Connection error: " + e.getMessage()
+ + " (use /irc connect to try again)")
+ .formatted(Formatting.GRAY))
+ );
+ } finally {
+ connected = false;
+ running = false;
+ }
+ }
+
+ private void handleLine(String line) {
+ // respond to server PINGs
+ if (line.startsWith("PING")) {
+ String payload = line.length() > 5 ? line.substring(5) : "";
+ sendRaw("PONG " + payload);
+ return;
+ }
+
+ // only care about PRIVMSG
+ if (!line.contains(" PRIVMSG ")) {
+ return;
+ }
+
+ int prefixEnd = line.indexOf(' ');
+ if (!line.startsWith(":") || prefixEnd <= 1) {
+ return;
+ }
+
+ String prefix = line.substring(1, prefixEnd);
+
+ String nick = prefix;
+ int bang = prefix.indexOf('!');
+ if (bang != -1) {
+ nick = prefix.substring(0, bang);
+ }
+
+ String[] split = line.split(" :", 2);
+ if (split.length < 2) {
+ return;
+ }
+
+ String trailing = split[1];
+ String commandPart = split[0];
+
+ String[] cmdParts = commandPart.split(" ");
+ if (cmdParts.length < 3) {
+ return;
+ }
+
+ String target = cmdParts[2];
+
+ if (!target.equalsIgnoreCase(this.channel)
+ && !target.equalsIgnoreCase(this.nick)) {
+ return;
+ }
+
+ MutableText prefixText = Text.literal("[IRC] ")
+ .formatted(Formatting.AQUA);
+ MutableText nickText = Text.literal("<" + nick + "> ")
+ .formatted(Formatting.WHITE);
+ MutableText msgText = Text.literal(trailing)
+ .formatted(Formatting.WHITE);
+
+ sendClientChat(prefixText.append(nickText).append(msgText));
+ }
+
+ /**
+ * sends a message to the configured channel
+ */
+ public void sendChannelMessage(String message) {
+ if (!connected) return;
+ sendRaw("PRIVMSG " + channel + " :" + message);
+ }
+
+ /**
+ * low-level raw IRC send
+ */
+ private synchronized void sendRaw(String line) {
+ if (writer == null) return;
+ try {
+ writer.write(line);
+ writer.write("\r\n");
+ writer.flush();
+ } catch (IOException e) {
+ sendClientChat(
+ Text.literal("[IRC] ").formatted(Formatting.AQUA)
+ .append(Text.literal("Send error: " + e.getMessage())
+ .formatted(Formatting.GRAY))
+ );
+ disconnect();
+ }
+ }
+
+ private void sendClientChat(Text text) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client == null) return;
+
+ client.execute(() -> {
+ if (client.inGameHud != null) {
+ client.inGameHud.getChatHud().addMessage(text);
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/src/main/java/dev/plutorocks/MinecraftIRC.java b/src/main/java/dev/plutorocks/MinecraftIRC.java
index a8aeed9..c802f20 100644
--- a/src/main/java/dev/plutorocks/MinecraftIRC.java
+++ b/src/main/java/dev/plutorocks/MinecraftIRC.java
@@ -1,24 +1,237 @@
package dev.plutorocks;
-import net.fabricmc.api.ModInitializer;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.arguments.StringArgumentType;
+
+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.fabric.api.client.message.v1.ClientSendMessageEvents;
+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;
-public class MinecraftIRC implements ModInitializer {
+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 MinecraftIRC implements ClientModInitializer {
public static final String MOD_ID = "minecraftirc";
- // This logger is used to write text to the console and the log file.
- // It is considered best practice to use your mod id as the logger's name.
- // That way, it's clear which mod wrote info, warnings, and errors.
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("minecraftirc.json");
+
+ public static String ircHost = "irc.libera.chat";
+ public static final int ircPort = 6667;
+ public static String ircChannel = "#chat";
+ public static String ircNick = "Player";
+
+ public static IrcClient IRC_CLIENT;
+
@Override
- public void onInitialize() {
- // This code runs as soon as Minecraft is in a mod-load-ready state.
- // However, some things (like resources) may still be uninitialized.
- // Proceed with mild caution.
+ public void onInitializeClient() {
+ loadConfig();
+
+ IRC_CLIENT = new IrcClient(ircHost, ircPort, ircChannel, ircNick);
+ IRC_CLIENT.connect();
+
+ ClientSendMessageEvents.CHAT.register(message -> {
+ if (message.startsWith("!")) {
+ if (IRC_CLIENT != null && IRC_CLIENT.isConnected()) {
+ IRC_CLIENT.sendChannelMessage(message.substring(1));
+ } else {
+ sendIrcMessage("Not connected.");
+ }
+ }
+ });
+
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
+ dispatcher.register(
+ literal("irc")
+ .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("channel")
+ .then(argument("channel", StringArgumentType.word())
+ .executes(this::handleSetChannel)))
+ .then(literal("nick")
+ .then(argument("nick", StringArgumentType.word())
+ .executes(this::handleSetNick)))
+ );
+ });
+
+ LOGGER.info("MinecraftIRC 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()) {
+ ircHost = cfg.host;
+ } else {
+ ircHost = "irc.libera.chat";
+ }
+
+ if (cfg.channel != null && !cfg.channel.isEmpty()) {
+ ircChannel = normalizeChannel(cfg.channel);
+ } else {
+ ircChannel = normalizeChannel("chat");
+ }
+
+ if (cfg.nick != null && !cfg.nick.isEmpty()) {
+ ircNick = cfg.nick;
+ } else {
+ ircNick = defaultNickFromSession();
+ }
+ } catch (IOException | JsonSyntaxException e) {
+ LOGGER.warn("Failed to read MinecraftIRC config, using defaults", e);
+ applyDefaultsFromSession();
+ }
+ }
+
+ private static void saveConfig() {
+ Config cfg = new Config();
+ cfg.host = ircHost;
+ cfg.channel = ircChannel;
+ cfg.nick = ircNick;
+
+ try (Writer writer = Files.newBufferedWriter(CONFIG_PATH)) {
+ GSON.toJson(cfg, writer);
+ } catch (IOException e) {
+ LOGGER.warn("Failed to save MinecraftIRC config", e);
+ }
+ }
+
+ private static void applyDefaultsFromSession() {
+ ircHost = "irc.libera.chat";
+ ircChannel = normalizeChannel("chat");
+ ircNick = 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<FabricClientCommandSource> ctx) {
+ if (IRC_CLIENT != null && IRC_CLIENT.isConnected()) {
+ IRC_CLIENT.disconnect();
+ }
+
+ IRC_CLIENT = new IrcClient(ircHost, ircPort, ircChannel, ircNick);
+
+ sendCmdFeedback(ctx, "Connecting to " + ircHost + " " + ircChannel
+ + " as " + ircNick + " (port " + ircPort + ")...");
+ IRC_CLIENT.connect();
+ return 1;
+ }
+
+ private int handleDisconnect(CommandContext<FabricClientCommandSource> ctx) {
+ if (IRC_CLIENT == null || !IRC_CLIENT.isConnected()) {
+ sendCmdFeedback(ctx, "Already disconnected.");
+ return 1;
+ }
+
+ IRC_CLIENT.disconnect();
+ sendCmdFeedback(ctx, "Disconnected.");
+ return 1;
+ }
+
+ private int handleStatus(CommandContext<FabricClientCommandSource> ctx) {
+ String state = (IRC_CLIENT != null && IRC_CLIENT.isConnected()) ? "Connected" : "Not connected";
+ sendCmdFeedback(ctx, state + ". Host=" + ircHost
+ + " Channel=" + ircChannel + " Nick=" + ircNick
+ + " Port=" + ircPort);
+ return 1;
+ }
+
+ private int handleSetHost(CommandContext<FabricClientCommandSource> ctx) {
+ ircHost = StringArgumentType.getString(ctx, "host");
+ saveConfig();
+ sendCmdFeedback(ctx, "Host set to " + ircHost + ". Use /irc connect to apply.");
+ return 1;
+ }
+
+ private static String normalizeChannel(String raw) {
+ raw = raw.trim();
+ if (raw.isEmpty()) return "#chat";
+ if (!raw.startsWith("#")) raw = "#" + raw;
+ return raw;
+ }
+
+ private int handleSetChannel(CommandContext<FabricClientCommandSource> ctx) {
+ String raw = StringArgumentType.getString(ctx, "channel");
+ ircChannel = normalizeChannel(raw);
+ saveConfig();
+ sendCmdFeedback(ctx, "Channel set to " + ircChannel + ". Use /irc connect to apply.");
+ return 1;
+ }
+
+ private int handleSetNick(CommandContext<FabricClientCommandSource> ctx) {
+ ircNick = StringArgumentType.getString(ctx, "nick");
+ saveConfig();
+ sendCmdFeedback(ctx, "Nick set to " + ircNick + ". Use /irc connect to apply.");
+ return 1;
+ }
+
+ private void sendCmdFeedback(CommandContext<FabricClientCommandSource> ctx, String grayMessage) {
+ MutableText prefix = Text.literal("[IRC] ").formatted(Formatting.AQUA);
+ MutableText body = Text.literal(grayMessage).formatted(Formatting.GRAY);
+ ctx.getSource().sendFeedback(prefix.append(body));
+ }
+
+ private void sendIrcMessage(String grayMessage) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client == null) return;
+
+ client.execute(() -> {
+ if (client.inGameHud != null) {
+ MutableText prefix = Text.literal("[IRC] ").formatted(Formatting.AQUA);
+ MutableText body = Text.literal(grayMessage).formatted(Formatting.GRAY);
+ client.inGameHud.getChatHud().addMessage(prefix.append(body));
+ }
+ });
+ }
- LOGGER.info("Hello Fabric world!");
+ private static class Config {
+ String host;
+ String channel;
+ String nick;
}
} \ No newline at end of file
diff --git a/src/main/java/dev/plutorocks/mixin/ChatScreenMixin.java b/src/main/java/dev/plutorocks/mixin/ChatScreenMixin.java
new file mode 100644
index 0000000..6bc044d
--- /dev/null
+++ b/src/main/java/dev/plutorocks/mixin/ChatScreenMixin.java
@@ -0,0 +1,61 @@
+package dev.plutorocks.mixin;
+
+import dev.plutorocks.MinecraftIRC;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.ChatScreen;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ChatScreen.class)
+public abstract class ChatScreenMixin {
+
+ @Inject(method = "sendMessage", at = @At("HEAD"), cancellable = true)
+ private void minecraftirc$interceptIrcMessages(String chatText, boolean addToHistory, CallbackInfo ci) {
+ if (chatText == null) {
+ return;
+ }
+
+ String trimmed = chatText.trim();
+ if (!trimmed.startsWith("!")) {
+ return;
+ }
+
+ String ircMessage = trimmed.substring(1);
+
+ MinecraftClient client = MinecraftClient.getInstance();
+
+ if (MinecraftIRC.IRC_CLIENT != null && MinecraftIRC.IRC_CLIENT.isConnected()) {
+ if (!ircMessage.isEmpty()) {
+ MinecraftIRC.IRC_CLIENT.sendChannelMessage(ircMessage);
+ }
+
+ if (client != null && client.inGameHud != null) {
+ MutableText prefix = Text.literal("[IRC] ").formatted(Formatting.AQUA);
+ MutableText nick = Text.literal("<" + MinecraftIRC.ircNick + "> ")
+ .formatted(Formatting.WHITE);
+ MutableText body = Text.literal(ircMessage)
+ .formatted(Formatting.GRAY);
+
+ client.inGameHud.getChatHud().addMessage(
+ prefix.append(nick).append(body)
+ );
+ }
+ } else {
+ if (client != null && client.inGameHud != null) {
+ MutableText prefix = Text.literal("[IRC] ").formatted(Formatting.AQUA);
+ MutableText body = Text.literal("Not connected.")
+ .formatted(Formatting.GRAY);
+ client.inGameHud.getChatHud().addMessage(
+ prefix.append(body)
+ );
+ }
+ }
+
+ ci.cancel();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/dev/plutorocks/mixin/ExampleMixin.java b/src/main/java/dev/plutorocks/mixin/ExampleMixin.java
deleted file mode 100644
index da1eff9..0000000
--- a/src/main/java/dev/plutorocks/mixin/ExampleMixin.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package dev.plutorocks.mixin;
-
-import net.minecraft.server.MinecraftServer;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-
-@Mixin(MinecraftServer.class)
-public class ExampleMixin {
- @Inject(at = @At("HEAD"), method = "loadWorld")
- private void init(CallbackInfo info) {
- // This code is injected into the start of MinecraftServer.loadWorld()V
- }
-} \ No newline at end of file