Compare commits
14 Commits
41b68d45c5
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f75e29ac6 | |||
| be277c08c1 | |||
| fb9218459b | |||
| c25e394d50 | |||
| 0112bde962 | |||
| b3f24d9828 | |||
| 0e6377fce1 | |||
| b0501acfcd | |||
| 5f366792c3 | |||
| 8a969bc843 | |||
| 71dc665ef1 | |||
| fb233cee93 | |||
| 77a44be34b | |||
| 153d85602c |
16
.drone.yml
16
.drone.yml
@@ -6,6 +6,7 @@ trigger:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
- tag
|
||||
|
||||
steps:
|
||||
- name: gradle-build
|
||||
@@ -15,3 +16,18 @@ steps:
|
||||
commands:
|
||||
- gradle --no-daemon clean build
|
||||
- ls -la build/libs
|
||||
|
||||
- name: gitea-release
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_token
|
||||
base_url: https://git.nevetime.ru
|
||||
files:
|
||||
- build/libs/*.jar
|
||||
title: ${DRONE_TAG}
|
||||
checksum:
|
||||
- sha256
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,4 +5,5 @@ run/
|
||||
*.iml
|
||||
.idea/
|
||||
*.class
|
||||
.primer_*.md
|
||||
|
||||
|
||||
25
README.md
25
README.md
@@ -30,7 +30,7 @@
|
||||
|
||||
Дата релиза `Minecraft 26.2`: `2026-06-16`.
|
||||
|
||||
Для этой версии я использую official Mojang mappings через Loom, потому что стандартный Yarn endpoint для `26.2` на момент правки ещё не отдавал mappings.
|
||||
Начиная с `26.1` Mojang перестал обфусцировать клиент, а Yarn и Intermediary перестали публиковаться после `1.21.11`. Поэтому в `build.gradle` нет блока `mappings` вообще: исходники Minecraft уже в человекочитаемых именах Mojang, и Loom собирает мод напрямую против них.
|
||||
|
||||
Если в CI стоит старый Loom вроде `1.10.5`, сборка `26.2` упадёт с ошибкой `Unsupported class file major version 69`. Для `26.2` нужен новый Loom из актуальной ветки Fabric.
|
||||
|
||||
@@ -144,6 +144,27 @@ gradle build
|
||||
|
||||
- запуск Docker pipeline;
|
||||
- `gradle clean build`;
|
||||
- вывод содержимого `build/libs`.
|
||||
- вывод содержимого `build/libs`;
|
||||
- по тегу — публикует jar-ы из `build/libs/*.jar` как Gitea Release.
|
||||
|
||||
Для работы Drone runner должен иметь доступ в интернет, чтобы скачать зависимости Gradle/Fabric.
|
||||
|
||||
### Автоматический релиз по тегу
|
||||
|
||||
Чтобы Drone выложил собранный мод в раздел Releases репозитория, нужны две разовые настройки и одна команда на каждую новую версию.
|
||||
|
||||
**Один раз:**
|
||||
|
||||
1. В Gitea: `Settings → Applications → Generate New Token`. Дать токену права `write:repository`. Скопировать.
|
||||
2. В Drone, на странице репозитория: `Settings → Secrets → New Secret`. Имя — `gitea_token`, значение — токен из шага 1. Поставить галку `Allow Pull Requests` снять (релизы только из push/tag).
|
||||
|
||||
**На каждую новую версию:**
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
Drone поднимет тег как событие, соберёт мод и положит `client-irc-chat-<версия>.jar` в `https://git.nevetime.ru/Arkon/fdgdfg/releases/tag/v1.0.0`. Версия в имени jar-а берётся из `mod_version` в `gradle.properties` — обычно её стоит поднимать вместе с тегом, чтобы они совпадали.
|
||||
|
||||
Если что-то идёт не так, посмотреть лог конкретного шага `gitea-release` в Drone — плагин печатает HTTP-ответ Gitea, по нему сразу видно, проблема в токене, URL или путях к файлам.
|
||||
|
||||
@@ -6,8 +6,7 @@ org.gradle.configuration-cache=false
|
||||
minecraft_version=26.2
|
||||
loader_version=0.19.3
|
||||
fabric_version=0.152.2+26.2
|
||||
loom_version=1.17-SNAPSHOT
|
||||
|
||||
mod_version=1.0.0
|
||||
mod_version=1.0.2
|
||||
maven_group=ru.nevetime
|
||||
archives_base_name=client-irc-chat
|
||||
|
||||
@@ -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,6 +13,7 @@ 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;
|
||||
|
||||
@@ -15,33 +21,78 @@ 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");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
private static IrcConfig config;
|
||||
private static IrcService service;
|
||||
|
||||
@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);
|
||||
|
||||
MiddleClickHandler.init(() -> service, () -> resolvePlayerName(Minecraft.getInstance()));
|
||||
|
||||
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
|
||||
if (config.autoConnect) {
|
||||
service.connect(resolvePlayerName(client));
|
||||
String playerName = resolvePlayerName(client);
|
||||
IO_EXECUTOR.execute(() -> service.connect(playerName));
|
||||
}
|
||||
});
|
||||
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> {
|
||||
service.disconnect();
|
||||
IO_EXECUTOR.shutdownNow();
|
||||
});
|
||||
|
||||
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()
|
||||
));
|
||||
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) {
|
||||
String trimmed = rawMessage.trim();
|
||||
if (!trimmed.startsWith(COMMAND_PREFIX)) {
|
||||
@@ -49,59 +100,139 @@ 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)) {
|
||||
boolean connected = service.reconnect(resolvePlayerName(client));
|
||||
ChatFormatting color = connected ? ChatFormatting.GREEN : ChatFormatting.RED;
|
||||
pushClientChat(client, Component.literal("[IRC] reconnect " + (connected ? "ok" : "failed")).withStyle(color));
|
||||
if ("reconnect".equals(lower)) {
|
||||
String playerName = resolvePlayerName(client);
|
||||
IO_EXECUTOR.execute(() -> {
|
||||
boolean connected = service.reconnect(playerName);
|
||||
ChatFormatting color = connected ? ChatFormatting.GREEN : ChatFormatting.RED;
|
||||
pushClientChat(client, Component.literal("[IRC] reconnect " + (connected ? "ok" : "failed")).withStyle(color));
|
||||
});
|
||||
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, rest)) {
|
||||
pushClientChat(client, Component.literal("[IRC] send failed").withStyle(ChatFormatting.RED));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!service.ensureConnected(resolvePlayerName(client))) {
|
||||
pushClientChat(client, Component.literal("[IRC] server is offline or config is wrong").withStyle(ChatFormatting.RED));
|
||||
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() : "";
|
||||
|
||||
boolean sent = service.sendChat(resolvePlayerName(client), argument);
|
||||
if (!sent) {
|
||||
pushClientChat(client, Component.literal("[IRC] send failed").withStyle(ChatFormatting.RED));
|
||||
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;
|
||||
}
|
||||
client.execute(() -> {
|
||||
if (client.gui != null && client.gui.getChat() != null) {
|
||||
client.gui.getChat().addMessage(text);
|
||||
if (client.player != null) {
|
||||
client.player.sendSystemMessage(text);
|
||||
} else {
|
||||
LOGGER.info(text.getString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String resolvePlayerName(Minecraft client) {
|
||||
if (client != null && client.player != null) {
|
||||
return client.player.getGameProfile().getName();
|
||||
return client.player.getGameProfile().name();
|
||||
}
|
||||
if (client != null && client.getUser() != null) {
|
||||
return client.getUser().getName();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,10 @@ public final class IrcConfig {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
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 String clientName = "MY-CLIENT";
|
||||
public String token = "";
|
||||
public String clientName = "CLIENT";
|
||||
public String token = "0etnVzgvmZzHj8H09X8xO6gf8L7qN9DZktYuSTwg";
|
||||
public boolean autoConnect = true;
|
||||
public int connectTimeoutMs = 5000;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
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