Add friends, markers, HUD overlays, tab complete
- FriendsManager persists a per-user friend list at config/clientirc-friends.json. - MarkerManager holds one ephemeral marker per owner (5 min expiry). - IRC protocol extended with marker_set / marker_remove; server relays them. - IrcService DisplayLine replaced with sealed IrcEvent (Chat / System / MarkerSet / MarkerRemove); chat formatting moved into the client. - ClientIrcMod handles .friends add/remove/list, .marker remove, .help; chat lines from friends are prefixed with a green dot; marker events from non-friends are ignored. - OverlayHud draws a top-center compass strip and a top-left watermark (nick / FPS / ping) via HudLayerRegistrationCallback. - WorldOverlayRenderer draws green crosses above friends and magenta beams + base cubes at active markers via WorldRenderEvents.AFTER_ENTITIES. - MiddleClickHandler binds MMB via KeyMapping, raycasts a block or 20-block look ray, stores locally and sends marker_set on a daemon executor. - ChatScreenMixin completes . prefix commands on Tab; first mixin in the project, plumbed through clientirc.client.mixins.json and fabric.mod.json.
This commit is contained in:
@@ -106,6 +106,46 @@ const server = net.createServer((socket) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (payload.type === 'marker_set') {
|
||||
const clientName = String(payload.client || 'UNKNOWN-CLIENT').trim();
|
||||
const playerName = String(payload.player || 'unknown').trim();
|
||||
const dimensionId = String(payload.dimensionId || 'minecraft:overworld').trim();
|
||||
const x = Number(payload.x);
|
||||
const y = Number(payload.y);
|
||||
const z = Number(payload.z);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
||||
sendJson(socket, systemMessage('Bad marker_set coords.'));
|
||||
continue;
|
||||
}
|
||||
const outgoing = {
|
||||
type: 'marker_set',
|
||||
client: clientName,
|
||||
player: playerName,
|
||||
dimensionId,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
console.log(`[marker_set] ${clientName} >> ${playerName} @ ${dimensionId} ${x.toFixed(1)},${y.toFixed(1)},${z.toFixed(1)}`);
|
||||
broadcast(outgoing);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (payload.type === 'marker_remove') {
|
||||
const clientName = String(payload.client || 'UNKNOWN-CLIENT').trim();
|
||||
const playerName = String(payload.player || 'unknown').trim();
|
||||
const outgoing = {
|
||||
type: 'marker_remove',
|
||||
client: clientName,
|
||||
player: playerName,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
console.log(`[marker_remove] ${clientName} >> ${playerName}`);
|
||||
broadcast(outgoing);
|
||||
continue;
|
||||
}
|
||||
|
||||
sendJson(socket, systemMessage(`Unknown packet type: ${payload.type}`));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package ru.nevetime.clientirc;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
@@ -8,17 +13,15 @@ import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class ClientIrcMod implements ClientModInitializer {
|
||||
public static final String MOD_ID = "clientirc";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
private static final String COMMAND_PREFIX = ".irc";
|
||||
private static final String COMMAND_PREFIX = ".";
|
||||
|
||||
private static final ExecutorService IO_EXECUTOR = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "clientirc-io");
|
||||
@@ -31,9 +34,15 @@ public final class ClientIrcMod implements ClientModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
config = IrcConfig.load(FabricLoader.getInstance().getConfigDir().resolve("clientirc.json"));
|
||||
Path configDir = FabricLoader.getInstance().getConfigDir();
|
||||
config = IrcConfig.load(configDir.resolve("clientirc.json"));
|
||||
FriendsManager.init(configDir);
|
||||
service = new IrcService(config, LOGGER);
|
||||
|
||||
OverlayHud.init();
|
||||
WorldOverlayRenderer.init();
|
||||
MiddleClickHandler.init(() -> service, () -> resolvePlayerName(Minecraft.getInstance()));
|
||||
|
||||
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
|
||||
if (config.autoConnect) {
|
||||
String playerName = resolvePlayerName(client);
|
||||
@@ -47,16 +56,44 @@ public final class ClientIrcMod implements ClientModInitializer {
|
||||
});
|
||||
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
IrcService.DisplayLine line;
|
||||
while ((line = service.pollLine()) != null) {
|
||||
ChatFormatting color = line.system() ? ChatFormatting.GRAY : ChatFormatting.AQUA;
|
||||
pushClientChat(client, Component.literal(line.text()).withStyle(color));
|
||||
IrcService.IrcEvent event;
|
||||
while ((event = service.pollEvent()) != null) {
|
||||
handleEvent(client, event);
|
||||
}
|
||||
});
|
||||
|
||||
ClientSendMessageEvents.ALLOW_CHAT.register(message -> !interceptOutgoingChat(message));
|
||||
}
|
||||
|
||||
private static void handleEvent(Minecraft client, IrcService.IrcEvent event) {
|
||||
switch (event) {
|
||||
case IrcService.IrcEvent.Chat chat -> {
|
||||
MutableComponent line = Component.literal("[IRC] " + chat.client() + " >> ").withStyle(ChatFormatting.AQUA);
|
||||
if (FriendsManager.isFriend(chat.sender())) {
|
||||
line.append(Component.literal("● ").withStyle(ChatFormatting.GREEN));
|
||||
}
|
||||
line.append(Component.literal(chat.sender() + ": " + chat.message()).withStyle(ChatFormatting.AQUA));
|
||||
pushClientChat(client, line);
|
||||
}
|
||||
case IrcService.IrcEvent.System sys ->
|
||||
pushClientChat(client, Component.literal("[IRC] " + sys.message()).withStyle(ChatFormatting.GRAY));
|
||||
case IrcService.IrcEvent.MarkerSet ms -> {
|
||||
if (FriendsManager.isFriend(ms.sender())) {
|
||||
MarkerManager.set(new MarkerManager.Marker(
|
||||
ms.sender(), ms.dimensionId(), ms.x(), ms.y(), ms.z(), System.currentTimeMillis()
|
||||
));
|
||||
pushClientChat(client, Component.literal("[IRC] marker from " + ms.sender()).withStyle(ChatFormatting.LIGHT_PURPLE));
|
||||
}
|
||||
}
|
||||
case IrcService.IrcEvent.MarkerRemove mr -> {
|
||||
if (FriendsManager.isFriend(mr.sender())) {
|
||||
MarkerManager.remove(mr.sender());
|
||||
pushClientChat(client, Component.literal("[IRC] marker removed by " + mr.sender()).withStyle(ChatFormatting.LIGHT_PURPLE));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean interceptOutgoingChat(String rawMessage) {
|
||||
String trimmed = rawMessage.trim();
|
||||
if (!trimmed.startsWith(COMMAND_PREFIX)) {
|
||||
@@ -64,20 +101,34 @@ public final class ClientIrcMod implements ClientModInitializer {
|
||||
}
|
||||
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
String argument = trimmed.length() == COMMAND_PREFIX.length() ? "" : trimmed.substring(COMMAND_PREFIX.length()).trim();
|
||||
String[] parts = trimmed.split("\\s+", 2);
|
||||
String cmd = parts[0].toLowerCase(Locale.ROOT);
|
||||
String rest = parts.length > 1 ? parts[1].trim() : "";
|
||||
|
||||
if (argument.isBlank()) {
|
||||
return switch (cmd) {
|
||||
case ".irc" -> handleIrcCommand(client, rest);
|
||||
case ".friends" -> handleFriendsCommand(client, rest);
|
||||
case ".marker" -> handleMarkerCommand(client, rest);
|
||||
case ".help" -> {
|
||||
showHelp(client);
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean handleIrcCommand(Minecraft client, String rest) {
|
||||
if (rest.isBlank()) {
|
||||
pushClientChat(client, Component.literal("[IRC] Usage: .irc <message> | .irc status | .irc reconnect | .irc reload").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("status".equalsIgnoreCase(argument)) {
|
||||
String lower = rest.toLowerCase(Locale.ROOT);
|
||||
if ("status".equals(lower)) {
|
||||
String status = service.isConnected() ? "connected" : "disconnected";
|
||||
pushClientChat(client, Component.literal("[IRC] " + status + " | " + config.host + ":" + config.port + " | clientName=" + config.clientName).withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("reconnect".equalsIgnoreCase(argument)) {
|
||||
if ("reconnect".equals(lower)) {
|
||||
String playerName = resolvePlayerName(client);
|
||||
IO_EXECUTOR.execute(() -> {
|
||||
boolean connected = service.reconnect(playerName);
|
||||
@@ -86,27 +137,87 @@ public final class ClientIrcMod implements ClientModInitializer {
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("reload".equalsIgnoreCase(argument)) {
|
||||
if ("reload".equals(lower)) {
|
||||
config = IrcConfig.load(FabricLoader.getInstance().getConfigDir().resolve("clientirc.json"));
|
||||
service.updateConfig(config);
|
||||
pushClientChat(client, Component.literal("[IRC] config reloaded").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
|
||||
String playerName = resolvePlayerName(client);
|
||||
IO_EXECUTOR.execute(() -> {
|
||||
if (!service.ensureConnected(playerName)) {
|
||||
pushClientChat(client, Component.literal("[IRC] server is offline or config is wrong").withStyle(ChatFormatting.RED));
|
||||
return;
|
||||
}
|
||||
if (!service.sendChat(playerName, argument)) {
|
||||
if (!service.sendChat(playerName, rest)) {
|
||||
pushClientChat(client, Component.literal("[IRC] send failed").withStyle(ChatFormatting.RED));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean handleFriendsCommand(Minecraft client, String rest) {
|
||||
if (rest.isBlank()) {
|
||||
pushClientChat(client, Component.literal("[IRC] Usage: .friends add <name> | .friends remove <name> | .friends list").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
String[] parts = rest.split("\\s+", 2);
|
||||
String sub = parts[0].toLowerCase(Locale.ROOT);
|
||||
String arg = parts.length > 1 ? parts[1].trim() : "";
|
||||
|
||||
if ("add".equals(sub)) {
|
||||
if (arg.isBlank()) {
|
||||
pushClientChat(client, Component.literal("[IRC] usage: .friends add <name>").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
boolean added = FriendsManager.add(arg);
|
||||
pushClientChat(client, Component.literal("[IRC] " + (added ? "added friend " + arg : arg + " is already a friend"))
|
||||
.withStyle(added ? ChatFormatting.GREEN : ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
if ("remove".equals(sub)) {
|
||||
if (arg.isBlank()) {
|
||||
pushClientChat(client, Component.literal("[IRC] usage: .friends remove <name>").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
boolean removed = FriendsManager.remove(arg);
|
||||
pushClientChat(client, Component.literal("[IRC] " + (removed ? "removed friend " + arg : arg + " was not a friend"))
|
||||
.withStyle(removed ? ChatFormatting.GREEN : ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
if ("list".equals(sub)) {
|
||||
List<String> friends = FriendsManager.list();
|
||||
if (friends.isEmpty()) {
|
||||
pushClientChat(client, Component.literal("[IRC] friends list is empty").withStyle(ChatFormatting.YELLOW));
|
||||
} else {
|
||||
pushClientChat(client, Component.literal("[IRC] friends (" + friends.size() + "): " + String.join(", ", friends))
|
||||
.withStyle(ChatFormatting.GREEN));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
pushClientChat(client, Component.literal("[IRC] unknown subcommand. Use add/remove/list.").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean handleMarkerCommand(Minecraft client, String rest) {
|
||||
String lower = rest.toLowerCase(Locale.ROOT);
|
||||
if ("remove".equals(lower)) {
|
||||
MiddleClickHandler.removeOwnMarker(() -> service, () -> resolvePlayerName(client));
|
||||
pushClientChat(client, Component.literal("[IRC] your marker removed").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
pushClientChat(client, Component.literal("[IRC] Usage: middle-click to drop a marker (friends only see it) | .marker remove").withStyle(ChatFormatting.YELLOW));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void showHelp(Minecraft client) {
|
||||
pushClientChat(client, Component.literal("[IRC] Commands:").withStyle(ChatFormatting.YELLOW));
|
||||
pushClientChat(client, Component.literal(" .irc <msg> | .irc status | .irc reconnect | .irc reload").withStyle(ChatFormatting.GRAY));
|
||||
pushClientChat(client, Component.literal(" .friends add <name> | .friends remove <name> | .friends list").withStyle(ChatFormatting.GRAY));
|
||||
pushClientChat(client, Component.literal(" MMB drops a marker (friends only see it) | .marker remove").withStyle(ChatFormatting.GRAY));
|
||||
pushClientChat(client, Component.literal(" .help shows this list").withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
|
||||
private static void pushClientChat(Minecraft client, Component text) {
|
||||
if (client == null) {
|
||||
return;
|
||||
|
||||
56
src/client/java/ru/nevetime/clientirc/MarkerManager.java
Normal file
56
src/client/java/ru/nevetime/clientirc/MarkerManager.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package ru.nevetime.clientirc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class MarkerManager {
|
||||
private static final long EXPIRY_MS = 5L * 60L * 1000L;
|
||||
private static final ConcurrentHashMap<String, Marker> MARKERS = new ConcurrentHashMap<>();
|
||||
|
||||
private MarkerManager() {
|
||||
}
|
||||
|
||||
public record Marker(String owner, String dimensionId, double x, double y, double z, long createdAtMs) {
|
||||
}
|
||||
|
||||
public static void set(Marker marker) {
|
||||
if (marker == null || marker.owner() == null || marker.owner().isBlank()) {
|
||||
return;
|
||||
}
|
||||
MARKERS.put(marker.owner().toLowerCase(Locale.ROOT), marker);
|
||||
}
|
||||
|
||||
public static void remove(String owner) {
|
||||
if (owner == null || owner.isBlank()) {
|
||||
return;
|
||||
}
|
||||
MARKERS.remove(owner.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
MARKERS.clear();
|
||||
}
|
||||
|
||||
public static Collection<Marker> active() {
|
||||
long now = System.currentTimeMillis();
|
||||
ArrayList<Marker> alive = new ArrayList<>(MARKERS.size());
|
||||
Iterator<Map.Entry<String, Marker>> it = MARKERS.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, Marker> entry = it.next();
|
||||
if (now - entry.getValue().createdAtMs() > EXPIRY_MS) {
|
||||
it.remove();
|
||||
} else {
|
||||
alive.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
return alive;
|
||||
}
|
||||
|
||||
public static long expiryMs() {
|
||||
return EXPIRY_MS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ru.nevetime.clientirc;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class MiddleClickHandler {
|
||||
private static final ExecutorService IO_EXECUTOR =
|
||||
Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread t = new Thread(runnable, "clientirc-marker-io");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private static KeyMapping markerKey;
|
||||
|
||||
private MiddleClickHandler() {}
|
||||
|
||||
public static void init(Supplier<IrcService> serviceSupplier, Supplier<String> playerNameSupplier) {
|
||||
markerKey = new KeyMapping(
|
||||
"key.clientirc.marker",
|
||||
InputConstants.Type.MOUSE,
|
||||
GLFW.GLFW_MOUSE_BUTTON_MIDDLE,
|
||||
"category.clientirc"
|
||||
);
|
||||
KeyBindingHelper.registerKeyBinding(markerKey);
|
||||
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
while (markerKey.consumeClick()) {
|
||||
if (client.player == null || client.level == null) break;
|
||||
|
||||
Vec3 target;
|
||||
if (client.hitResult instanceof BlockHitResult br && br.getType() != HitResult.Type.MISS) {
|
||||
target = br.getBlockPos().getCenter();
|
||||
} else {
|
||||
Vec3 eye = client.player.getEyePosition();
|
||||
Vec3 look = client.player.getViewVector(1.0f);
|
||||
target = eye.add(look.scale(20.0));
|
||||
}
|
||||
|
||||
String dimId = client.level.dimension().location().toString();
|
||||
String playerName = playerNameSupplier.get();
|
||||
double tx = target.x, ty = target.y, tz = target.z;
|
||||
|
||||
MarkerManager.set(new MarkerManager.Marker(playerName, dimId, tx, ty, tz, System.currentTimeMillis()));
|
||||
|
||||
IO_EXECUTOR.execute(() -> {
|
||||
IrcService service = serviceSupplier.get();
|
||||
if (service != null) service.sendMarkerSet(playerName, dimId, tx, ty, tz);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void removeOwnMarker(Supplier<IrcService> serviceSupplier, Supplier<String> playerNameSupplier) {
|
||||
String name = playerNameSupplier.get();
|
||||
MarkerManager.remove(name);
|
||||
IO_EXECUTOR.execute(() -> {
|
||||
IrcService service = serviceSupplier.get();
|
||||
if (service != null) service.sendMarkerRemove(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
126
src/client/java/ru/nevetime/clientirc/OverlayHud.java
Normal file
126
src/client/java/ru/nevetime/clientirc/OverlayHud.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package ru.nevetime.clientirc;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.HudLayerRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.LayeredDraw;
|
||||
import net.minecraft.client.multiplayer.PlayerInfo;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public final class OverlayHud {
|
||||
private static final ResourceLocation COMPASS_LAYER = ResourceLocation.fromNamespaceAndPath("clientirc", "compass");
|
||||
private static final ResourceLocation WATERMARK_LAYER = ResourceLocation.fromNamespaceAndPath("clientirc", "watermark");
|
||||
|
||||
private static final int COMPASS_WIDTH = 180;
|
||||
private static final int COMPASS_HEIGHT = 14;
|
||||
private static final int COMPASS_Y = 2;
|
||||
|
||||
private OverlayHud() {
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
HudLayerRegistrationCallback.EVENT.register(layered -> {
|
||||
layered.attachLayerAfter(IdentifiedLayer.MISC_OVERLAYS, COMPASS_LAYER, (LayeredDraw.Layer) OverlayHud::renderCompass);
|
||||
layered.attachLayerAfter(IdentifiedLayer.MISC_OVERLAYS, WATERMARK_LAYER, (LayeredDraw.Layer) OverlayHud::renderWatermark);
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean skip(Minecraft mc) {
|
||||
if (mc == null) {
|
||||
return true;
|
||||
}
|
||||
if (mc.options != null && mc.options.hideGui) {
|
||||
return true;
|
||||
}
|
||||
return mc.level == null;
|
||||
}
|
||||
|
||||
private static void renderCompass(GuiGraphics graphics, DeltaTracker deltaTracker) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (skip(mc)) {
|
||||
return;
|
||||
}
|
||||
Font font = mc.font;
|
||||
|
||||
int x = (graphics.guiWidth() - COMPASS_WIDTH) / 2;
|
||||
int y = COMPASS_Y;
|
||||
|
||||
graphics.fill(x, y, x + COMPASS_WIDTH, y + COMPASS_HEIGHT, 0x80000000);
|
||||
|
||||
float yaw = mc.gameRenderer.getMainCamera().getYRot();
|
||||
|
||||
int[] cardYaws = {180, -90, 0, 90};
|
||||
String[] cardLabels = {"N", "E", "S", "W"};
|
||||
|
||||
for (int i = 0; i < cardYaws.length; i++) {
|
||||
float delta = wrapDegrees(cardYaws[i] - yaw);
|
||||
if (Math.abs(delta) > 90.0f) {
|
||||
continue;
|
||||
}
|
||||
int px = x + 90 + (int) delta;
|
||||
int color = Math.abs(delta) <= 5.0f ? 0xFFFFFF55 : 0xFFFFFFFF;
|
||||
int labelWidth = font.width(cardLabels[i]);
|
||||
graphics.drawString(font, cardLabels[i], px - labelWidth / 2, y + 3, color, true);
|
||||
}
|
||||
|
||||
graphics.fill(x + 89, y + 11, x + 92, y + 14, 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
private static void renderWatermark(GuiGraphics graphics, DeltaTracker deltaTracker) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (skip(mc)) {
|
||||
return;
|
||||
}
|
||||
Font font = mc.font;
|
||||
int x = 4;
|
||||
int y = 4;
|
||||
int lineHeight = font.lineHeight + 2;
|
||||
|
||||
String nick = resolveNick(mc);
|
||||
int fps = mc.getFps();
|
||||
int ping = resolvePing(mc);
|
||||
|
||||
graphics.drawString(font, nick, x, y, 0xFFFFFFFF, true);
|
||||
graphics.drawString(font, "FPS: " + fps, x, y + lineHeight, 0xFFFFFFFF, true);
|
||||
graphics.drawString(font, "Ping: " + ping + " ms", x, y + 2 * lineHeight, 0xFFFFFFFF, true);
|
||||
}
|
||||
|
||||
private static String resolveNick(Minecraft mc) {
|
||||
if (mc.player != null) {
|
||||
GameProfile profile = mc.player.getGameProfile();
|
||||
if (profile != null && profile.name() != null && !profile.name().isBlank()) {
|
||||
return profile.name();
|
||||
}
|
||||
}
|
||||
if (mc.getUser() != null) {
|
||||
String name = mc.getUser().getName();
|
||||
if (name != null && !name.isBlank()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return "—";
|
||||
}
|
||||
|
||||
private static int resolvePing(Minecraft mc) {
|
||||
if (mc.getConnection() == null || mc.getUser() == null) {
|
||||
return 0;
|
||||
}
|
||||
PlayerInfo info = mc.getConnection().getPlayerInfo(mc.getUser().getProfileId());
|
||||
return info == null ? 0 : info.getLatency();
|
||||
}
|
||||
|
||||
private static float wrapDegrees(float v) {
|
||||
v = v % 360.0f;
|
||||
if (v >= 180.0f) {
|
||||
v -= 360.0f;
|
||||
}
|
||||
if (v < -180.0f) {
|
||||
v += 360.0f;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
120
src/client/java/ru/nevetime/clientirc/WorldOverlayRenderer.java
Normal file
120
src/client/java/ru/nevetime/clientirc/WorldOverlayRenderer.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package ru.nevetime.clientirc;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
import java.util.Collection;
|
||||
|
||||
public final class WorldOverlayRenderer {
|
||||
private WorldOverlayRenderer() {}
|
||||
|
||||
public static void init() {
|
||||
WorldRenderEvents.AFTER_ENTITIES.register(WorldOverlayRenderer::onAfterEntities);
|
||||
}
|
||||
|
||||
private static void onAfterEntities(WorldRenderContext context) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (client.level == null) return;
|
||||
if (client.options != null && client.options.hideGui) return;
|
||||
|
||||
PoseStack pose = context.matrixStack();
|
||||
if (pose == null) return;
|
||||
|
||||
Camera camera = context.camera();
|
||||
if (camera == null) return;
|
||||
Vec3 camPos = camera.getPosition();
|
||||
|
||||
MultiBufferSource consumers = context.consumers();
|
||||
if (consumers == null) return;
|
||||
|
||||
VertexConsumer lines = consumers.getBuffer(RenderType.lines());
|
||||
|
||||
// Friend dots
|
||||
for (Player player : client.level.players()) {
|
||||
if (player == client.player) continue;
|
||||
if (player.getGameProfile() == null) continue;
|
||||
if (FriendsManager.isFriend(player.getGameProfile().name())) {
|
||||
double px = player.getX() - camPos.x;
|
||||
double py = player.getY() + player.getBbHeight() + 0.5 - camPos.y;
|
||||
double pz = player.getZ() - camPos.z;
|
||||
int r = 0x33, g = 0xFF, b = 0x55, a = 0xFF;
|
||||
drawLine(lines, pose, px - 0.15, py, pz, px + 0.15, py, pz, r, g, b, a);
|
||||
drawLine(lines, pose, px, py - 0.15, pz, px, py + 0.15, pz, r, g, b, a);
|
||||
drawLine(lines, pose, px, py, pz - 0.15, px, py, pz + 0.15, r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
// Marker beacons
|
||||
String currentDim = client.level.dimension().location().toString();
|
||||
Collection<MarkerManager.Marker> markers = MarkerManager.active();
|
||||
for (MarkerManager.Marker marker : markers) {
|
||||
if (!currentDim.equals(marker.dimensionId())) continue;
|
||||
double mx = marker.x() - camPos.x;
|
||||
double my = marker.y() - camPos.y;
|
||||
double mz = marker.z() - camPos.z;
|
||||
int r = 0xFF, g = 0x55, b = 0xFF, a = 0xFF;
|
||||
|
||||
// Vertical beam
|
||||
drawLine(lines, pose, mx, my, mz, mx, my + 50.0, mz, r, g, b, a);
|
||||
|
||||
// 1x1x1 cube outline centered at (mx, my + 0.5, mz)
|
||||
double cx = mx, cy = my + 0.5, cz = mz;
|
||||
double x0 = cx - 0.5, x1 = cx + 0.5;
|
||||
double y0 = cy - 0.5, y1 = cy + 0.5;
|
||||
double z0 = cz - 0.5, z1 = cz + 0.5;
|
||||
|
||||
// Bottom square (y = y0)
|
||||
drawLine(lines, pose, x0, y0, z0, x1, y0, z0, r, g, b, a);
|
||||
drawLine(lines, pose, x1, y0, z0, x1, y0, z1, r, g, b, a);
|
||||
drawLine(lines, pose, x1, y0, z1, x0, y0, z1, r, g, b, a);
|
||||
drawLine(lines, pose, x0, y0, z1, x0, y0, z0, r, g, b, a);
|
||||
|
||||
// Top square (y = y1)
|
||||
drawLine(lines, pose, x0, y1, z0, x1, y1, z0, r, g, b, a);
|
||||
drawLine(lines, pose, x1, y1, z0, x1, y1, z1, r, g, b, a);
|
||||
drawLine(lines, pose, x1, y1, z1, x0, y1, z1, r, g, b, a);
|
||||
drawLine(lines, pose, x0, y1, z1, x0, y1, z0, r, g, b, a);
|
||||
|
||||
// Vertical edges
|
||||
drawLine(lines, pose, x0, y0, z0, x0, y1, z0, r, g, b, a);
|
||||
drawLine(lines, pose, x1, y0, z0, x1, y1, z0, r, g, b, a);
|
||||
drawLine(lines, pose, x1, y0, z1, x1, y1, z1, r, g, b, a);
|
||||
drawLine(lines, pose, x0, y0, z1, x0, y1, z1, r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawLine(VertexConsumer lines, PoseStack pose,
|
||||
double x1, double y1, double z1,
|
||||
double x2, double y2, double z2,
|
||||
int r, int g, int b, int a) {
|
||||
Matrix4f matrix = pose.last().pose();
|
||||
Matrix3f normalMatrix = pose.last().normal();
|
||||
|
||||
double dx = x2 - x1;
|
||||
double dy = y2 - y1;
|
||||
double dz = z2 - z1;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
float nx, ny, nz;
|
||||
if (len < 1e-6) {
|
||||
nx = 0.0f;
|
||||
ny = 1.0f;
|
||||
nz = 0.0f;
|
||||
} else {
|
||||
nx = (float) (dx / len);
|
||||
ny = (float) (dy / len);
|
||||
nz = (float) (dz / len);
|
||||
}
|
||||
|
||||
lines.vertex(matrix, (float) x1, (float) y1, (float) z1).color(r, g, b, a).normal(normalMatrix, nx, ny, nz);
|
||||
lines.vertex(matrix, (float) x2, (float) y2, (float) z2).color(r, g, b, a).normal(normalMatrix, nx, ny, nz);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ru.nevetime.clientirc.mixin;
|
||||
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.ChatScreen;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(ChatScreen.class)
|
||||
public abstract class ChatScreenMixin {
|
||||
@Shadow protected EditBox input;
|
||||
|
||||
private static final String[] CLIENTIRC_COMMANDS = new String[] {
|
||||
".irc",
|
||||
".irc status",
|
||||
".irc reconnect",
|
||||
".irc reload",
|
||||
".friends add ",
|
||||
".friends remove ",
|
||||
".friends list",
|
||||
".marker remove",
|
||||
".help"
|
||||
};
|
||||
|
||||
@Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true)
|
||||
private void clientirc$tabComplete(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) {
|
||||
if (keyCode != GLFW.GLFW_KEY_TAB) return;
|
||||
if (input == null) return;
|
||||
String value = input.getValue();
|
||||
if (value == null || !value.startsWith(".")) return;
|
||||
|
||||
String best = null;
|
||||
for (String candidate : CLIENTIRC_COMMANDS) {
|
||||
if (candidate.startsWith(value) && !candidate.equals(value)) {
|
||||
if (best == null || candidate.length() < best.length()) {
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
input.setValue(best);
|
||||
input.setCursorPosition(best.length());
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/client/resources/clientirc.client.mixins.json
Normal file
11
src/client/resources/clientirc.client.mixins.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "ru.nevetime.clientirc.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"ChatScreenMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
129
src/main/java/ru/nevetime/clientirc/FriendsManager.java
Normal file
129
src/main/java/ru/nevetime/clientirc/FriendsManager.java
Normal file
@@ -0,0 +1,129 @@
|
||||
package ru.nevetime.clientirc;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class FriendsManager {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FriendsManager.class);
|
||||
|
||||
private static final ConcurrentHashMap<String, String> FRIENDS = new ConcurrentHashMap<>();
|
||||
private static volatile Path file;
|
||||
|
||||
private FriendsManager() {
|
||||
}
|
||||
|
||||
public static void init(Path configDir) {
|
||||
file = configDir.resolve("clientirc-friends.json");
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
} catch (IOException ex) {
|
||||
LOGGER.warn("Failed to create config directory for friends", ex);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
public static boolean add(String name) {
|
||||
if (name == null || name.isBlank() || file == null) {
|
||||
return false;
|
||||
}
|
||||
String trimmed = name.trim();
|
||||
String key = trimmed.toLowerCase(Locale.ROOT);
|
||||
if (FRIENDS.putIfAbsent(key, trimmed) != null) {
|
||||
return false;
|
||||
}
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean remove(String name) {
|
||||
if (name == null || name.isBlank() || file == null) {
|
||||
return false;
|
||||
}
|
||||
String key = name.trim().toLowerCase(Locale.ROOT);
|
||||
if (FRIENDS.remove(key) == null) {
|
||||
return false;
|
||||
}
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isFriend(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return FRIENDS.containsKey(name.trim().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public static List<String> list() {
|
||||
List<String> all = new ArrayList<>(FRIENDS.values());
|
||||
all.sort(Comparator.comparing(s -> s.toLowerCase(Locale.ROOT)));
|
||||
return all;
|
||||
}
|
||||
|
||||
private static void load() {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
if (!Files.exists(file)) {
|
||||
save();
|
||||
return;
|
||||
}
|
||||
try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
|
||||
Storage storage = GSON.fromJson(reader, Storage.class);
|
||||
FRIENDS.clear();
|
||||
if (storage != null && storage.friends != null) {
|
||||
for (String name : storage.friends) {
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
String trimmed = name.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
FRIENDS.put(trimmed.toLowerCase(Locale.ROOT), trimmed);
|
||||
}
|
||||
}
|
||||
} catch (IOException | JsonParseException ex) {
|
||||
LOGGER.warn("Failed to load friends list from {}", file, ex);
|
||||
FRIENDS.clear();
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private static void save() {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
Storage storage = new Storage();
|
||||
storage.friends = list();
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
|
||||
GSON.toJson(storage, writer);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOGGER.warn("Failed to save friends list to {}", file, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Storage {
|
||||
@SerializedName("friends")
|
||||
List<String> friends;
|
||||
}
|
||||
}
|
||||
@@ -6,27 +6,55 @@ public final class IrcMessage {
|
||||
public String player;
|
||||
public String message;
|
||||
public String token;
|
||||
public String dimensionId;
|
||||
public Double x;
|
||||
public Double y;
|
||||
public Double z;
|
||||
public long timestamp;
|
||||
|
||||
public static IrcMessage hello(String clientName, String playerName, String token) {
|
||||
IrcMessage message = new IrcMessage();
|
||||
message.type = "hello";
|
||||
message.client = clientName;
|
||||
message.player = playerName;
|
||||
message.token = token;
|
||||
message.timestamp = System.currentTimeMillis();
|
||||
return message;
|
||||
IrcMessage m = new IrcMessage();
|
||||
m.type = "hello";
|
||||
m.client = clientName;
|
||||
m.player = playerName;
|
||||
m.token = token;
|
||||
m.timestamp = System.currentTimeMillis();
|
||||
return m;
|
||||
}
|
||||
|
||||
public static IrcMessage chat(String clientName, String playerName, String text, String token) {
|
||||
IrcMessage message = new IrcMessage();
|
||||
message.type = "chat";
|
||||
message.client = clientName;
|
||||
message.player = playerName;
|
||||
message.message = text;
|
||||
message.token = token;
|
||||
message.timestamp = System.currentTimeMillis();
|
||||
return message;
|
||||
IrcMessage m = new IrcMessage();
|
||||
m.type = "chat";
|
||||
m.client = clientName;
|
||||
m.player = playerName;
|
||||
m.message = text;
|
||||
m.token = token;
|
||||
m.timestamp = System.currentTimeMillis();
|
||||
return m;
|
||||
}
|
||||
|
||||
public static IrcMessage markerSet(String clientName, String playerName, String token,
|
||||
String dimensionId, double x, double y, double z) {
|
||||
IrcMessage m = new IrcMessage();
|
||||
m.type = "marker_set";
|
||||
m.client = clientName;
|
||||
m.player = playerName;
|
||||
m.token = token;
|
||||
m.dimensionId = dimensionId;
|
||||
m.x = x;
|
||||
m.y = y;
|
||||
m.z = z;
|
||||
m.timestamp = System.currentTimeMillis();
|
||||
return m;
|
||||
}
|
||||
|
||||
public static IrcMessage markerRemove(String clientName, String playerName, String token) {
|
||||
IrcMessage m = new IrcMessage();
|
||||
m.type = "marker_remove";
|
||||
m.client = clientName;
|
||||
m.player = playerName;
|
||||
m.token = token;
|
||||
m.timestamp = System.currentTimeMillis();
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public final class IrcService {
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private final Logger logger;
|
||||
private final ConcurrentLinkedQueue<DisplayLine> queue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentLinkedQueue<IrcEvent> queue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private volatile IrcConfig config;
|
||||
private volatile Socket socket;
|
||||
@@ -33,6 +33,20 @@ public final class IrcService {
|
||||
this.logger = Objects.requireNonNull(logger, "logger");
|
||||
}
|
||||
|
||||
public sealed interface IrcEvent {
|
||||
record Chat(String client, String sender, String message) implements IrcEvent {
|
||||
}
|
||||
|
||||
record System(String message) implements IrcEvent {
|
||||
}
|
||||
|
||||
record MarkerSet(String sender, String dimensionId, double x, double y, double z) implements IrcEvent {
|
||||
}
|
||||
|
||||
record MarkerRemove(String sender) implements IrcEvent {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateConfig(IrcConfig config) {
|
||||
this.config = Objects.requireNonNull(config, "config");
|
||||
}
|
||||
@@ -66,12 +80,12 @@ public final class IrcService {
|
||||
sendInternal(IrcMessage.hello(config.clientName, playerName, config.token));
|
||||
enqueueSystem("Connected to " + config.host + ":" + config.port);
|
||||
return true;
|
||||
} catch (IOException exception) {
|
||||
} catch (IOException ex) {
|
||||
closeQuietly(newReader);
|
||||
closeQuietly(newWriter);
|
||||
closeQuietly(newSocket);
|
||||
enqueueSystem("Connection failed: " + exception.getMessage());
|
||||
logger.warn("IRC connection failed", exception);
|
||||
enqueueSystem("Connection failed: " + ex.getMessage());
|
||||
logger.warn("IRC connection failed", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -90,13 +104,44 @@ public final class IrcService {
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
sendInternal(IrcMessage.chat(config.clientName, playerName, text, config.token));
|
||||
return true;
|
||||
} catch (IOException exception) {
|
||||
enqueueSystem("Send failed: " + exception.getMessage());
|
||||
logger.warn("Failed to send IRC message", exception);
|
||||
} catch (IOException ex) {
|
||||
enqueueSystem("Send failed: " + ex.getMessage());
|
||||
logger.warn("Failed to send IRC chat", ex);
|
||||
running = false;
|
||||
closeSocketState();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean sendMarkerSet(String playerName, String dimensionId, double x, double y, double z) {
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
sendInternal(IrcMessage.markerSet(config.clientName, playerName, config.token, dimensionId, x, y, z));
|
||||
return true;
|
||||
} catch (IOException ex) {
|
||||
enqueueSystem("Send failed: " + ex.getMessage());
|
||||
logger.warn("Failed to send IRC marker_set", ex);
|
||||
running = false;
|
||||
closeSocketState();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean sendMarkerRemove(String playerName) {
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
sendInternal(IrcMessage.markerRemove(config.clientName, playerName, config.token));
|
||||
return true;
|
||||
} catch (IOException ex) {
|
||||
enqueueSystem("Send failed: " + ex.getMessage());
|
||||
logger.warn("Failed to send IRC marker_remove", ex);
|
||||
running = false;
|
||||
closeSocketState();
|
||||
return false;
|
||||
@@ -107,7 +152,7 @@ public final class IrcService {
|
||||
return socket != null && socket.isConnected() && !socket.isClosed() && writer != null;
|
||||
}
|
||||
|
||||
public DisplayLine pollLine() {
|
||||
public IrcEvent pollEvent() {
|
||||
return queue.poll();
|
||||
}
|
||||
|
||||
@@ -124,14 +169,13 @@ public final class IrcService {
|
||||
while (running && (line = activeReader.readLine()) != null) {
|
||||
handleIncomingLine(line);
|
||||
}
|
||||
|
||||
if (running) {
|
||||
enqueueSystem("Server closed the connection.");
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
} catch (IOException ex) {
|
||||
if (running) {
|
||||
enqueueSystem("Disconnected: " + exception.getMessage());
|
||||
logger.warn("IRC reader stopped", exception);
|
||||
enqueueSystem("Disconnected: " + ex.getMessage());
|
||||
logger.warn("IRC reader stopped", ex);
|
||||
}
|
||||
} finally {
|
||||
running = false;
|
||||
@@ -148,25 +192,39 @@ public final class IrcService {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("chat".equals(message.type)) {
|
||||
String clientName = safeValue(message.client, "UNKNOWN-CLIENT");
|
||||
String playerName = safeValue(message.player, "unknown");
|
||||
String text = safeValue(message.message, "");
|
||||
queue.add(new DisplayLine("[IRC] " + clientName + " >> " + playerName + ": " + text, false));
|
||||
return;
|
||||
switch (message.type) {
|
||||
case "chat" -> {
|
||||
String clientName = safeValue(message.client, "UNKNOWN-CLIENT");
|
||||
String sender = safeValue(message.player, "unknown");
|
||||
String text = safeValue(message.message, "");
|
||||
queue.add(new IrcEvent.Chat(clientName, sender, text));
|
||||
}
|
||||
case "system" -> {
|
||||
if (message.message != null && !message.message.isBlank()) {
|
||||
queue.add(new IrcEvent.System(message.message));
|
||||
}
|
||||
}
|
||||
case "marker_set" -> {
|
||||
if (message.x != null && message.y != null && message.z != null && message.dimensionId != null) {
|
||||
String sender = safeValue(message.player, "unknown");
|
||||
queue.add(new IrcEvent.MarkerSet(sender, message.dimensionId, message.x, message.y, message.z));
|
||||
}
|
||||
}
|
||||
case "marker_remove" -> {
|
||||
String sender = safeValue(message.player, "unknown");
|
||||
queue.add(new IrcEvent.MarkerRemove(sender));
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
if ("system".equals(message.type) && message.message != null && !message.message.isBlank()) {
|
||||
enqueueSystem(message.message);
|
||||
}
|
||||
} catch (JsonParseException exception) {
|
||||
} catch (JsonParseException ex) {
|
||||
enqueueSystem("Malformed packet: " + rawLine);
|
||||
logger.warn("Failed to parse IRC packet: {}", rawLine, exception);
|
||||
logger.warn("Failed to parse IRC packet: {}", rawLine, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void enqueueSystem(String text) {
|
||||
queue.add(new DisplayLine("[IRC] " + text, true));
|
||||
queue.add(new IrcEvent.System(text));
|
||||
}
|
||||
|
||||
private void sendInternal(IrcMessage message) throws IOException {
|
||||
@@ -189,7 +247,6 @@ public final class IrcService {
|
||||
if (closeable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (IOException ignored) {
|
||||
@@ -199,8 +256,4 @@ public final class IrcService {
|
||||
private static String safeValue(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
public record DisplayLine(String text, boolean system) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"ru.nevetime.clientirc.ClientIrcMod"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
{ "config": "clientirc.client.mixins.json", "environment": "client" }
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.19.3",
|
||||
"minecraft": "26.2",
|
||||
|
||||
Reference in New Issue
Block a user