From 172d4d5b79824c6fb9c39ca54512041704743c30 Mon Sep 17 00:00:00 2001 From: JFronny Date: Sun, 8 Oct 2023 17:26:08 +0200 Subject: [PATCH] feat(config-network): implement relevant config abstractions over network and add clientside GUI for server config --- build.gradle.kts | 2 +- .../config/impl/commands/JfConfigCommand.java | 2 +- .../config/api/v2/ui/ConfigScreenFactory.java | 17 ++ .../assets/libjf-config-core/lang/en_us.json | 1 + .../libjf/config/api/v2/EntryInfo.java | 8 + .../libjf/config/api/v2/type/Type.java | 4 +- libjf-config-network-v0/build.gradle.kts | 17 ++ .../network/client/JfConfigNetworkClient.java | 26 +++ .../client/JfConfigNetworkCommands.java | 33 +++ .../impl/network/client/ScheduledScreen.java | 23 ++ .../config/impl/network/FollowupSender.java | 10 + .../impl/network/PMResponseHandler.java | 64 ++++++ .../config/impl/network/RequestHandler.java | 7 + .../config/impl/network/RequestRouter.java | 113 ++++++++++ .../config/impl/network/ResponseHandler.java | 10 + .../packet/ConfigurationCompletePacket.java | 25 +++ .../network/packet/ConfigurationPacket.java | 26 +++ .../network/rci/MirrorConfigCategory.java | 138 ++++++++++++ .../impl/network/rci/MirrorConfigHolder.java | 60 ++++++ .../network/rci/MirrorConfigInstance.java | 57 +++++ .../config/impl/network/rci/MirrorObject.java | 53 +++++ .../impl/network/rci/entry/Datatype.java | 5 + .../network/rci/entry/MirrorEntryInfo.java | 101 +++++++++ .../rci/entry/MirrorEntryInfoBase.java | 59 +++++ .../rci/entry/MirrorEntryInfoUnsupported.java | 46 ++++ .../network/server/JfConfigNetworkServer.java | 201 ++++++++++++++++++ .../src/main/resources/fabric.mod.json | 33 +++ .../impl/ui/tiny/TinyConfigScreenFactory.java | 4 +- .../config/impl/ui/tiny/TinyConfigTab.java | 7 +- .../ui/tiny/entry/EntryInfoWidgetBuilder.java | 12 +- .../libjf/config/test/tiny/TestConfig.java | 2 + settings.gradle.kts | 1 + 32 files changed, 1156 insertions(+), 11 deletions(-) create mode 100644 libjf-config-network-v0/build.gradle.kts create mode 100644 libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkClient.java create mode 100644 libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkCommands.java create mode 100644 libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/ScheduledScreen.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/FollowupSender.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/PMResponseHandler.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestHandler.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestRouter.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/ResponseHandler.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationCompletePacket.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationPacket.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigCategory.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigHolder.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigInstance.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorObject.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/Datatype.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfo.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoBase.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoUnsupported.java create mode 100644 libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/server/JfConfigNetworkServer.java create mode 100644 libjf-config-network-v0/src/main/resources/fabric.mod.json diff --git a/build.gradle.kts b/build.gradle.kts index 90d2182..e3cef6b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,11 +38,11 @@ allprojects { } dependencies { - // Temporarily disabled since modmenu doesn't support snapshots modLocalRuntime("com.terraformersmc:modmenu:$modmenuVersion") { exclude("net.fabricmc") // required to work around duplicate fabric loaders } modLocalRuntime(fabricApi.module("fabric-command-api-v2", fabricVersion)) + modLocalRuntime(fabricApi.module("fabric-networking-api-v1", fabricVersion)) compileOnly("io.gitlab.jfronny:commons:$commonsVersion") baseCommonsModules.forEach { compileOnly("io.gitlab.jfronny:commons-$it:$commonsVersion") } } diff --git a/libjf-config-commands/src/main/java/io/gitlab/jfronny/libjf/config/impl/commands/JfConfigCommand.java b/libjf-config-commands/src/main/java/io/gitlab/jfronny/libjf/config/impl/commands/JfConfigCommand.java index 7e813ad..5675342 100644 --- a/libjf-config-commands/src/main/java/io/gitlab/jfronny/libjf/config/impl/commands/JfConfigCommand.java +++ b/libjf-config-commands/src/main/java/io/gitlab/jfronny/libjf/config/impl/commands/JfConfigCommand.java @@ -17,7 +17,6 @@ import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Language; -import org.apache.commons.codec.language.bm.Lang; import java.util.function.Consumer; import java.util.function.Function; @@ -111,6 +110,7 @@ public class JfConfigCommand implements ModInitializer { }); private void registerEntry(ConfigCategory config, String subpath, LiteralArgumentBuilder cns, EntryInfo entry) { + if (!entry.supportsRepresentation()) return; LiteralArgumentBuilder c_entry = literal(entry.getName()).executes(context -> { String msg = "The value of " + subpath + "." + entry.getName() + " is "; Text visualized = visualizeOption(config, entry, tryRun(entry::getValue)); diff --git a/libjf-config-core-v2/src/client/java/io/gitlab/jfronny/libjf/config/api/v2/ui/ConfigScreenFactory.java b/libjf-config-core-v2/src/client/java/io/gitlab/jfronny/libjf/config/api/v2/ui/ConfigScreenFactory.java index 01b9b41..0db1d87 100644 --- a/libjf-config-core-v2/src/client/java/io/gitlab/jfronny/libjf/config/api/v2/ui/ConfigScreenFactory.java +++ b/libjf-config-core-v2/src/client/java/io/gitlab/jfronny/libjf/config/api/v2/ui/ConfigScreenFactory.java @@ -1,9 +1,13 @@ package io.gitlab.jfronny.libjf.config.api.v2.ui; +import io.gitlab.jfronny.libjf.config.api.v2.ConfigHolder; import io.gitlab.jfronny.libjf.config.api.v2.ConfigInstance; +import io.gitlab.jfronny.libjf.config.api.v2.dsl.DSL; import io.gitlab.jfronny.libjf.config.impl.ui.ConfigScreenFactoryDiscovery; import net.minecraft.client.gui.screen.Screen; +import java.util.Objects; + public interface ConfigScreenFactory> { static ConfigScreenFactory getInstance() { return ConfigScreenFactoryDiscovery.getConfigured2(); @@ -13,6 +17,19 @@ public interface ConfigScreenFactory { + Objects.requireNonNull(holder).getRegistered().forEach((n, ci) -> { + builder.referenceConfig(ci); + }); + return builder; + }), parent).get(); + } + interface Built { S get(); diff --git a/libjf-config-core-v2/src/client/resources/assets/libjf-config-core/lang/en_us.json b/libjf-config-core-v2/src/client/resources/assets/libjf-config-core/lang/en_us.json index 6553852..8fa10ca 100644 --- a/libjf-config-core-v2/src/client/resources/assets/libjf-config-core/lang/en_us.json +++ b/libjf-config-core-v2/src/client/resources/assets/libjf-config-core/lang/en_us.json @@ -2,6 +2,7 @@ "libjf-config-core-v2.jfconfig.title": "LibJF Config", "libjf-config-core-v2.jfconfig.watchForChanges": "Watch for changes", "libjf-config-core-v2.jfconfig.watchForChanges.tooltip": "Automatically reload configs when they are changed", + "libjf-config-core-v2.errored_entry": "Could not render entry: %s", "libjf-config-core-v2.presets": "Presets", "libjf-config-core-v2.default": "Default", "libjf-config-core-v2.see-also": "See also: %s", diff --git a/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/EntryInfo.java b/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/EntryInfo.java index c3e6290..a6c880a 100644 --- a/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/EntryInfo.java +++ b/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/EntryInfo.java @@ -51,6 +51,14 @@ public interface EntryInfo { */ Type getValueType(); + /** + * Whether the entry can be represented. If this is false, all methods except getName, fix and reset must throw UnsupportedOperationExceptions + * @return Whether this entry can be represented + */ + default boolean supportsRepresentation() { + return true; + } + /** * Ensure the current value is within expected bounds. */ diff --git a/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/type/Type.java b/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/type/Type.java index c92ca32..1febe17 100644 --- a/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/type/Type.java +++ b/libjf-config-core-v2/src/main/java/io/gitlab/jfronny/libjf/config/api/v2/type/Type.java @@ -162,7 +162,7 @@ public sealed interface Type { } } - final record TEnum(@Nullable Class klazz, String name, T[] options) implements Type { + record TEnum(@Nullable Class klazz, String name, T[] options) implements Type { public TEnum(Class klazz) { this(klazz, klazz.getSimpleName(), klazz.getEnumConstants()); } @@ -194,7 +194,7 @@ public sealed interface Type { } } - final record TUnknown(java.lang.reflect.Type klazz) implements Type { + record TUnknown(java.lang.reflect.Type klazz) implements Type { @Override public @Nullable java.lang.reflect.Type asClass() { return klazz; diff --git a/libjf-config-network-v0/build.gradle.kts b/libjf-config-network-v0/build.gradle.kts new file mode 100644 index 0000000..641d02e --- /dev/null +++ b/libjf-config-network-v0/build.gradle.kts @@ -0,0 +1,17 @@ +import io.gitlab.jfronny.scripts.* + +plugins { + id("jfmod.module") +} + +base { + archivesName.set("libjf-config-network-v0") +} + +dependencies { + val fabricVersion: String by rootProject.extra + api(devProject(":libjf-base")) + api(devProject(":libjf-config-core-v2")) + include(modImplementation(fabricApi.module("fabric-networking-api-v1", fabricVersion))!!) + include(modImplementation(fabricApi.module("fabric-command-api-v2", fabricVersion))!!) +} diff --git a/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkClient.java b/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkClient.java new file mode 100644 index 0000000..5b27ffe --- /dev/null +++ b/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkClient.java @@ -0,0 +1,26 @@ +package io.gitlab.jfronny.libjf.config.impl.network.client; + +import io.gitlab.jfronny.libjf.config.impl.network.packet.ConfigurationCompletePacket; +import io.gitlab.jfronny.libjf.config.impl.network.packet.ConfigurationPacket; +import io.gitlab.jfronny.libjf.config.impl.network.RequestRouter; +import net.fabricmc.fabric.api.client.networking.v1.*; + +public class JfConfigNetworkClient { + public static boolean isAvailable = false; + public static void initialize() { + JfConfigNetworkCommands.initialize(); + ClientPlayNetworking.registerGlobalReceiver(RequestRouter.RESPONSE_ID, (client, handler, buf, responseSender) -> { + RequestRouter.acceptResponse(buf, responseSender); + }); + ClientPlayNetworking.registerGlobalReceiver(RequestRouter.REQUEST_ID, (client, handler, buf, responseSender) -> { + RequestRouter.acceptRequest(buf, responseSender); + }); + ClientConfigurationNetworking.registerGlobalReceiver(ConfigurationPacket.PACKET_TYPE, (packet, responseSender) -> { + isAvailable = packet.version() == RequestRouter.PROTOCOL_VERSION; // Handshake possible? + responseSender.sendPacket(new ConfigurationCompletePacket()); + }); + ClientConfigurationConnectionEvents.INIT.register((handler, client) -> { + isAvailable = false; // Reset for new server connection + }); + } +} diff --git a/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkCommands.java b/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkCommands.java new file mode 100644 index 0000000..42f130b --- /dev/null +++ b/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/JfConfigNetworkCommands.java @@ -0,0 +1,33 @@ +package io.gitlab.jfronny.libjf.config.impl.network.client; + +import com.mojang.brigadier.Command; +import io.gitlab.jfronny.libjf.config.api.v2.ConfigHolder; +import io.gitlab.jfronny.libjf.config.api.v2.ui.ConfigScreenFactory; +import io.gitlab.jfronny.libjf.config.impl.network.RequestRouter; +import io.gitlab.jfronny.libjf.config.impl.network.rci.MirrorConfigHolder; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public class JfConfigNetworkCommands { + public static void initialize() { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { + dispatcher.register(literal(RequestRouter.MOD_ID).executes(ctx -> { + if (JfConfigNetworkClient.isAvailable) { + ConfigHolder ch = new MirrorConfigHolder(ClientPlayNetworking.getSender()); + Screen screen = ConfigScreenFactory.getInstance().createOverview(ch, null); + // Delay since the chat needs to close first + MinecraftClient.getInstance().setScreen(new ScheduledScreen(screen)); + return Command.SINGLE_SUCCESS; + } else { + ctx.getSource().sendError(Text.literal(RequestRouter.MOD_ID + " server is unavailable")); + return -1; + } + })); + }); + } +} diff --git a/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/ScheduledScreen.java b/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/ScheduledScreen.java new file mode 100644 index 0000000..5b3a1aa --- /dev/null +++ b/libjf-config-network-v0/src/client/java/io/gitlab/jfronny/libjf/config/impl/network/client/ScheduledScreen.java @@ -0,0 +1,23 @@ +package io.gitlab.jfronny.libjf.config.impl.network.client; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +public class ScheduledScreen extends Screen { + private Screen scheduled; + + protected ScheduledScreen(Screen scheduled) { + super(Text.literal("Close this screen")); + + this.scheduled = scheduled; + } + + @Override + public void removed() { + if (scheduled != null) { + Screen s = scheduled; + client.send(() -> client.setScreen(s)); + } + scheduled = null; + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/FollowupSender.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/FollowupSender.java new file mode 100644 index 0000000..6026bdf --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/FollowupSender.java @@ -0,0 +1,10 @@ +package io.gitlab.jfronny.libjf.config.impl.network; + +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.network.PacketByteBuf; + +import java.util.Map; + +public interface FollowupSender { + void sendFollowupRequest(PacketSender responseSender, String name, PacketByteBuf body, ResponseHandler responseHandler, Map temporaryHandlers); +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/PMResponseHandler.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/PMResponseHandler.java new file mode 100644 index 0000000..ea43c0c --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/PMResponseHandler.java @@ -0,0 +1,64 @@ +package io.gitlab.jfronny.libjf.config.impl.network; + +import net.minecraft.network.PacketByteBuf; + +import java.util.Objects; +import java.util.function.Consumer; + +public record PMResponseHandler(Consumer handler) implements ResponseHandler { + public PMResponseHandler { + Objects.requireNonNull(handler); + } + + @Override + public void onSuccess(PacketByteBuf buf) { + handler.accept(new Response.Success(buf)); + } + + @Override + public void onDeny() { + handler.accept(Response.SimpleFailure.DENY); + } + + @Override + public void onNotFound() { + handler.accept(Response.SimpleFailure.NOT_FOUND); + } + + @Override + public void onFailure(String message) { + handler.accept(new Response.Failure(message)); + } + + public sealed interface Response { + PacketByteBuf get(); + + record Success(PacketByteBuf buf) implements Response { + @Override + public PacketByteBuf get() { + return buf; + } + } + + enum SimpleFailure implements Response { + DENY, NOT_FOUND; + + @Override + public PacketByteBuf get() { + if (this == DENY) throw new RuntimeException("Access not allowed"); + else throw new UnsupportedOperationException("Method not found"); + } + } + + record Failure(String message) implements Response { + public Failure { + Objects.requireNonNull(message); + } + + @Override + public PacketByteBuf get() { + throw new RuntimeException(message); + } + } + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestHandler.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestHandler.java new file mode 100644 index 0000000..aee7e01 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestHandler.java @@ -0,0 +1,7 @@ +package io.gitlab.jfronny.libjf.config.impl.network; + +import net.minecraft.network.PacketByteBuf; + +public interface RequestHandler { + PacketByteBuf handle(PacketByteBuf buf, FollowupSender followupSender) throws Throwable; +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestRouter.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestRouter.java new file mode 100644 index 0000000..a3e9347 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/RequestRouter.java @@ -0,0 +1,113 @@ +package io.gitlab.jfronny.libjf.config.impl.network; + +import io.gitlab.jfronny.libjf.LibJf; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Identifier; + +import java.util.*; + +public class RequestRouter { + public static final String MOD_ID = "libjf-config-network-v0"; + public static final Identifier RESPONSE_ID = new Identifier(MOD_ID, "response"); + public static final Identifier REQUEST_ID = new Identifier(MOD_ID, "request"); + public static int PROTOCOL_VERSION = 1; + + private static final Map persistendHandlers = new HashMap<>(); + private static final Map currentRequests = new HashMap<>(); //TODO implement timeout and prune old requests + private static final Random random = new Random(); + + public static void acceptResponse(PacketByteBuf buf, PacketSender responseSender) { + Request request = currentRequests.remove(buf.readLong()); + if (request != null) { + switch (buf.readInt()) { + case 0 -> request.responseHandler.onSuccess(buf); + case 1 -> request.responseHandler.onNotFound(); + case 2 -> request.responseHandler.onDeny(); + case 3 -> request.responseHandler.onFailure(buf.readString()); + default -> request.responseHandler.onFailure("Unrecognized error received"); + } + } + } + + public static void acceptRequest(PacketByteBuf buf, PacketSender responseSender) { + PacketByteBuf resp = PacketByteBufs.create(); + long id = buf.readLong(); + resp.writeLong(id); + try { + if (buf.readBoolean()) { + // persistent + handleRequest(resp, id, buf, persistendHandlers.get(buf.readString())); + } else { + // followup + Request parent = currentRequests.get(buf.readLong()); + if (parent == null) { + resp.writeInt(1); + } else { + String key = buf.readString(); + RequestHandler handler = parent.temporaryHandlers.get(key); + if (handler == null) handler = persistendHandlers.get(key); + handleRequest(resp, id, buf, handler); + } + } + } catch (Throwable t) { + LibJf.LOGGER.error("Cannot complete request", t); + resp.writeInt(3); + resp.writeString(t.getMessage() == null ? "null" : t.getMessage()); + } finally { + responseSender.sendPacket(RESPONSE_ID, resp); + } + } + + private static void handleRequest(PacketByteBuf resp, long id, PacketByteBuf buf, RequestHandler handler) throws Throwable { + if (handler == null) { + resp.writeInt(1); + } else { + PacketByteBuf response = handler.handle( + buf, + (responseSender, name, body, responseHandler, temporaryHandlers) -> + RequestRouter.sendRequest(responseSender, id, name, body, responseHandler, temporaryHandlers) + ); + resp.writeInt(0); + if (response != null) resp.writeBytes(response.copy()); + } + } + + public static void deny(PacketByteBuf buf, PacketSender responseSender) { + PacketByteBuf resp = PacketByteBufs.create(); + resp.writeLong(buf.readLong()); + resp.writeInt(2); + responseSender.sendPacket(RESPONSE_ID, resp); + } + + public static void registerHandler(String name, RequestHandler handler) { + persistendHandlers.put(name, handler); + } + + public static void sendRequest(PacketSender responseSender, String name, PacketByteBuf body, ResponseHandler responseHandler, Map temporaryHandlers) { + sendRequest(responseSender, null, name, body, responseHandler, temporaryHandlers); + } + + private static void sendRequest(PacketSender responseSender, Long parent, String name, PacketByteBuf body, ResponseHandler responseHandler, Map temporaryHandlers) { + long id; + synchronized (currentRequests) { + Set keys = currentRequests.keySet(); + do { + id = random.nextLong(); + } while (keys.contains(id)); + currentRequests.put(id, new Request(temporaryHandlers, responseHandler)); + } + PacketByteBuf req = PacketByteBufs.create(); + req.writeLong(id); + req.writeBoolean(parent == null); + if (parent != null) req.writeLong(parent); + req.writeString(name); + if (body != null) { + req.writeBytes(body.copy()); + } + responseSender.sendPacket(REQUEST_ID, req); + } + + private record Request(Map temporaryHandlers, ResponseHandler responseHandler) {} +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/ResponseHandler.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/ResponseHandler.java new file mode 100644 index 0000000..4d37f99 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/ResponseHandler.java @@ -0,0 +1,10 @@ +package io.gitlab.jfronny.libjf.config.impl.network; + +import net.minecraft.network.PacketByteBuf; + +public interface ResponseHandler { + void onSuccess(PacketByteBuf buf); + void onDeny(); + void onNotFound(); + void onFailure(String message); +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationCompletePacket.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationCompletePacket.java new file mode 100644 index 0000000..19a15d6 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationCompletePacket.java @@ -0,0 +1,25 @@ +package io.gitlab.jfronny.libjf.config.impl.network.packet; + +import io.gitlab.jfronny.libjf.config.impl.network.RequestRouter; +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Identifier; + +public record ConfigurationCompletePacket() implements FabricPacket { + public static final Identifier ID = new Identifier(RequestRouter.MOD_ID, "handshake_complete"); + public static final PacketType PACKET_TYPE = PacketType.create(ID, ConfigurationCompletePacket::new); + + public ConfigurationCompletePacket(PacketByteBuf buf) { + this(); + } + + @Override + public void write(PacketByteBuf buf) { + } + + @Override + public PacketType getType() { + return PACKET_TYPE; + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationPacket.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationPacket.java new file mode 100644 index 0000000..01baff5 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/packet/ConfigurationPacket.java @@ -0,0 +1,26 @@ +package io.gitlab.jfronny.libjf.config.impl.network.packet; + +import io.gitlab.jfronny.libjf.config.impl.network.RequestRouter; +import net.fabricmc.fabric.api.networking.v1.FabricPacket; +import net.fabricmc.fabric.api.networking.v1.PacketType; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Identifier; + +public record ConfigurationPacket(int version) implements FabricPacket { + public static final Identifier ID = new Identifier(RequestRouter.MOD_ID, "handshake"); + public static final PacketType PACKET_TYPE = PacketType.create(ID, ConfigurationPacket::new); + + public ConfigurationPacket(PacketByteBuf buf) { + this(buf.readInt()); + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeInt(version); + } + + @Override + public PacketType getType() { + return PACKET_TYPE; + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigCategory.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigCategory.java new file mode 100644 index 0000000..32145d7 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigCategory.java @@ -0,0 +1,138 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci; + +import io.gitlab.jfronny.libjf.LibJf; +import io.gitlab.jfronny.libjf.config.api.v2.*; +import io.gitlab.jfronny.libjf.config.api.v2.type.Type; +import io.gitlab.jfronny.libjf.config.impl.network.rci.entry.*; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.network.PacketByteBuf; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class MirrorConfigCategory extends MirrorObject implements ConfigCategory { + protected final String id; + protected final String categoryPath; + protected final Supplier root; + + public MirrorConfigCategory(PacketSender packetSender, String id, String categoryPath, Supplier root) { + super(packetSender); + this.id = id; + this.categoryPath = categoryPath; + this.root = root; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getCategoryPath() { + return categoryPath; + } + + public void writeCategoryPath(PacketByteBuf buf) { + root.get().writeConfigInstance(buf); + buf.writeCollection(getCategoryPathList(), PacketByteBuf::writeString); + } + + protected List getCategoryPathList() { + if (getCategoryPath().isEmpty()) return List.of(); + String[] sc = getCategoryPath().split("\\."); + return Arrays.stream(sc).toList(); + } + + @Override + public List> getEntries() { + PacketByteBuf buf = PacketByteBufs.create(); + writeCategoryPath(buf); + buf = sendRequest("getEntries", buf); + return List.copyOf(buf.readList(b -> { + String name = b.readString(); + int type = b.readInt(); + if (type == -1) return new MirrorEntryInfoUnsupported<>(packetSender, this, name); + Type t = switch (Datatype.values()[type]) { + case INT -> Type.TInt.INSTANCE; + case LONG -> Type.TLong.INSTANCE; + case FLOAT -> Type.TFloat.INSTANCE; + case DOUBLE -> Type.TDouble.INSTANCE; + case STRING -> Type.TString.INSTANCE; + case BOOL -> Type.TBool.INSTANCE; + case ENUM -> Type.TEnum.create(name, b.readList(PacketByteBuf::readString).toArray(String[]::new)); + }; + return new MirrorEntryInfo<>( + packetSender, + this, + name, + MirrorEntryInfo.read(b, t), + t, + b.readInt(), + b.readDouble(), + b.readDouble()); + })); + } + + @Override + public Map getPresets() { + PacketByteBuf buf = PacketByteBufs.create(); + writeCategoryPath(buf); + buf = sendRequest("getPresets", buf); + return buf.readList(PacketByteBuf::readString).stream().collect(Collectors.toUnmodifiableMap( + Function.identity(), + s -> () -> runPreset(s) + )); + } + + private void runPreset(String id) { + PacketByteBuf buf = PacketByteBufs.create(); + writeCategoryPath(buf); + buf.writeString(id); + sendRequest("runPreset", buf); + } + + @Override + public List getReferencedConfigs() { + PacketByteBuf buf = PacketByteBufs.create(); + writeCategoryPath(buf); + buf = sendRequest("getReferencedConfigs", buf); + return buf.readList(b -> b.readBoolean() + ? MirrorConfigInstance.create(packetSender, b.readString()) + : MirrorConfigInstance.create( + packetSender, + b.readString(), + Stream.concat( + root.get().streamParentPaths(), + Stream.of( + Stream.concat( + Stream.of(root.get().getId()), + getCategoryPathList().stream() + ).toList() + ) + ).toList() + ) + ); + } + + @Override + public Map getCategories() { + PacketByteBuf buf = PacketByteBufs.create(); + writeCategoryPath(buf); + buf = sendRequest("getCategories", buf); + return buf.readList(PacketByteBuf::readString) + .stream() + .collect(Collectors.toUnmodifiableMap( + Function.identity(), + s -> new MirrorConfigCategory(packetSender, s, categoryPath + s + ".", root) + )); + } + + @Override + public ConfigInstance getRoot() { + return root.get(); + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigHolder.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigHolder.java new file mode 100644 index 0000000..8626a04 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigHolder.java @@ -0,0 +1,60 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci; + +import io.gitlab.jfronny.libjf.config.api.v2.ConfigHolder; +import io.gitlab.jfronny.libjf.config.api.v2.ConfigInstance; +import net.fabricmc.fabric.api.networking.v1.*; +import net.minecraft.network.PacketByteBuf; + +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.stream.*; + +public class MirrorConfigHolder extends MirrorObject implements ConfigHolder { + public MirrorConfigHolder(PacketSender packetSender) { + super(packetSender); + } + + @Override + public void register(String modId, ConfigInstance config) { + throw new UnsupportedOperationException(); + } + + @Override + public Map getRegistered() { + PacketByteBuf buf = sendRequest("getRegistered", null); + return buf.readList(PacketByteBuf::readString) + .stream() + .collect(Collectors.toUnmodifiableMap(Function.identity(), s -> MirrorConfigInstance.create(packetSender, s))); + } + + @Override + public ConfigInstance get(String modId) { + return isRegistered(modId) ? MirrorConfigInstance.create(packetSender, modId) : null; + } + + @Override + public ConfigInstance get(Path configPath) { + return null; + } + + @Override + public boolean isRegistered(String modId) { + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeString(modId); + buf = sendRequest("isRegistered", buf); + return buf.readBoolean(); + } + + @Override + public boolean isRegistered(Path configPath) { + return false; + } + + @Override + public void migrateFiles(String modId) { + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeString(modId); + sendRequest("migrateFiles", buf); + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigInstance.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigInstance.java new file mode 100644 index 0000000..898f03c --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorConfigInstance.java @@ -0,0 +1,57 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci; + +import io.gitlab.jfronny.libjf.config.api.v2.*; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.network.PacketByteBuf; + +import java.nio.file.Path; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class MirrorConfigInstance extends MirrorConfigCategory implements ConfigInstance { + protected final List> parentPaths; + protected MirrorConfigInstance(PacketSender packetSender, String id, Supplier root, List> parentPaths) { + super(packetSender, id, "", root); + this.parentPaths = parentPaths; + } + + public static MirrorConfigInstance create(PacketSender packetSender, String id) { + return create(packetSender, id, List.of()); + } + + public static MirrorConfigInstance create(PacketSender packetSender, String id, List> parentPaths) { + MirrorConfigInstance[] cis = {null}; + cis[0] = new MirrorConfigInstance(packetSender, id, () -> cis[0], parentPaths); + return cis[0]; + } + + public Stream> streamParentPaths() { + return parentPaths.stream(); + } + + public void writeConfigInstance(PacketByteBuf buf) { + buf.writeCollection(parentPaths, (b, l) -> b.writeCollection(l, PacketByteBuf::writeString)); + buf.writeString(id); + } + + @Override + public void load() { + PacketByteBuf buf = PacketByteBufs.create(); + writeConfigInstance(buf); + sendRequest("load", buf); + } + + @Override + public void write() { + PacketByteBuf buf = PacketByteBufs.create(); + writeConfigInstance(buf); + sendRequest("write", buf); + } + + @Override + public Optional getFilePath() { + return Optional.empty(); + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorObject.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorObject.java new file mode 100644 index 0000000..0c7737b --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/MirrorObject.java @@ -0,0 +1,53 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci; + +import io.gitlab.jfronny.libjf.config.impl.network.*; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.network.PacketByteBuf; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class MirrorObject { + protected final PacketSender packetSender; + + public MirrorObject(PacketSender packetSender) { + this.packetSender = packetSender; + } + + protected T synchronize(Consumer> action) { + Object[] result = {null}; + AtomicBoolean pause = new AtomicBoolean(true); + action.accept(with -> { + result[0] = with; + pause.set(false); + synchronized (result) { + result.notifyAll(); + } + }); + synchronized (result) { + while (pause.get()) { + try { + if (pause.get()) result.wait(); + } catch (InterruptedException e) { + // ignored + } + } + } + return (T) result[0]; + } + + protected PacketByteBuf sendRequest(String name, PacketByteBuf body) { + return sendRequest(name, body, Map.of()); + } + + protected PacketByteBuf sendRequest(String name, PacketByteBuf body, Map temporaryHandlers) { + return this.synchronize(hold -> { + RequestRouter.sendRequest(packetSender, name, body, new PMResponseHandler(hold::resume), temporaryHandlers); + }).get(); + } + + protected interface Hold { + void resume(T with); + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/Datatype.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/Datatype.java new file mode 100644 index 0000000..46199db --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/Datatype.java @@ -0,0 +1,5 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci.entry; + +public enum Datatype { + INT, LONG, FLOAT, DOUBLE, STRING, BOOL, ENUM +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfo.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfo.java new file mode 100644 index 0000000..48438cd --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfo.java @@ -0,0 +1,101 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci.entry; + +import io.gitlab.jfronny.libjf.config.api.v2.type.Type; +import io.gitlab.jfronny.libjf.config.impl.network.rci.MirrorConfigCategory; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.network.PacketByteBuf; + +public class MirrorEntryInfo extends MirrorEntryInfoBase { + private final T defaultValue; + private final Type valueType; + private final int width; + private final double minValue; + private final double maxValue; + + public MirrorEntryInfo( + PacketSender packetSender, + MirrorConfigCategory category, + String entryName, + T defaultValue, + Type valueType, + int width, + double minValue, + double maxValue + ) { + super(packetSender, category, entryName); + this.defaultValue = defaultValue; + this.valueType = valueType; + this.width = width; + this.minValue = minValue; + this.maxValue = maxValue; + } + + @Override + public T getDefault() { + return defaultValue; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public double getMinValue() { + return minValue; + } + + @Override + public double getMaxValue() { + return maxValue; + } + + @Override + public Type getValueType() { + return valueType; + } + + public static Object read(PacketByteBuf buf, Type type) { + if (!buf.readBoolean()) return null; + if (type.isInt()) return buf.readInt(); + if (type.isLong()) return buf.readLong(); + if (type.isFloat()) return buf.readFloat(); + if (type.isDouble()) return buf.readDouble(); + if (type.isString()) return buf.readString(); + if (type.isBool()) return buf.readBoolean(); + if (type.isEnum()) return type.asEnum().optionForString(buf.readString()); + throw new UnsupportedOperationException(); + } + + public static void write(PacketByteBuf buf, Type type, Object data) { + if (data == null) buf.writeBoolean(false); + else { + buf.writeBoolean(true); + if (type.isInt()) buf.writeInt((int) data); + else if (type.isLong()) buf.writeLong((long) data); + else if (type.isFloat()) buf.writeFloat((float) data); + else if (type.isDouble()) buf.writeDouble((double) data); + else if (type.isString()) buf.writeString((String) data); + else if (type.isBool()) buf.writeBoolean((boolean) data); + else if (type.isEnum()) buf.writeString(data.toString()); + else throw new UnsupportedOperationException(); + } + } + + @Override + public T getValue() { + PacketByteBuf buf = PacketByteBufs.create(); + writePath(buf); + buf = sendRequest("getEntryValue", buf); + return (T) read(buf, valueType); + } + + @Override + public void setValue(T value) { + PacketByteBuf buf = PacketByteBufs.create(); + writePath(buf); + write(buf, valueType, value); + sendRequest("setEntryValue", buf); + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoBase.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoBase.java new file mode 100644 index 0000000..7082a48 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoBase.java @@ -0,0 +1,59 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci.entry; + +import io.gitlab.jfronny.gson.stream.JsonReader; +import io.gitlab.jfronny.gson.stream.JsonWriter; +import io.gitlab.jfronny.libjf.config.api.v2.EntryInfo; +import io.gitlab.jfronny.libjf.config.api.v2.type.Type; +import io.gitlab.jfronny.libjf.config.impl.network.rci.MirrorConfigCategory; +import io.gitlab.jfronny.libjf.config.impl.network.rci.MirrorObject; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.network.PacketByteBuf; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +public abstract class MirrorEntryInfoBase extends MirrorObject implements EntryInfo { + protected final MirrorConfigCategory category; + protected final String entryName; + + public MirrorEntryInfoBase(PacketSender packetSender, MirrorConfigCategory category, String entryName) { + super(packetSender); + this.category = category; + this.entryName = entryName; + } + + protected void writePath(PacketByteBuf buf) { + category.writeCategoryPath(buf); + buf.writeString(entryName); + } + + @Override + public String getName() { + return entryName; + } + + @Override + public void loadFromJson(JsonReader reader) throws IOException, IllegalAccessException { + throw new UnsupportedEncodingException(); + } + + @Override + public void writeTo(JsonWriter writer, String translationPrefix) throws IOException, IllegalAccessException { + throw new UnsupportedEncodingException(); + } + + @Override + public void fix() { + PacketByteBuf buf = PacketByteBufs.create(); + writePath(buf); + sendRequest("fixEntry", buf); + } + + @Override + public void reset() throws IllegalAccessException { + PacketByteBuf buf = PacketByteBufs.create(); + writePath(buf); + sendRequest("resetEntry", buf); + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoUnsupported.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoUnsupported.java new file mode 100644 index 0000000..71f8098 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/rci/entry/MirrorEntryInfoUnsupported.java @@ -0,0 +1,46 @@ +package io.gitlab.jfronny.libjf.config.impl.network.rci.entry; + +import io.gitlab.jfronny.libjf.config.api.v2.type.Type; +import io.gitlab.jfronny.libjf.config.impl.network.rci.MirrorConfigCategory; +import net.fabricmc.fabric.api.networking.v1.PacketSender; + +public class MirrorEntryInfoUnsupported extends MirrorEntryInfoBase { + public MirrorEntryInfoUnsupported(PacketSender packetSender, MirrorConfigCategory category, String entryName) { + super(packetSender, category, entryName); + } + + @Override + public T getDefault() { + throw new UnsupportedOperationException(); + } + + @Override + public T getValue() { + throw new UnsupportedOperationException(); + } + + @Override + public void setValue(T value) { + throw new UnsupportedOperationException(); + } + + @Override + public Type getValueType() { + throw new UnsupportedOperationException(); + } + + @Override + public int getWidth() { + throw new UnsupportedOperationException(); + } + + @Override + public double getMinValue() { + throw new UnsupportedOperationException(); + } + + @Override + public double getMaxValue() { + throw new UnsupportedOperationException(); + } +} diff --git a/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/server/JfConfigNetworkServer.java b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/server/JfConfigNetworkServer.java new file mode 100644 index 0000000..877a158 --- /dev/null +++ b/libjf-config-network-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/network/server/JfConfigNetworkServer.java @@ -0,0 +1,201 @@ +package io.gitlab.jfronny.libjf.config.impl.network.server; + +import io.gitlab.jfronny.libjf.config.api.v2.*; +import io.gitlab.jfronny.libjf.config.api.v2.type.Type; +import io.gitlab.jfronny.libjf.config.impl.network.packet.ConfigurationCompletePacket; +import io.gitlab.jfronny.libjf.config.impl.network.packet.ConfigurationPacket; +import io.gitlab.jfronny.libjf.config.impl.network.RequestRouter; +import io.gitlab.jfronny.libjf.config.impl.network.rci.entry.Datatype; +import io.gitlab.jfronny.libjf.config.impl.network.rci.entry.MirrorEntryInfo; +import net.fabricmc.fabric.api.networking.v1.*; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.Packet; +import net.minecraft.server.network.ServerPlayerConfigurationTask; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.util.*; +import java.util.function.Consumer; + +public class JfConfigNetworkServer { + public static void initialize() { + ServerPlayNetworking.registerGlobalReceiver(RequestRouter.REQUEST_ID, (server, player, handler, buf, responseSender) -> { + if (authenticate(player)) RequestRouter.acceptRequest(buf, responseSender); + else RequestRouter.deny(buf, responseSender); + }); + ServerPlayNetworking.registerGlobalReceiver(RequestRouter.RESPONSE_ID, (server, player, handler, buf, responseSender) -> { + if (authenticate(player)) RequestRouter.acceptResponse(buf, responseSender); + else RequestRouter.deny(buf, responseSender); + }); + ServerConfigurationNetworking.registerGlobalReceiver(ConfigurationCompletePacket.PACKET_TYPE, (packet, networkHandler, responseSender) -> { + networkHandler.completeTask(JfConfigNetworkConfigurationTask.KEY); + }); + ServerConfigurationConnectionEvents.CONFIGURE.register((handler, server) -> { + if (ServerConfigurationNetworking.canSend(handler, ConfigurationPacket.PACKET_TYPE)) { + handler.addTask(new JfConfigNetworkConfigurationTask()); + } + }); + registerRPC(); + } + + public record JfConfigNetworkConfigurationTask() implements ServerPlayerConfigurationTask { + public static final Key KEY = new Key(ConfigurationPacket.ID.toString()); + + @Override + public void sendPacket(Consumer> sender) { + sender.accept(ServerConfigurationNetworking.createS2CPacket(new ConfigurationPacket(RequestRouter.PROTOCOL_VERSION))); + } + + @Override + public Key getKey() { + return KEY; + } + } + + public static void registerRPC() { + ConfigHolder ch = ConfigHolder.getInstance(); + RequestRouter.registerHandler("getRegistered", (buf, followupSender) -> { + PacketByteBuf resp = PacketByteBufs.create(); + resp.writeCollection(ch.getRegistered().keySet(), PacketByteBuf::writeString); + return resp; + }); + RequestRouter.registerHandler("isRegistered", (buf, followupSender) -> { + PacketByteBuf resp = PacketByteBufs.create(); + resp.writeBoolean(ch.isRegistered(buf.readString())); + return resp; + }); + RequestRouter.registerHandler("migrateFiles", (buf, followupSender) -> { + ch.migrateFiles(buf.readString()); + return null; + }); + RequestRouter.registerHandler("load", (buf, followupSender) -> { + readInstance(ch, buf).load(); + return null; + }); + RequestRouter.registerHandler("write", (buf, followupSender) -> { + readInstance(ch, buf).write(); + return null; + }); + RequestRouter.registerHandler("getPresets", (buf, followupSender) -> { + PacketByteBuf resp = PacketByteBufs.create(); + resp.writeCollection(readCategory(ch, buf).getPresets().keySet(), PacketByteBuf::writeString); + return resp; + }); + RequestRouter.registerHandler("runPreset", (buf, followupSender) -> { + Objects.requireNonNull(readCategory(ch, buf).getPresets().get(buf.readString())).run(); + return null; + }); + RequestRouter.registerHandler("getReferencedConfigs", (buf, followupSender) -> { + PacketByteBuf resp = PacketByteBufs.create(); + resp.writeCollection(readCategory(ch, buf).getReferencedConfigs(), (b, ci) -> { + b.writeBoolean(Objects.equals(ch.get(ci.getId()), ci)); + b.writeString(ci.getId()); + }); + return resp; + }); + RequestRouter.registerHandler("getCategories", (buf, followupSender) -> { + PacketByteBuf resp = PacketByteBufs.create(); + resp.writeCollection(readCategory(ch, buf).getCategories().keySet(), PacketByteBuf::writeString); + return resp; + }); + RequestRouter.registerHandler("getEntries", (buf, followupSender) -> { + PacketByteBuf resp = PacketByteBufs.create(); + resp.writeCollection(readCategory(ch, buf).getEntries(), (b, s) -> { + b.writeString(s.getName()); + if (s.supportsRepresentation()) { + Type type = s.getValueType(); + boolean foundType = true; + if (type.isInt()) { + b.writeInt(Datatype.INT.ordinal()); + } else if (type.isLong()) { + b.writeInt(Datatype.LONG.ordinal()); + } else if (type.isFloat()) { + b.writeInt(Datatype.FLOAT.ordinal()); + } else if (type.isDouble()) { + b.writeInt(Datatype.DOUBLE.ordinal()); + } else if (type.isString()) { + b.writeInt(Datatype.STRING.ordinal()); + } else if (type.isBool()) { + b.writeInt(Datatype.BOOL.ordinal()); + } else if (type.isEnum()) { + b.writeInt(Datatype.ENUM.ordinal()); + b.writeCollection(Arrays.stream(type.asEnum().options()).map(Object::toString).toList(), PacketByteBuf::writeString); + } else { + foundType = false; + b.writeInt(-1); + } + if (foundType) { + MirrorEntryInfo.write(b, type, s.getDefault()); + b.writeInt(s.getWidth()); + b.writeDouble(s.getMinValue()); + b.writeDouble(s.getMaxValue()); + } + } else { + b.writeInt(-1); + } + }); + return resp; + }); + RequestRouter.registerHandler("fixEntry", (buf, followupSender) -> { + readEntry(ch, buf).fix(); + return null; + }); + RequestRouter.registerHandler("resetEntry", (buf, followupSender) -> { + readEntry(ch, buf).reset(); + return null; + }); + RequestRouter.registerHandler("getEntryValue", (buf, followupSender) -> { + PacketByteBuf resp = PacketByteBufs.create(); + EntryInfo entryInfo = readEntry(ch, buf); + MirrorEntryInfo.write(resp, entryInfo.getValueType(), entryInfo.getValue()); + return resp; + }); + RequestRouter.registerHandler("setEntryValue", (buf, followupSender) -> { + EntryInfo entryInfo = readEntry(ch, buf); + entryInfo.setValue(MirrorEntryInfo.read(buf, entryInfo.getValueType())); + return null; + }); + } + + private static ConfigInstance readInstance(ConfigHolder ch, PacketByteBuf buf) { + List> parentPaths = buf.readList(s -> s.readList(PacketByteBuf::readString)); + String id = buf.readString(); + if (parentPaths.isEmpty()) return Objects.requireNonNull(ch.get(id)); + parentPaths = new LinkedList<>(parentPaths); + ConfigCategory ci = null; + for (List parentPath : parentPaths) { + ci = ci == null + ? ch.get(parentPath.get(0)) + : getReferencedConfig(ci, parentPath.get(0)); + for (String segment : parentPath.subList(1, parentPath.size() - 1)) { + ci = Objects.requireNonNull(ci.getCategories().get(segment)); + } + } + return getReferencedConfig(Objects.requireNonNull(ci), id); + } + + private static ConfigInstance getReferencedConfig(ConfigCategory category, String id) { + return category.getReferencedConfigs() + .stream() + .filter(s -> s.getId().equals(id)) + .findFirst() + .orElseThrow(); + } + + private static ConfigCategory readCategory(ConfigHolder ch, PacketByteBuf buf) { + ConfigCategory result = readInstance(ch, buf); + for (String s : buf.readList(PacketByteBuf::readString)) { + result = Objects.requireNonNull(result.getCategories().get(s)); + } + return result; + } + + private static EntryInfo readEntry(ConfigHolder ch, PacketByteBuf buf) { + ConfigCategory cat = readCategory(ch, buf); + String name = buf.readString(); + return cat.getEntries().stream().filter(s -> s.getName().equals(name)).findFirst().orElseThrow(); + } + + private static boolean authenticate(ServerPlayerEntity player) { + return player.hasPermissionLevel(2); + } +} diff --git a/libjf-config-network-v0/src/main/resources/fabric.mod.json b/libjf-config-network-v0/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..1ddad2b --- /dev/null +++ b/libjf-config-network-v0/src/main/resources/fabric.mod.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "libjf-config-network-v0", + "name": "LibJF Config: Network", + "version": "${version}", + "authors": ["JFronny"], + "contact": { + "email": "projects.contact@frohnmeyer-wds.de", + "homepage": "https://jfronny.gitlab.io", + "issues": "https://git.frohnmeyer-wds.de/JfMods/LibJF/issues", + "sources": "https://git.frohnmeyer-wds.de/JfMods/LibJF" + }, + "license": "MIT", + "environment": "*", + "entrypoints": { + "server": ["io.gitlab.jfronny.libjf.config.impl.network.server.JfConfigNetworkServer::initialize"], + "client": ["io.gitlab.jfronny.libjf.config.impl.network.client.JfConfigNetworkClient::initialize"] + }, + "depends": { + "fabricloader": ">=0.12.0", + "minecraft": "*", + "fabric-networking-api-v1": "*", + "fabric-command-api-v2": "*", + "libjf-base": ">=${version}", + "libjf-config-core-v2": ">=${version}" + }, + "custom": { + "modmenu": { + "badges": ["library"], + "parent": "libjf" + } + } +} diff --git a/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/TinyConfigScreenFactory.java b/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/TinyConfigScreenFactory.java index 75a3120..8cfeb05 100644 --- a/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/TinyConfigScreenFactory.java +++ b/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/TinyConfigScreenFactory.java @@ -22,8 +22,8 @@ public class TinyConfigScreenFactory implements ConfigScreenFactory> widgets = EntryInfoWidgetBuilder.buildWidgets(config, screen.widgets); + List erroredEntries = new LinkedList<>(); + List> widgets = EntryInfoWidgetBuilder.buildWidgets(config, screen.widgets, erroredEntries); config.fix(); for (WidgetState widget : widgets) { @@ -40,6 +41,10 @@ public class TinyConfigTab implements Tab { this.list.addText(Text.translatable(tooltipPath)); } + for (String erroredEntry : erroredEntries) { + this.list.addText(Text.translatable(ConfigCore.MOD_ID + ".errored_entry", erroredEntry)); + } + if (!isRoot && !config.getPresets().isEmpty()) { this.list.addReference(Text.translatable(ConfigCore.MOD_ID + ".presets"), () -> new PresetsScreen(screen, config, screen::afterSelectPreset)); diff --git a/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/entry/EntryInfoWidgetBuilder.java b/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/entry/EntryInfoWidgetBuilder.java index 6c84b90..842add5 100644 --- a/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/entry/EntryInfoWidgetBuilder.java +++ b/libjf-config-ui-tiny/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/entry/EntryInfoWidgetBuilder.java @@ -30,12 +30,16 @@ public class EntryInfoWidgetBuilder { private static final Pattern INTEGER_ONLY = Pattern.compile("(-?\\d*)"); private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)"); - public static List> buildWidgets(ConfigCategory config, List> knownStates) { + public static List> buildWidgets(ConfigCategory config, List> knownStates, List erroredEntries) { List> knownStates2 = new LinkedList<>(); for (EntryInfo info : config.getEntries()) { - WidgetState state = initEntry(config, info, knownStates); - knownStates.add(state); - knownStates2.add(state); + if (info.supportsRepresentation()) { + WidgetState state = initEntry(config, info, knownStates); + knownStates.add(state); + knownStates2.add(state); + } else { + erroredEntries.add(info.getName()); + } } return knownStates2; } diff --git a/libjf-config-ui-tiny/src/testmod/java/io/gitlab/jfronny/libjf/config/test/tiny/TestConfig.java b/libjf-config-ui-tiny/src/testmod/java/io/gitlab/jfronny/libjf/config/test/tiny/TestConfig.java index 4fe7487..4b3802d 100644 --- a/libjf-config-ui-tiny/src/testmod/java/io/gitlab/jfronny/libjf/config/test/tiny/TestConfig.java +++ b/libjf-config-ui-tiny/src/testmod/java/io/gitlab/jfronny/libjf/config/test/tiny/TestConfig.java @@ -6,6 +6,7 @@ import io.gitlab.jfronny.libjf.config.api.v2.dsl.DSL; public class TestConfig implements JfCustomConfig { private int value1 = 0; private double doubleValue = 0.3; + private double doubleValue2 = 0.3; private String value2 = ""; private boolean value3 = false; private int value4 = 0; @@ -18,6 +19,7 @@ public class TestConfig implements JfCustomConfig { .category("ca1", builder1 -> builder1 .value("value1", value1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, () -> value1, v -> value1 = v) .value("doubleValue", doubleValue, -0.74, 1.6, () -> doubleValue, v -> doubleValue = v) + .category("nestedCa", builder2 -> builder2.value("doubleValue", doubleValue2, 12, 47, () -> doubleValue2, v -> doubleValue2 = v)) ).category("ca2", builder1 -> builder1 .value("value2", value2, () -> value2, v -> value2 = v) ).category("ca3", builder1 -> builder1 diff --git a/settings.gradle.kts b/settings.gradle.kts index 83731fb..e44249e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,7 @@ include("libjf-base") include("libjf-config-core-v2") include("libjf-config-commands") +include("libjf-config-network-v0") include("libjf-config-ui-tiny") include("libjf-config-compiler-plugin-v2")