diff --git a/src/main/java/io/github/gaming32/worldhost/WorldHost.java b/src/main/java/io/github/gaming32/worldhost/WorldHost.java index f9598f6..9f37fc5 100644 --- a/src/main/java/io/github/gaming32/worldhost/WorldHost.java +++ b/src/main/java/io/github/gaming32/worldhost/WorldHost.java @@ -2,6 +2,7 @@ import com.demonwav.mcdev.annotations.Translatable; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftSessionService; import com.mojang.brigadier.Command; @@ -11,6 +12,7 @@ import io.github.gaming32.worldhost.config.WorldHostConfig; import io.github.gaming32.worldhost.ext.ServerDataExt; import io.github.gaming32.worldhost.gui.OnlineStatusLocation; +import io.github.gaming32.worldhost.gui.screen.FriendsScreen; import io.github.gaming32.worldhost.gui.screen.JoiningWorldHostScreen; import io.github.gaming32.worldhost.gui.screen.OnlineFriendsScreen; import io.github.gaming32.worldhost.plugin.FriendAdder; @@ -50,10 +52,12 @@ import net.minecraft.network.protocol.status.ServerStatus; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.players.GameProfileCache; +import org.apache.commons.io.function.IOConsumer; import org.apache.commons.io.function.IOFunction; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.EnglishReasonPhraseCatalog; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import org.quiltmc.parsers.json.JsonReader; import org.quiltmc.parsers.json.JsonWriter; @@ -74,6 +78,7 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; @@ -182,9 +187,14 @@ public class WorldHost public static final Path CACHE_DIR = GAME_DIR.resolve(".world-host-cache"); public static final Path CONFIG_DIR = GAME_DIR.resolve("config"); + public static final Path GLOBAL_CONFIG_DIR = locateGlobalConfigDir(); + public static final Path CONFIG_FILE = CONFIG_DIR.resolve("world-host.json5"); - public static final Path FRIENDS_FILE = CONFIG_DIR.resolve("world-host-friends.json"); public static final Path OLD_CONFIG_FILE = CONFIG_DIR.resolve("world-host.json"); + + public static final Path FRIENDS_FILE = GLOBAL_CONFIG_DIR.resolve("friends.json"); + public static final Path OLD_FRIENDS_FILE = CONFIG_DIR.resolve("world-host-friends.json"); + public static final WorldHostConfig CONFIG = new WorldHostConfig(); private static List<String> wordsForCid; @@ -220,6 +230,7 @@ public class WorldHost public static SocketAddress proxySocketAddress; + public static boolean clientLoadedFully; public static long tickCount; private static List<LoadedWorldHostPlugin> plugins; @@ -275,6 +286,7 @@ private static void init(IOFunction<String, Path> assetGetter) { LOGGER.info("Using client-generated connection ID {}", connectionIdToString(CONNECTION_ID)); loadConfig(); + prepareFileWatcher(); try { Files.createDirectories(CACHE_DIR); @@ -317,53 +329,145 @@ private static void init(IOFunction<String, Path> assetGetter) { reconnect(false, true); } + private static Path locateGlobalConfigDir() { + return switch (Util.getPlatform()) { + case WINDOWS -> Path.of(System.getenv("APPDATA"), "World Host Mod"); + case OSX -> Path.of(System.getProperty("user.home"), "Library/Application Support/World Host Mod"); + default -> { + var configHome = System.getenv("XDG_CONFIG_HOME"); + if (configHome == null) { + configHome = System.getProperty("user.home") + "/.config"; + } + yield Path.of(configHome, "world-host-mod"); + } + }; + } + public static void loadConfig() { - try (JsonReader reader = JsonReader.json5(CONFIG_FILE)) { - CONFIG.read(reader); - if (Files.exists(OLD_CONFIG_FILE)) { - LOGGER.info("Old {} still exists. Maybe consider removing it?", OLD_CONFIG_FILE.getFileName()); + loadConfigFile(CONFIG_FILE, JsonReader::json5, OLD_CONFIG_FILE, JsonReader::json, CONFIG::read); + loadFriendsOnly(); + saveConfig(); + } + + private static void loadFriendsOnly() { + loadConfigFile(FRIENDS_FILE, JsonReader::json, OLD_FRIENDS_FILE, JsonReader::json, CONFIG::readFriends); + } + + @Contract("_, _, !null, null, _ -> fail") + private static void loadConfigFile( + Path configFile, IOFunction<Path, JsonReader> configFormat, + @Nullable Path oldConfigFile, @Nullable IOFunction<Path, JsonReader> oldConfigFormat, + IOConsumer<JsonReader> configReader + ) { + try (JsonReader reader = configFormat.apply(configFile)) { + configReader.accept(reader); + if (oldConfigFile != null && Files.exists(oldConfigFile)) { + LOGGER.info("Old {} still exists. Consider removing it.", oldConfigFile.getFileName()); } } catch (NoSuchFileException e) { - LOGGER.info("{} not found. Trying to load old {}.", CONFIG_FILE.getFileName(), OLD_CONFIG_FILE.getFileName()); - try (JsonReader reader = JsonReader.json(OLD_CONFIG_FILE)) { - CONFIG.read(reader); - LOGGER.info( - "Found and read old {} into new {}. Maybe consider deleting the old {}?", - OLD_CONFIG_FILE.getFileName(), CONFIG_FILE.getFileName(), OLD_CONFIG_FILE.getFileName() - ); - } catch (NoSuchFileException e1) { - LOGGER.info("Old {} not found. Writing default config.", OLD_CONFIG_FILE.getFileName()); - } catch (IOException e1) { - LOGGER.error("Failed to load old {}.", OLD_CONFIG_FILE.getFileName(), e1); + if (oldConfigFile != null) { + assert oldConfigFormat != null; + LOGGER.info("{} not found. Trying to load old {}.", configFile.getFileName(), oldConfigFile.getFileName()); + try (JsonReader reader = oldConfigFormat.apply(oldConfigFile)) { + configReader.accept(reader); + LOGGER.info( + "Found and read old {} into new {}. Consider removing the old {}.", + oldConfigFile.getFileName(), configFile.getFileName(), oldConfigFile.getFileName() + ); + } catch (NoSuchFileException e1) { + LOGGER.info("Old {} not found. Writing default config.", oldConfigFile.getFileName()); + } catch (IOException e1) { + LOGGER.error("Failed to load old {}.", oldConfigFile.getFileName(), e1); + } } } catch (Exception e) { - LOGGER.error("Failed to load {}.", CONFIG_FILE.getFileName(), e); + LOGGER.error("Failed to load {}.", configFile.getFileName(), e); } - try (JsonReader reader = JsonReader.json(FRIENDS_FILE)) { - CONFIG.readFriends(reader); - } catch (NoSuchFileException ignored) { - } catch (Exception e) { - LOGGER.error("Failed to load {}.", FRIENDS_FILE.getFileName(), e); - } - saveConfig(); } public static void saveConfig() { + saveConfigFile(CONFIG_FILE, JsonWriter::json5, CONFIG::write); + saveConfigFile(FRIENDS_FILE, JsonWriter::json, CONFIG::writeFriends); + } + + private static void saveConfigFile( + Path configFile, IOFunction<Path, JsonWriter> configFormat, + IOConsumer<JsonWriter> configWriter + ) { try { - Files.createDirectories(CONFIG_FILE.getParent()); - try (JsonWriter writer = JsonWriter.json5(CONFIG_FILE)) { - CONFIG.write(writer); + Files.createDirectories(configFile.getParent()); + try (JsonWriter writer = configFormat.apply(configFile)) { + configWriter.accept(writer); } } catch (Exception e) { - LOGGER.error("Failed to write {}.", CONFIG_FILE.getFileName(), e); + LOGGER.error("Failed to write {}.", configFile.getFileName(), e); } + } + + private static void prepareFileWatcher() { try { - Files.createDirectories(FRIENDS_FILE.getParent()); - try (JsonWriter writer = JsonWriter.json(FRIENDS_FILE)) { - CONFIG.writeFriends(writer); - } + final var watchService = GLOBAL_CONFIG_DIR.getFileSystem().newWatchService(); + GLOBAL_CONFIG_DIR.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY); + Thread.ofVirtual().name("WorldHostFileWatcher").start(() -> { + try { + while (Minecraft.getInstance().isRunning() || !clientLoadedFully) { + final var key = watchService.take(); + for (final var event : key.pollEvents()) { + final var eventPath = GLOBAL_CONFIG_DIR.resolve((Path)event.context()); + if (eventPath.equals(FRIENDS_FILE)) { + LOGGER.info("Friends file modified. Reloading..."); + Minecraft.getInstance().execute(() -> { + final var oldFriends = Set.copyOf(CONFIG.getFriends()); + loadFriendsOnly(); + fullFriendsRefresh(oldFriends); + }); + } + } + key.reset(); + } + } catch (Exception e) { + LOGGER.error("Exception in file watcher. Stopping.", e); + } + try { + watchService.close(); + } catch (IOException e) { + LOGGER.error("Exception closing file watcher", e); + } + }); } catch (Exception e) { - LOGGER.error("Failed to write {}.", FRIENDS_FILE.getFileName(), e); + LOGGER.warn( + "Failed to setup watch service for {}. Friends setup in other instances will not be dynamically refreshed.", + FRIENDS_FILE, e + ); + } + } + + public static void setFriends(Set<UUID> friends) { + final var oldFriends = Set.copyOf(CONFIG.getFriends()); + CONFIG.getFriends().clear(); + CONFIG.getFriends().addAll(friends); + fullFriendsRefresh(oldFriends); + } + + public static void fullFriendsRefresh(Set<UUID> oldFriends) { + if (!CONFIG.isEnableFriends()) return; + final var friends = CONFIG.getFriends(); + final var removedFriends = Sets.difference(oldFriends, friends); + final var newFriends = Sets.difference(oldFriends, friends); + if (protoClient != null) { + final var server = Minecraft.getInstance().getSingleplayerServer(); + if (server != null && server.isPublished()) { + if (!removedFriends.isEmpty()) { + protoClient.closedWorld(removedFriends); + } + if (!newFriends.isEmpty()) { + protoClient.publishedWorld(newFriends); + } + } + refreshFriendsList(); + if (Minecraft.getInstance().screen instanceof FriendsScreen friendsScreen) { + friendsScreen.refresh(); + } } } diff --git a/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java b/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java index ba393c9..e08a08f 100644 --- a/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java +++ b/src/main/java/io/github/gaming32/worldhost/gui/screen/FriendsScreen.java @@ -33,11 +33,6 @@ public class FriendsScreen extends ScreenWithInfoTexts { private Button removeButton; private FriendsList list; - private final Runnable refresher = () -> { - assert minecraft != null; - minecraft.execute(() -> list.reloadEntries()); - }; - public FriendsScreen(Screen parent) { super(WorldHostComponents.FRIENDS, InfoTextsCategory.FRIENDS_SCREEN); this.parent = parent; @@ -63,7 +58,7 @@ protected void init() { assert minecraft != null; minecraft.setScreen(new AddFriendScreen( this, ADD_FRIEND_TEXT, null, - (friend, notify) -> friend.addFriend(notify, refresher) + (friend, notify) -> friend.addFriend(notify, this::refresh) )); }).width(152) .pos(width / 2 - 154, height - 54) @@ -110,6 +105,11 @@ public void onClose() { minecraft.setScreen(parent); } + public void refresh() { + assert minecraft != null; + minecraft.execute(() -> list.reloadEntries()); + } + @Override public void render( @NotNull @@ -216,7 +216,7 @@ public void maybeRemove() { minecraft.setScreen(new ConfirmScreen( yes -> { if (yes) { - friend.removeFriend(refresher); + friend.removeFriend(FriendsScreen.this::refresh); } minecraft.setScreen(FriendsScreen.this); }, diff --git a/src/main/java/io/github/gaming32/worldhost/mixin/MixinMinecraft.java b/src/main/java/io/github/gaming32/worldhost/mixin/MixinMinecraft.java index 9d5490a..ab6e598 100644 --- a/src/main/java/io/github/gaming32/worldhost/mixin/MixinMinecraft.java +++ b/src/main/java/io/github/gaming32/worldhost/mixin/MixinMinecraft.java @@ -44,14 +44,12 @@ public abstract class MixinMinecraft { @Unique private Class<? extends Screen> wh$lastScreenClass; - @Unique - private boolean wh$readyForTesting; @Inject(method = "setOverlay", at = @At("HEAD")) private void deferredToastReady(Overlay loadingGui, CallbackInfo ci) { if (loadingGui == null) { WHToast.ready(); - wh$readyForTesting = true; + WorldHost.clientLoadedFully = true; } } @@ -59,7 +57,7 @@ private void deferredToastReady(Overlay loadingGui, CallbackInfo ci) { private void preTick(CallbackInfo ci) { WHToast.tick(); final var screenClass = ScreenChain.getScreenClass(screen); - if (WorldHostTesting.ENABLED && wh$readyForTesting && screenClass != wh$lastScreenClass) { + if (WorldHostTesting.ENABLED && WorldHost.clientLoadedFully && screenClass != wh$lastScreenClass) { wh$lastScreenClass = screenClass; WorldHostTesting.SCREEN_CHAIN.get().advance(screen); } diff --git a/src/main/java/io/github/gaming32/worldhost/mixin/MixinMinecraftServer.java b/src/main/java/io/github/gaming32/worldhost/mixin/MixinMinecraftServer.java deleted file mode 100644 index 5d61e29..0000000 --- a/src/main/java/io/github/gaming32/worldhost/mixin/MixinMinecraftServer.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.gaming32.worldhost.mixin; - -import net.minecraft.server.MinecraftServer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.function.BooleanSupplier; - -@Mixin(MinecraftServer.class) -public class MixinMinecraftServer { - @Inject(method = "tickServer", at = @At("RETURN")) - private void tickVoiceChat(BooleanSupplier hasTimeLeft, CallbackInfo ci) { -// if (WorldHost.isModLoaded("voicechat")) { -// WorldHostSimpleVoiceChatCompat.tick((MinecraftServer)(Object)this); -// } - } -} diff --git a/src/main/resources/world-host.mixins.json b/src/main/resources/world-host.mixins.json index 3c7ab28..9bfdc51 100644 --- a/src/main/resources/world-host.mixins.json +++ b/src/main/resources/world-host.mixins.json @@ -8,7 +8,6 @@ "MixinConnection", "MixinInputConstants", "MixinLevelSummary", - "MixinMinecraftServer", "MixinPlayerList", "MixinPublishCommand", "MixinServerConnectionListener_1", @@ -33,7 +32,9 @@ "MixinWorldSelectionList_WorldListEntry", "PlainTextButtonAccessor", "ServerStatusPingerAccessor", + //#if FABRIC "modmenu.MixinModMenuEventHandler" + //#endif ], "injectors": { "defaultRequire": 1