Author SHA1 Message Date
john b43549fe1c remove: fakeleave/fakejoin commands and leave 2026-08-19 12:17:38 +02:00
john 1ac3ab0a72 feat: spectator tab fix, fakejoin/fakeleave commands with LP permissions
- PlayerTabOverlayMixin: remove italic and gray color for spectators in tab
  (inject getNameForDisplay + redirect text() color in extractRenderState)
- FakeLeaveCommand/FakeJoinCommand: admin-only via op or LuckPerms
  (msnexus.command.fakeleave/fakejoin), hides/shows player from tab
- LeaveMessageMixin: redirect to suppress leave message for fake-left players
  (was @Inject cancel which broke entire disconnect flow)
- Fix ChatHandler: add missing imports (Style, ClickEvent, HoverEvent),
  extract getLuckPermsPrefix method, fix Localchat->MSNexus references
- Fix fabric.mod.json typo (stray 'gi')
2026-08-16 20:46:18 +02:00
13 changed files with 148 additions and 287 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ loom {
splitEnvironmentSourceSets() splitEnvironmentSourceSets()
mods { mods {
"msnexus" { "localchat" {
sourceSet sourceSets.main sourceSet sourceSets.main
sourceSet sourceSets.client sourceSet sourceSets.client
} }
+1 -1
View File
@@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx2G
minecraft_version=26.1.2 minecraft_version=26.1.2
yarn_mappings=1.21.11+build.6 yarn_mappings=1.21.11+build.6
loader_version=0.19.2 loader_version=0.19.2
mod_version=1.1 mod_version=1.0
maven_group=org.example1 maven_group=org.example1
archives_base_name=msnexus archives_base_name=msnexus
fabric_version=0.155.2+26.1.2 fabric_version=0.155.2+26.1.2
@@ -0,0 +1,49 @@
package org.examp.john.mixin.client;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.PlayerTabOverlay;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.GameType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Прибирає курсив та сірий колір для spectator у таб-листі.
*/
@Mixin(PlayerTabOverlay.class)
public class PlayerTabOverlayMixin {
/**
* Прибирає курсив з назви spectator в табі.
*/
@Inject(method = "getNameForDisplay", at = @At("RETURN"), cancellable = true)
private void removeSpectatorItalic(PlayerInfo playerInfo, CallbackInfoReturnable<Component> cir) {
if (playerInfo.getGameMode() == GameType.SPECTATOR) {
Component original = cir.getReturnValue();
cir.setReturnValue(original.copy().withStyle(original.getStyle().withItalic(false)));
}
}
/**
* Перехоплюємо виклик text(Font, Component, x, y, color) в extractRenderState.
* Якщо колір = -1862270977 (сірий для spectator), замінюємо на -1 (білий).
*/
@Redirect(
method = "extractRenderState",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;text(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;III)V"
)
)
private void forceWhiteText(GuiGraphicsExtractor instance, Font font, Component text, int x, int y, int color) {
if (color == -1862270977) {
color = -1; // білий
}
instance.text(font, text, x, y, color);
}
}
@@ -4,6 +4,7 @@
"package": "org.examp.john.mixin.client", "package": "org.examp.john.mixin.client",
"compatibilityLevel": "JAVA_25", "compatibilityLevel": "JAVA_25",
"client": [ "client": [
"PlayerTabOverlayMixin"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1
+1 -18
View File
@@ -1,6 +1,5 @@
package org.examp.john; package org.examp.john;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import java.util.ArrayList; import java.util.ArrayList;
@@ -21,21 +20,12 @@ import java.util.UUID;
*/ */
public class AntiAbuse { public class AntiAbuse {
private static final int REPEAT_THRESHOLD = 5; private static final int REPEAT_THRESHOLD = 5;
private static final double DISTANCE_THRESHOLD = 50.0; private static final double DISTANCE_THRESHOLD = 50.0;
private static final long TIME_WINDOW_MS = 5 * 60 * 1000; private static final long TIME_WINDOW_MS = 5 * 60 * 1000;
private static final long ALERT_COOLDOWN_MS = 30 * 1000;
private static final Map<UUID, List<MessageRecord>> history = new HashMap<>(); private static final Map<UUID, List<MessageRecord>> history = new HashMap<>();
private static final Map<UUID, Long> lastAlertTime = new HashMap<>();
public static void register() {
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {
UUID uuid = handler.player.getUUID();
history.remove(uuid);
lastAlertTime.remove(uuid);
});
}
public static void recordMessage(ServerPlayer player, String message) { public static void recordMessage(ServerPlayer player, String message) {
UUID uuid = player.getUUID(); UUID uuid = player.getUUID();
@@ -53,7 +43,6 @@ public class AntiAbuse {
} }
private static void checkForAbuse(ServerPlayer player, List<MessageRecord> records) { private static void checkForAbuse(ServerPlayer player, List<MessageRecord> records) {
UUID uuid = player.getUUID();
Map<String, int[]> counts = new HashMap<>(); Map<String, int[]> counts = new HashMap<>();
Map<String, double[]> bounds = new HashMap<>(); Map<String, double[]> bounds = new HashMap<>();
@@ -77,12 +66,6 @@ public class AntiAbuse {
double spread = Math.max(b[1] - b[0], b[3] - b[2]); double spread = Math.max(b[1] - b[0], b[3] - b[2]);
if (spread > DISTANCE_THRESHOLD) { if (spread > DISTANCE_THRESHOLD) {
long now = System.currentTimeMillis();
Long lastAlert = lastAlertTime.get(uuid);
if (lastAlert != null && now - lastAlert < ALERT_COOLDOWN_MS) {
continue;
}
lastAlertTime.put(uuid, now);
MSNexus.magicModLog.warn( MSNexus.magicModLog.warn(
"[ANTI-ABUSE] Гравець {} ймовірно шукає інших: " "[ANTI-ABUSE] Гравець {} ймовірно шукає інших: "
+ "'{}' ×{} разів, переміщення ~{} блоків", + "'{}' ×{} разів, переміщення ~{} блоків",
+75 -174
View File
@@ -17,126 +17,77 @@ import org.examp.john.stealth.StealthStorage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale;
public class ChatHandler { public class ChatHandler {
public static void register() { public static void register() {
ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> {
try {
return handleMessage(message.signedContent(), sender);
} catch (Exception e) {
MSNexus.magicModLog.error(
"[CHAT] Не вдалося обробити повідомлення від {}", sender.getName().getString(), e);
return false;
}
});
}
private static boolean handleMessage(String text, ServerPlayer sender) {
final String prefix = "!"; final String prefix = "!";
final float LOCAL_CHAT_RADIUS = 100; final float LOCAL_CHAT_RADIUS = 100;
if (text.isBlank()) { ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> {
return false; String text = message.signedContent();
} ServerLevel serverLevel = sender.level();
List<ServerPlayer> players = serverLevel.getServer().getPlayerList().getPlayers();
ServerLevel serverLevel = sender.level(); MutableComponent formattedName = buildFormattedName(sender);
List<ServerPlayer> players = serverLevel.getServer().getPlayerList().getPlayers();
MutableComponent formattedName = buildFormattedName(sender); if (text.startsWith(prefix)) {
String actualMessage = text.substring(1).trim();
if (text.startsWith(prefix)) { Component globalMessage = Component.empty()
String actualMessage = text.substring(1).trim();
if (actualMessage.isEmpty()) {
return false;
}
Component globalMessage = Component.empty()
.append(formattedName)
.append(Component.literal(": "))
.append(buildFormattedMessage(actualMessage, players));
for (ServerPlayer player : players) {
player.sendSystemMessage(globalMessage);
}
ChatLogger.logGlobal(sender, actualMessage, players);
} else {
AntiAbuse.recordMessage(sender, text);
if (StealthStorage.isEnabled(sender.getUUID())) {
sender.sendSystemMessage(
Component.literal("Вас ніхто не почув...")
.withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW)
);
ChatLogger.logUnheard(sender, text);
return false;
}
List<ServerPlayer> hearers = new ArrayList<>();
for (ServerPlayer player : players) {
if (player != sender
&& player.level() == serverLevel
&& player.distanceTo(sender) <= LOCAL_CHAT_RADIUS) {
hearers.add(player);
}
}
if (!hearers.isEmpty()) {
Component localMessage = Component.empty()
.append(formattedName) .append(formattedName)
.append(Component.literal(": ").withStyle(ChatFormatting.GRAY)) .append(Component.literal(": "))
.append(buildFormattedMessage(text, players).withStyle(ChatFormatting.GRAY)); .append(buildMentionMessage(actualMessage, players));
sender.sendSystemMessage(localMessage); for (ServerPlayer player : players) {
for (ServerPlayer hearer : hearers) { player.sendSystemMessage(globalMessage);
hearer.sendSystemMessage(localMessage);
} }
ChatLogger.logLocal(sender, text, hearers); ChatLogger.logGlobal(sender, actualMessage, players);
} else { } else {
sender.sendSystemMessage( AntiAbuse.recordMessage(sender, text);
Component.literal("Вас ніхто не почув, додайте \"!\" на початок для глобального чату")
.withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW)
);
ChatLogger.logUnheard(sender, text); if (StealthStorage.isEnabled(sender.getUUID())) {
sender.sendSystemMessage(
Component.literal("Вас ніхто не почув...")
.withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW)
);
ChatLogger.logUnheard(sender, text);
return false;
}
List<ServerPlayer> hearers = new ArrayList<>();
for (ServerPlayer player : players) {
if (player != sender
&& player.level() == serverLevel
&& player.distanceTo(sender) <= LOCAL_CHAT_RADIUS) {
hearers.add(player);
}
}
if (!hearers.isEmpty()) {
Component localMessage = Component.empty()
.append(formattedName)
.append(Component.literal(": ").withStyle(ChatFormatting.GRAY))
.append(buildMentionMessage(text, players).withStyle(ChatFormatting.GRAY));
sender.sendSystemMessage(localMessage);
for (ServerPlayer hearer : hearers) {
hearer.sendSystemMessage(localMessage);
}
ChatLogger.logLocal(sender, text, hearers);
} else {
sender.sendSystemMessage(
Component.literal("Вас ніхто не почув, додайте \"!\" на початок для глобального чату")
.withStyle(ChatFormatting.ITALIC, ChatFormatting.YELLOW)
);
ChatLogger.logUnheard(sender, text);
}
} }
} return false;
return false; });
}
/**
* Парсить текст між ** і робить його курсивом, поєднуючи з підсвіткою згадок гравців.
* Приклад: "привіт **світе** як справи?" -> "привіт [курсив]світе[/курсив] як справи?"
*/
private static MutableComponent buildFormattedMessage(String text, List<ServerPlayer> players) {
MutableComponent result = Component.empty();
StringBuilder buffer = new StringBuilder();
boolean italic = false;
for (int i = 0; i < text.length(); i++) {
if (i + 1 < text.length() && text.charAt(i) == '*' && text.charAt(i + 1) == '*') {
appendFormattedSegment(result, buffer.toString(), players, italic);
buffer.setLength(0);
italic = !italic;
i++;
} else {
buffer.append(text.charAt(i));
}
}
appendFormattedSegment(result, buffer.toString(), players, italic);
return result;
}
private static void appendFormattedSegment(MutableComponent result, String segment, List<ServerPlayer> players, boolean italic) {
MutableComponent parsed = buildMentionMessage(segment, players);
if (italic) {
parsed.withStyle(Style.EMPTY.withItalic(true));
}
result.append(parsed);
} }
private static MutableComponent buildMentionMessage(String text, List<ServerPlayer> onlinePlayers) { private static MutableComponent buildMentionMessage(String text, List<ServerPlayer> onlinePlayers) {
@@ -150,11 +101,10 @@ public class ChatHandler {
while (!remaining.isEmpty()) { while (!remaining.isEmpty()) {
int earliestIndex = remaining.length(); int earliestIndex = remaining.length();
ServerPlayer mentioned = null; ServerPlayer mentioned = null;
String lower = remaining.toLowerCase(Locale.ROOT);
for (ServerPlayer player : sorted) { for (ServerPlayer player : sorted) {
String nameLower = player.getName().getString().toLowerCase(Locale.ROOT); String name = player.getName().getString();
int index = indexOfBoundary(lower, nameLower); int index = remaining.toLowerCase().indexOf(name.toLowerCase());
if (index >= 0 && index < earliestIndex) { if (index >= 0 && index < earliestIndex) {
earliestIndex = index; earliestIndex = index;
mentioned = player; mentioned = player;
@@ -166,13 +116,11 @@ public class ChatHandler {
break; break;
} }
boolean withAt = earliestIndex > 0 && remaining.charAt(earliestIndex - 1) == '@'; if (earliestIndex > 0) {
int prefixEnd = withAt ? earliestIndex - 1 : earliestIndex; result.append(Component.literal(remaining.substring(0, earliestIndex)));
if (prefixEnd > 0) {
result.append(Component.literal(remaining.substring(0, prefixEnd)));
} }
result.append(createMentionComponent(mentioned, withAt)); result.append(createMentionComponent(mentioned));
remaining = remaining.substring(earliestIndex + mentioned.getName().getString().length()); remaining = remaining.substring(earliestIndex + mentioned.getName().getString().length());
} }
@@ -180,32 +128,13 @@ public class ChatHandler {
return result; return result;
} }
private static int indexOfBoundary(String lowerHaystack, String lowerNeedle) { private static Component createMentionComponent(ServerPlayer player) {
int index = lowerHaystack.indexOf(lowerNeedle); String lpPrefix = getLuckPermsPrefix(player);
while (index >= 0) { String formattedPrefix = lpPrefix.replace("&", "\u00a7");
boolean okBefore = index == 0 || !isNameChar(lowerHaystack.charAt(index - 1));
boolean okAfter = index + lowerNeedle.length() == lowerHaystack.length()
|| !isNameChar(lowerHaystack.charAt(index + lowerNeedle.length()));
if (okBefore && okAfter) {
return index;
}
index = lowerHaystack.indexOf(lowerNeedle, index + 1);
}
return -1;
}
private static boolean isNameChar(char c) {
return Character.isLetterOrDigit(c) || c == '_';
}
private static Component createMentionComponent(ServerPlayer player, boolean withAt) {
String name = player.getName().getString(); String name = player.getName().getString();
String prefix = formatLegacyPrefix(getLuckPermsPrefix(player));
String text = (withAt ? "@" : "") + prefix + name;
return Component.literal(text) return Component.literal(formattedPrefix + name)
.withStyle(Style.EMPTY .withStyle(Style.EMPTY
.withColor(ChatFormatting.AQUA)
.withClickEvent(new ClickEvent.SuggestCommand("/msg " + name + " ")) .withClickEvent(new ClickEvent.SuggestCommand("/msg " + name + " "))
.withHoverEvent(new HoverEvent.ShowText( .withHoverEvent(new HoverEvent.ShowText(
Component.literal("Натисніть щоб написати " + name) Component.literal("Натисніть щоб написати " + name)
@@ -213,38 +142,18 @@ public class ChatHandler {
); );
} }
private static String formatLegacyPrefix(String raw) { private static MutableComponent buildFormattedName(ServerPlayer player) {
if (raw == null || raw.isEmpty()) { String lpPrefix = getLuckPermsPrefix(player);
return ""; String playerName = player.getName().getString();
} String formattedPrefix = lpPrefix.replace("&", "\u00a7");
String s = raw.replace('&', '§');
StringBuilder out = new StringBuilder(s.length() + 14);
int i = 0;
while (i < s.length()) {
if (s.charAt(i) == '§' && i + 8 <= s.length() && s.charAt(i + 1) == '#'
&& isSixHex(s, i + 2)) {
out.append("§x");
for (int j = 2; j < 8; j++) {
out.append('§').append(s.charAt(i + j));
}
i += 8;
} else {
out.append(s.charAt(i));
i++;
}
}
return out.toString();
}
private static boolean isSixHex(String s, int start) { Style clickStyle = Style.EMPTY
for (int i = start; i < start + 6; i++) { .withClickEvent(new ClickEvent.SuggestCommand("/msg " + playerName + " "))
char c = s.charAt(i); .withHoverEvent(new HoverEvent.ShowText(
boolean hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); Component.literal("Натисніть щоб написати " + playerName)
if (!hex) { .withStyle(ChatFormatting.GRAY)));
return false;
} return Component.literal(formattedPrefix + playerName).withStyle(clickStyle);
}
return true;
} }
private static String getLuckPermsPrefix(ServerPlayer player) { private static String getLuckPermsPrefix(ServerPlayer player) {
@@ -261,12 +170,4 @@ public class ChatHandler {
} }
return ""; return "";
} }
private static MutableComponent buildFormattedName(ServerPlayer player) {
String prefix = formatLegacyPrefix(getLuckPermsPrefix(player));
return Component.empty()
.append(Component.literal(prefix))
.append(player.getDisplayName());
}
} }
+1 -1
View File
@@ -48,7 +48,7 @@ public class ChatLogger {
); );
} }
static String formatPos(ServerPlayer player) { private static String formatPos(ServerPlayer player) {
ServerLevel level = player.level(); ServerLevel level = player.level();
// У ResourceKey дістаємо Identifier/ResourceLocation через .identifier(), // У ResourceKey дістаємо Identifier/ResourceLocation через .identifier(),
@@ -1,21 +0,0 @@
package org.examp.john;
import net.minecraft.server.level.ServerPlayer;
/**
* Логує команди, які виконують гравці, в консоль сервера.
* Формат: хто виконав ➔ яка команда ➔ координати гравця.
*/
public class CommandLogger {
public static void logCommand(ServerPlayer player, String command) {
String normalized = command.startsWith("/") ? command : "/" + command;
MSNexus.magicModLog.info(
"[CMD] {} ran: \"{}\" | pos: {}",
player.getName().getString(),
normalized,
ChatLogger.formatPos(player)
);
}
}
@@ -13,7 +13,6 @@ public class MSNexus implements ModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
StealthStorage.load(); StealthStorage.load();
AntiAbuse.register();
ChatHandler.register(); ChatHandler.register();
StealthCommand.register(); StealthCommand.register();
} }
@@ -1,38 +0,0 @@
package org.examp.john.mixin;
import com.mojang.brigadier.ParseResults;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.server.level.ServerPlayer;
import org.examp.john.CommandLogger;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Логує виконання команд гравцями в консоль сервера.
* Хукає Commands.performCommand — спільну точку виконання всіх команд.
*/
@Mixin(Commands.class)
public class CommandLogMixin {
@Inject(
method = "performCommand(Lcom/mojang/brigadier/ParseResults;Ljava/lang/String;)V",
at = @At("HEAD")
)
private void msnexus$logExecutedCommand(ParseResults<CommandSourceStack> parseResults, String command, CallbackInfo ci) {
try {
CommandSourceStack source = parseResults.getContext().getSource();
if (source == null) {
return;
}
ServerPlayer player = source.getPlayer();
if (player != null) {
CommandLogger.logCommand(player, command);
}
} catch (Exception e) {
org.examp.john.MSNexus.magicModLog.warn("[CMD] Не вдалося залогувати команду", e);
}
}
}
@@ -2,7 +2,7 @@ package org.examp.john.stealth;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import net.fabricmc.loader.api.FabricLoader; import com.google.gson.JsonSyntaxException;
import org.examp.john.MSNexus; import org.examp.john.MSNexus;
import java.io.IOException; import java.io.IOException;
@@ -11,7 +11,6 @@ import java.io.Writer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -24,9 +23,7 @@ import java.util.UUID;
public class StealthStorage { public class StealthStorage {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir() private static final Path CONFIG_PATH = Path.of("config", "localchat", "stealth.json");
.resolve("localchat")
.resolve("stealth.json");
private static Map<UUID, Boolean> data = new HashMap<>(); private static Map<UUID, Boolean> data = new HashMap<>();
@@ -38,33 +35,23 @@ public class StealthStorage {
return; return;
} }
try (Reader reader = Files.newBufferedReader(CONFIG_PATH, StandardCharsets.UTF_8)) { try (Reader reader = Files.newBufferedReader(CONFIG_PATH, StandardCharsets.UTF_8)) {
Map<String, Object> raw = GSON.fromJson(reader, Map.class); @SuppressWarnings("unchecked")
Map<String, Boolean> raw = GSON.fromJson(reader, Map.class);
data = new HashMap<>(); data = new HashMap<>();
if (raw != null) { if (raw != null) {
for (Map.Entry<String, Object> entry : raw.entrySet()) { for (Map.Entry<String, Boolean> entry : raw.entrySet()) {
try { data.put(UUID.fromString(entry.getKey()), entry.getValue());
UUID uuid = UUID.fromString(entry.getKey());
if (entry.getValue() instanceof Boolean value) {
data.put(uuid, value);
} else {
MSNexus.magicModLog.warn(
"stealth.json: пропущено некоректне значення для {}", entry.getKey());
}
} catch (IllegalArgumentException e) {
MSNexus.magicModLog.warn(
"stealth.json: пропущено некоректний ключ {}", entry.getKey());
}
} }
} }
} }
} catch (IOException | RuntimeException e) { } catch (IOException | JsonSyntaxException | IllegalArgumentException e) {
MSNexus.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage()); MSNexus.magicModLog.warn("Не вдалося прочитати stealth.json: {}", e.getMessage());
data = new HashMap<>(); data = new HashMap<>();
} }
} }
public static boolean isEnabled(UUID uuid) { public static boolean isEnabled(UUID uuid) {
return Boolean.TRUE.equals(data.get(uuid)); return data.getOrDefault(uuid, false);
} }
public static boolean toggle(UUID uuid) { public static boolean toggle(UUID uuid) {
@@ -83,20 +70,13 @@ public class StealthStorage {
private static void save() { private static void save() {
try { try {
Files.createDirectories(CONFIG_PATH.getParent()); Files.createDirectories(CONFIG_PATH.getParent());
Path tmp = CONFIG_PATH.resolveSibling(CONFIG_PATH.getFileName() + ".tmp"); try (Writer writer = Files.newBufferedWriter(CONFIG_PATH, StandardCharsets.UTF_8)) {
try (Writer writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8)) {
Map<String, Boolean> raw = new HashMap<>(); Map<String, Boolean> raw = new HashMap<>();
for (Map.Entry<UUID, Boolean> entry : data.entrySet()) { for (Map.Entry<UUID, Boolean> entry : data.entrySet()) {
raw.put(entry.getKey().toString(), entry.getValue()); raw.put(entry.getKey().toString(), entry.getValue());
} }
GSON.toJson(raw, writer); GSON.toJson(raw, writer);
} }
try {
Files.move(tmp, CONFIG_PATH,
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
Files.move(tmp, CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) { } catch (IOException e) {
MSNexus.magicModLog.error("Не вдалося зберегти stealth.json: {}", e.getMessage()); MSNexus.magicModLog.error("Не вдалося зберегти stealth.json: {}", e.getMessage());
} }
+10 -2
View File
@@ -7,17 +7,25 @@
"authors": [], "authors": [],
"contact": {}, "contact": {},
"license": "All-Rights-Reserved", "license": "All-Rights-Reserved",
"environment": "server", "icon": "assets/msnexus/icon.png",
"environment": "*",
"entrypoints": { "entrypoints": {
"fabric-datagen": [ "fabric-datagen": [
"org.examp.john.client.MSNexusDataGenerator" "org.examp.john.client.MSNexusDataGenerator"
], ],
"client": [
"org.examp.john.client.MSNexusClient"
],
"main": [ "main": [
"org.examp.john.MSNexus" "org.examp.john.MSNexus"
] ]
}, },
"mixins": [ "mixins": [
"msnexus.mixins.json" "msnexus.mixins.json",
{
"config": "msnexus.client.mixins.json",
"environment": "client"
}
], ],
"depends": { "depends": {
"fabricloader": ">=${loader_version}", "fabricloader": ">=${loader_version}",
-1
View File
@@ -4,7 +4,6 @@
"package": "org.examp.john.mixin", "package": "org.examp.john.mixin",
"compatibilityLevel": "JAVA_25", "compatibilityLevel": "JAVA_25",
"mixins": [ "mixins": [
"CommandLogMixin"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1