Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b918b79fd5 | |||
| db20575681 | |||
| 1bbc10c9a9 | |||
| b6ce51dfb9 | |||
| 3b5e555ce4 | |||
| 74bcd64bc0 | |||
| 6f75e29ac6 | |||
| be277c08c1 | |||
| fb9218459b | |||
| c25e394d50 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,6 @@ run/
|
|||||||
*.iml
|
*.iml
|
||||||
.idea/
|
.idea/
|
||||||
*.class
|
*.class
|
||||||
|
.primer_*.md
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ minecraft_version=26.2
|
|||||||
loader_version=0.19.3
|
loader_version=0.19.3
|
||||||
fabric_version=0.152.2+26.2
|
fabric_version=0.152.2+26.2
|
||||||
|
|
||||||
mod_version=1.0.0
|
mod_version=1.0.3
|
||||||
maven_group=ru.nevetime
|
maven_group=ru.nevetime
|
||||||
archives_base_name=client-irc-chat
|
archives_base_name=client-irc-chat
|
||||||
|
|||||||
@@ -106,6 +106,46 @@ const server = net.createServer((socket) => {
|
|||||||
continue;
|
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}`));
|
sendJson(socket, systemMessage(`Unknown packet type: ${payload.type}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package ru.nevetime.clientirc;
|
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.api.ClientModInitializer;
|
||||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
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.ChatFormatting;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.chat.MutableComponent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
public final class ClientIrcMod implements ClientModInitializer {
|
public final class ClientIrcMod implements ClientModInitializer {
|
||||||
public static final String MOD_ID = "clientirc";
|
public static final String MOD_ID = "clientirc";
|
||||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
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 -> {
|
private static final ExecutorService IO_EXECUTOR = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
Thread thread = new Thread(runnable, "clientirc-io");
|
Thread thread = new Thread(runnable, "clientirc-io");
|
||||||
@@ -31,9 +34,15 @@ public final class ClientIrcMod implements ClientModInitializer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitializeClient() {
|
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);
|
service = new IrcService(config, LOGGER);
|
||||||
|
|
||||||
|
OverlayHud.init();
|
||||||
|
WorldOverlayRenderer.init();
|
||||||
|
MiddleClickHandler.init(() -> service, () -> resolvePlayerName(Minecraft.getInstance()));
|
||||||
|
|
||||||
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
|
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
|
||||||
if (config.autoConnect) {
|
if (config.autoConnect) {
|
||||||
String playerName = resolvePlayerName(client);
|
String playerName = resolvePlayerName(client);
|
||||||
@@ -47,16 +56,45 @@ public final class ClientIrcMod implements ClientModInitializer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||||
IrcService.DisplayLine line;
|
IrcService.IrcEvent event;
|
||||||
while ((line = service.pollLine()) != null) {
|
while ((event = service.pollEvent()) != null) {
|
||||||
ChatFormatting color = line.system() ? ChatFormatting.GRAY : ChatFormatting.AQUA;
|
handleEvent(client, event);
|
||||||
pushClientChat(client, Component.literal(line.text()).withStyle(color));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ClientSendMessageEvents.ALLOW_CHAT.register(message -> !interceptOutgoingChat(message));
|
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()
|
||||||
|
));
|
||||||
|
String coords = String.format(java.util.Locale.ROOT, "%.0f, %.0f, %.0f", ms.x(), ms.y(), ms.z());
|
||||||
|
pushClientChat(client, Component.literal("[IRC] marker from " + ms.sender() + " @ " + coords).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) {
|
private static boolean interceptOutgoingChat(String rawMessage) {
|
||||||
String trimmed = rawMessage.trim();
|
String trimmed = rawMessage.trim();
|
||||||
if (!trimmed.startsWith(COMMAND_PREFIX)) {
|
if (!trimmed.startsWith(COMMAND_PREFIX)) {
|
||||||
@@ -64,20 +102,34 @@ public final class ClientIrcMod implements ClientModInitializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Minecraft client = Minecraft.getInstance();
|
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));
|
pushClientChat(client, Component.literal("[IRC] Usage: .irc <message> | .irc status | .irc reconnect | .irc reload").withStyle(ChatFormatting.YELLOW));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
String lower = rest.toLowerCase(Locale.ROOT);
|
||||||
if ("status".equalsIgnoreCase(argument)) {
|
if ("status".equals(lower)) {
|
||||||
String status = service.isConnected() ? "connected" : "disconnected";
|
String status = service.isConnected() ? "connected" : "disconnected";
|
||||||
pushClientChat(client, Component.literal("[IRC] " + status + " | " + config.host + ":" + config.port + " | clientName=" + config.clientName).withStyle(ChatFormatting.YELLOW));
|
pushClientChat(client, Component.literal("[IRC] " + status + " | " + config.host + ":" + config.port + " | clientName=" + config.clientName).withStyle(ChatFormatting.YELLOW));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if ("reconnect".equals(lower)) {
|
||||||
if ("reconnect".equalsIgnoreCase(argument)) {
|
|
||||||
String playerName = resolvePlayerName(client);
|
String playerName = resolvePlayerName(client);
|
||||||
IO_EXECUTOR.execute(() -> {
|
IO_EXECUTOR.execute(() -> {
|
||||||
boolean connected = service.reconnect(playerName);
|
boolean connected = service.reconnect(playerName);
|
||||||
@@ -86,27 +138,87 @@ public final class ClientIrcMod implements ClientModInitializer {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if ("reload".equals(lower)) {
|
||||||
if ("reload".equalsIgnoreCase(argument)) {
|
|
||||||
config = IrcConfig.load(FabricLoader.getInstance().getConfigDir().resolve("clientirc.json"));
|
config = IrcConfig.load(FabricLoader.getInstance().getConfigDir().resolve("clientirc.json"));
|
||||||
service.updateConfig(config);
|
service.updateConfig(config);
|
||||||
pushClientChat(client, Component.literal("[IRC] config reloaded").withStyle(ChatFormatting.YELLOW));
|
pushClientChat(client, Component.literal("[IRC] config reloaded").withStyle(ChatFormatting.YELLOW));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String playerName = resolvePlayerName(client);
|
String playerName = resolvePlayerName(client);
|
||||||
IO_EXECUTOR.execute(() -> {
|
IO_EXECUTOR.execute(() -> {
|
||||||
if (!service.ensureConnected(playerName)) {
|
if (!service.ensureConnected(playerName)) {
|
||||||
pushClientChat(client, Component.literal("[IRC] server is offline or config is wrong").withStyle(ChatFormatting.RED));
|
pushClientChat(client, Component.literal("[IRC] server is offline or config is wrong").withStyle(ChatFormatting.RED));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!service.sendChat(playerName, argument)) {
|
if (!service.sendChat(playerName, rest)) {
|
||||||
pushClientChat(client, Component.literal("[IRC] send failed").withStyle(ChatFormatting.RED));
|
pushClientChat(client, Component.literal("[IRC] send failed").withStyle(ChatFormatting.RED));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
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) {
|
private static void pushClientChat(Minecraft client, Component text) {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return;
|
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,71 @@
|
|||||||
|
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.keymapping.v1.KeyMappingHelper;
|
||||||
|
import net.minecraft.client.KeyMapping;
|
||||||
|
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 = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||||
|
"key.clientirc.marker",
|
||||||
|
InputConstants.Type.MOUSE,
|
||||||
|
GLFW.GLFW_MOUSE_BUTTON_MIDDLE,
|
||||||
|
KeyMapping.Category.MISC
|
||||||
|
));
|
||||||
|
|
||||||
|
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) {
|
||||||
|
var bp = br.getBlockPos();
|
||||||
|
target = new Vec3(bp.getX() + 0.5, bp.getY() + 0.5, bp.getZ() + 0.5);
|
||||||
|
} 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().identifier().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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/client/java/ru/nevetime/clientirc/OverlayHud.java
Normal file
103
src/client/java/ru/nevetime/clientirc/OverlayHud.java
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package ru.nevetime.clientirc;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElement;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements;
|
||||||
|
import net.minecraft.client.DeltaTracker;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.Font;
|
||||||
|
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||||
|
import net.minecraft.client.multiplayer.PlayerInfo;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
public final class OverlayHud {
|
||||||
|
private static final Identifier COMPASS_LAYER = Identifier.fromNamespaceAndPath("clientirc", "compass");
|
||||||
|
private static final Identifier WATERMARK_LAYER = Identifier.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() {
|
||||||
|
HudElementRegistry.attachElementAfter(
|
||||||
|
VanillaHudElements.MISC_OVERLAYS, COMPASS_LAYER, (HudElement) OverlayHud::renderCompass);
|
||||||
|
HudElementRegistry.attachElementAfter(
|
||||||
|
VanillaHudElements.MISC_OVERLAYS, WATERMARK_LAYER, (HudElement) OverlayHud::renderWatermark);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderCompass(GuiGraphicsExtractor graphics, DeltaTracker deltaTracker) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc.level == null) 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.mainCamera().yRot();
|
||||||
|
|
||||||
|
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.text(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(GuiGraphicsExtractor graphics, DeltaTracker deltaTracker) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc.level == null) 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.text(font, nick, x, y, 0xFFFFFFFF, true);
|
||||||
|
graphics.text(font, "FPS: " + fps, x, y + lineHeight, 0xFFFFFFFF, true);
|
||||||
|
graphics.text(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package ru.nevetime.clientirc;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.renderer.OrderedSubmitNodeCollector;
|
||||||
|
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||||
|
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraft.world.phys.shapes.Shapes;
|
||||||
|
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||||
|
|
||||||
|
public final class WorldOverlayRenderer {
|
||||||
|
private static final int FRIEND_COLOR = 0xFF33FF55;
|
||||||
|
private static final int MARKER_COLOR = 0xFFFF55FF;
|
||||||
|
private static final float OUTLINE_WIDTH = 2.0f;
|
||||||
|
private static final double BEAM_HEIGHT = 50.0;
|
||||||
|
|
||||||
|
private WorldOverlayRenderer() {}
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
LevelRenderEvents.AFTER_TRANSLUCENT_FEATURES.register(WorldOverlayRenderer::onRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onRender(LevelRenderContext context) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc.level == null || mc.player == null) return;
|
||||||
|
|
||||||
|
SubmitNodeCollector base = context.submitNodeCollector();
|
||||||
|
if (!(base instanceof OrderedSubmitNodeCollector collector)) return;
|
||||||
|
|
||||||
|
PoseStack pose = context.poseStack();
|
||||||
|
Vec3 camPos = mc.gameRenderer.mainCamera().position();
|
||||||
|
double cx = camPos.x;
|
||||||
|
double cy = camPos.y;
|
||||||
|
double cz = camPos.z;
|
||||||
|
|
||||||
|
for (Player player : mc.level.players()) {
|
||||||
|
if (player == mc.player) continue;
|
||||||
|
GameProfile profile = player.getGameProfile();
|
||||||
|
if (profile == null) continue;
|
||||||
|
if (!FriendsManager.isFriend(profile.name())) continue;
|
||||||
|
|
||||||
|
double px = player.getX() - cx;
|
||||||
|
double py = player.getY() + player.getBbHeight() + 0.3 - cy;
|
||||||
|
double pz = player.getZ() - cz;
|
||||||
|
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(px - 0.15, py - 0.15, pz - 0.15);
|
||||||
|
VoxelShape dot = Shapes.box(0.0, 0.0, 0.0, 0.3, 0.3, 0.3);
|
||||||
|
collector.submitShapeOutline(pose, dot, RenderTypes.lines(), FRIEND_COLOR, OUTLINE_WIDTH, false);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentDim = mc.level.dimension().identifier().toString();
|
||||||
|
for (MarkerManager.Marker marker : MarkerManager.active()) {
|
||||||
|
if (!currentDim.equals(marker.dimensionId())) continue;
|
||||||
|
|
||||||
|
double mx = marker.x() - cx;
|
||||||
|
double my = marker.y() - cy;
|
||||||
|
double mz = marker.z() - cz;
|
||||||
|
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(mx - 0.5, my, mz - 0.5);
|
||||||
|
collector.submitShapeOutline(pose, Shapes.block(), RenderTypes.lines(), MARKER_COLOR, OUTLINE_WIDTH, false);
|
||||||
|
pose.popPose();
|
||||||
|
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(mx - 0.05, my, mz - 0.05);
|
||||||
|
VoxelShape beam = Shapes.box(0.0, 0.0, 0.0, 0.1, BEAM_HEIGHT, 0.1);
|
||||||
|
collector.submitShapeOutline(pose, beam, RenderTypes.lines(), MARKER_COLOR, OUTLINE_WIDTH, false);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package ru.nevetime.clientirc.mixin;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.components.EditBox;
|
||||||
|
import net.minecraft.client.gui.screens.ChatScreen;
|
||||||
|
import net.minecraft.client.input.KeyEvent;
|
||||||
|
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(KeyEvent event, CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
if (event.key() != 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,10 @@ public final class IrcConfig {
|
|||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(IrcConfig.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(IrcConfig.class);
|
||||||
|
|
||||||
public String host = "127.0.0.1";
|
public String host = "2.26.99.24";
|
||||||
public int port = 24469;
|
public int port = 24469;
|
||||||
public String clientName = "MY-CLIENT";
|
public String clientName = "CLIENT";
|
||||||
public String token = "";
|
public String token = "0etnVzgvmZzHj8H09X8xO6gf8L7qN9DZktYuSTwg";
|
||||||
public boolean autoConnect = true;
|
public boolean autoConnect = true;
|
||||||
public int connectTimeoutMs = 5000;
|
public int connectTimeoutMs = 5000;
|
||||||
|
|
||||||
|
|||||||
@@ -6,27 +6,55 @@ public final class IrcMessage {
|
|||||||
public String player;
|
public String player;
|
||||||
public String message;
|
public String message;
|
||||||
public String token;
|
public String token;
|
||||||
|
public String dimensionId;
|
||||||
|
public Double x;
|
||||||
|
public Double y;
|
||||||
|
public Double z;
|
||||||
public long timestamp;
|
public long timestamp;
|
||||||
|
|
||||||
public static IrcMessage hello(String clientName, String playerName, String token) {
|
public static IrcMessage hello(String clientName, String playerName, String token) {
|
||||||
IrcMessage message = new IrcMessage();
|
IrcMessage m = new IrcMessage();
|
||||||
message.type = "hello";
|
m.type = "hello";
|
||||||
message.client = clientName;
|
m.client = clientName;
|
||||||
message.player = playerName;
|
m.player = playerName;
|
||||||
message.token = token;
|
m.token = token;
|
||||||
message.timestamp = System.currentTimeMillis();
|
m.timestamp = System.currentTimeMillis();
|
||||||
return message;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IrcMessage chat(String clientName, String playerName, String text, String token) {
|
public static IrcMessage chat(String clientName, String playerName, String text, String token) {
|
||||||
IrcMessage message = new IrcMessage();
|
IrcMessage m = new IrcMessage();
|
||||||
message.type = "chat";
|
m.type = "chat";
|
||||||
message.client = clientName;
|
m.client = clientName;
|
||||||
message.player = playerName;
|
m.player = playerName;
|
||||||
message.message = text;
|
m.message = text;
|
||||||
message.token = token;
|
m.token = token;
|
||||||
message.timestamp = System.currentTimeMillis();
|
m.timestamp = System.currentTimeMillis();
|
||||||
return message;
|
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 static final Gson GSON = new Gson();
|
||||||
|
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
private final ConcurrentLinkedQueue<DisplayLine> queue = new ConcurrentLinkedQueue<>();
|
private final ConcurrentLinkedQueue<IrcEvent> queue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
private volatile IrcConfig config;
|
private volatile IrcConfig config;
|
||||||
private volatile Socket socket;
|
private volatile Socket socket;
|
||||||
@@ -33,6 +33,20 @@ public final class IrcService {
|
|||||||
this.logger = Objects.requireNonNull(logger, "logger");
|
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) {
|
public synchronized void updateConfig(IrcConfig config) {
|
||||||
this.config = Objects.requireNonNull(config, "config");
|
this.config = Objects.requireNonNull(config, "config");
|
||||||
}
|
}
|
||||||
@@ -66,12 +80,12 @@ public final class IrcService {
|
|||||||
sendInternal(IrcMessage.hello(config.clientName, playerName, config.token));
|
sendInternal(IrcMessage.hello(config.clientName, playerName, config.token));
|
||||||
enqueueSystem("Connected to " + config.host + ":" + config.port);
|
enqueueSystem("Connected to " + config.host + ":" + config.port);
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException exception) {
|
} catch (IOException ex) {
|
||||||
closeQuietly(newReader);
|
closeQuietly(newReader);
|
||||||
closeQuietly(newWriter);
|
closeQuietly(newWriter);
|
||||||
closeQuietly(newSocket);
|
closeQuietly(newSocket);
|
||||||
enqueueSystem("Connection failed: " + exception.getMessage());
|
enqueueSystem("Connection failed: " + ex.getMessage());
|
||||||
logger.warn("IRC connection failed", exception);
|
logger.warn("IRC connection failed", ex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,13 +104,44 @@ public final class IrcService {
|
|||||||
if (!isConnected()) {
|
if (!isConnected()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendInternal(IrcMessage.chat(config.clientName, playerName, text, config.token));
|
sendInternal(IrcMessage.chat(config.clientName, playerName, text, config.token));
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException exception) {
|
} catch (IOException ex) {
|
||||||
enqueueSystem("Send failed: " + exception.getMessage());
|
enqueueSystem("Send failed: " + ex.getMessage());
|
||||||
logger.warn("Failed to send IRC message", exception);
|
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;
|
running = false;
|
||||||
closeSocketState();
|
closeSocketState();
|
||||||
return false;
|
return false;
|
||||||
@@ -107,7 +152,7 @@ public final class IrcService {
|
|||||||
return socket != null && socket.isConnected() && !socket.isClosed() && writer != null;
|
return socket != null && socket.isConnected() && !socket.isClosed() && writer != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DisplayLine pollLine() {
|
public IrcEvent pollEvent() {
|
||||||
return queue.poll();
|
return queue.poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,14 +169,13 @@ public final class IrcService {
|
|||||||
while (running && (line = activeReader.readLine()) != null) {
|
while (running && (line = activeReader.readLine()) != null) {
|
||||||
handleIncomingLine(line);
|
handleIncomingLine(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (running) {
|
if (running) {
|
||||||
enqueueSystem("Server closed the connection.");
|
enqueueSystem("Server closed the connection.");
|
||||||
}
|
}
|
||||||
} catch (IOException exception) {
|
} catch (IOException ex) {
|
||||||
if (running) {
|
if (running) {
|
||||||
enqueueSystem("Disconnected: " + exception.getMessage());
|
enqueueSystem("Disconnected: " + ex.getMessage());
|
||||||
logger.warn("IRC reader stopped", exception);
|
logger.warn("IRC reader stopped", ex);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
running = false;
|
running = false;
|
||||||
@@ -148,25 +192,39 @@ public final class IrcService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("chat".equals(message.type)) {
|
switch (message.type) {
|
||||||
|
case "chat" -> {
|
||||||
String clientName = safeValue(message.client, "UNKNOWN-CLIENT");
|
String clientName = safeValue(message.client, "UNKNOWN-CLIENT");
|
||||||
String playerName = safeValue(message.player, "unknown");
|
String sender = safeValue(message.player, "unknown");
|
||||||
String text = safeValue(message.message, "");
|
String text = safeValue(message.message, "");
|
||||||
queue.add(new DisplayLine("[IRC] " + clientName + " >> " + playerName + ": " + text, false));
|
queue.add(new IrcEvent.Chat(clientName, sender, text));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
case "system" -> {
|
||||||
if ("system".equals(message.type) && message.message != null && !message.message.isBlank()) {
|
if (message.message != null && !message.message.isBlank()) {
|
||||||
enqueueSystem(message.message);
|
queue.add(new IrcEvent.System(message.message));
|
||||||
}
|
}
|
||||||
} catch (JsonParseException exception) {
|
}
|
||||||
|
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 -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (JsonParseException ex) {
|
||||||
enqueueSystem("Malformed packet: " + rawLine);
|
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) {
|
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 {
|
private void sendInternal(IrcMessage message) throws IOException {
|
||||||
@@ -189,7 +247,6 @@ public final class IrcService {
|
|||||||
if (closeable == null) {
|
if (closeable == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
closeable.close();
|
closeable.close();
|
||||||
} catch (IOException ignored) {
|
} catch (IOException ignored) {
|
||||||
@@ -199,8 +256,4 @@ public final class IrcService {
|
|||||||
private static String safeValue(String value, String fallback) {
|
private static String safeValue(String value, String fallback) {
|
||||||
return value == null || value.isBlank() ? fallback : value;
|
return value == null || value.isBlank() ? fallback : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record DisplayLine(String text, boolean system) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"ru.nevetime.clientirc.ClientIrcMod"
|
"ru.nevetime.clientirc.ClientIrcMod"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"mixins": [
|
||||||
|
{ "config": "clientirc.client.mixins.json", "environment": "client" }
|
||||||
|
],
|
||||||
"depends": {
|
"depends": {
|
||||||
"fabricloader": ">=0.19.3",
|
"fabricloader": ">=0.19.3",
|
||||||
"minecraft": "26.2",
|
"minecraft": "26.2",
|
||||||
|
|||||||
1
закомить_чтонибудь_Ионо_сбилдит.txt
Normal file
1
закомить_чтонибудь_Ионо_сбилдит.txt
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user