feat(config-network): implement relevant config abstractions over network and add clientside GUI for server config
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/jfmod Pipeline was successful Details

This commit is contained in:
Johannes Frohnmeyer 2023-10-08 17:26:08 +02:00
parent 1f9da79302
commit 172d4d5b79
Signed by: Johannes
GPG Key ID: E76429612C2929F4
32 changed files with 1156 additions and 11 deletions

View File

@ -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") }
}

View File

@ -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 <T> void registerEntry(ConfigCategory config, String subpath, LiteralArgumentBuilder<ServerCommandSource> cns, EntryInfo<T> entry) {
if (!entry.supportsRepresentation()) return;
LiteralArgumentBuilder<ServerCommandSource> c_entry = literal(entry.getName()).executes(context -> {
String msg = "The value of " + subpath + "." + entry.getName() + " is ";
Text visualized = visualizeOption(config, entry, tryRun(entry::getValue));

View File

@ -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<S extends Screen, B extends ConfigScreenFactory.Built<S>> {
static ConfigScreenFactory<?, ?> getInstance() {
return ConfigScreenFactoryDiscovery.getConfigured2();
@ -13,6 +17,19 @@ public interface ConfigScreenFactory<S extends Screen, B extends ConfigScreenFac
int getPriority();
default Screen createOverview(Screen parent) {
return createOverview(ConfigHolder.getInstance(), parent);
}
default Screen createOverview(ConfigHolder holder, Screen parent) {
return create(DSL.create("overview").config(builder -> {
Objects.requireNonNull(holder).getRegistered().forEach((n, ci) -> {
builder.referenceConfig(ci);
});
return builder;
}), parent).get();
}
interface Built<S extends Screen> {
S get();

View File

@ -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",

View File

@ -51,6 +51,14 @@ public interface EntryInfo<T> {
*/
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.
*/

View File

@ -162,7 +162,7 @@ public sealed interface Type {
}
}
final record TEnum<T>(@Nullable Class<T> klazz, String name, T[] options) implements Type {
record TEnum<T>(@Nullable Class<T> klazz, String name, T[] options) implements Type {
public TEnum(Class<T> 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;

View File

@ -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))!!)
}

View File

@ -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
});
}
}

View File

@ -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;
}
}));
});
}
}

View File

@ -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;
}
}

View File

@ -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<String, RequestHandler> temporaryHandlers);
}

View File

@ -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<Response> 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);
}
}
}
}

View File

@ -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;
}

View File

@ -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<String, RequestHandler> persistendHandlers = new HashMap<>();
private static final Map<Long, Request> 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<String, RequestHandler> temporaryHandlers) {
sendRequest(responseSender, null, name, body, responseHandler, temporaryHandlers);
}
private static void sendRequest(PacketSender responseSender, Long parent, String name, PacketByteBuf body, ResponseHandler responseHandler, Map<String, RequestHandler> temporaryHandlers) {
long id;
synchronized (currentRequests) {
Set<Long> 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<String, RequestHandler> temporaryHandlers, ResponseHandler responseHandler) {}
}

View File

@ -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);
}

View File

@ -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<ConfigurationCompletePacket> 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;
}
}

View File

@ -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<ConfigurationPacket> 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;
}
}

View File

@ -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<MirrorConfigInstance> root;
public MirrorConfigCategory(PacketSender packetSender, String id, String categoryPath, Supplier<MirrorConfigInstance> 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<String> getCategoryPathList() {
if (getCategoryPath().isEmpty()) return List.of();
String[] sc = getCategoryPath().split("\\.");
return Arrays.stream(sc).toList();
}
@Override
public List<EntryInfo<?>> 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<String, Runnable> 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<ConfigInstance> 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<String, ConfigCategory> 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();
}
}

View File

@ -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<String, ConfigInstance> 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);
}
}

View File

@ -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<List<String>> parentPaths;
protected MirrorConfigInstance(PacketSender packetSender, String id, Supplier<MirrorConfigInstance> root, List<List<String>> 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<List<String>> parentPaths) {
MirrorConfigInstance[] cis = {null};
cis[0] = new MirrorConfigInstance(packetSender, id, () -> cis[0], parentPaths);
return cis[0];
}
public Stream<List<String>> 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<Path> getFilePath() {
return Optional.empty();
}
}

View File

@ -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> T synchronize(Consumer<Hold<T>> 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<String, RequestHandler> temporaryHandlers) {
return this.<PMResponseHandler.Response>synchronize(hold -> {
RequestRouter.sendRequest(packetSender, name, body, new PMResponseHandler(hold::resume), temporaryHandlers);
}).get();
}
protected interface Hold<T> {
void resume(T with);
}
}

View File

@ -0,0 +1,5 @@
package io.gitlab.jfronny.libjf.config.impl.network.rci.entry;
public enum Datatype {
INT, LONG, FLOAT, DOUBLE, STRING, BOOL, ENUM
}

View File

@ -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<T> extends MirrorEntryInfoBase<T> {
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);
}
}

View File

@ -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<T> extends MirrorObject implements EntryInfo<T> {
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);
}
}

View File

@ -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<T> extends MirrorEntryInfoBase<T> {
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();
}
}

View File

@ -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<Packet<?>> 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<List<String>> 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<String> 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);
}
}

View File

@ -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"
}
}
}

View File

@ -22,8 +22,8 @@ public class TinyConfigScreenFactory implements ConfigScreenFactory<Screen, Tiny
&& config.getReferencedConfigs().isEmpty()
&& config.getCategories().isEmpty()) {
EntryInfo entry = config.getEntries().get(0);
Type type = entry.getValueType();
if (!type.isInt() && !type.isLong() && !type.isFloat() && !type.isDouble() && !type.isString() && !type.isBool() && !type.isEnum()) {
Type type = entry.supportsRepresentation() ? entry.getValueType() : null;
if (type != null && !type.isInt() && !type.isLong() && !type.isFloat() && !type.isDouble() && !type.isString() && !type.isBool() && !type.isEnum()) {
final String jsonified;
try {
jsonified = GsonHolders.CONFIG.getGson().toJson(entry.getValue());

View File

@ -25,7 +25,8 @@ public class TinyConfigTab implements Tab {
public TinyConfigTab(TinyConfigScreen screen, ConfigCategory config, TextRenderer textRenderer, boolean isRoot) {
this.config = config;
List<WidgetState<?>> widgets = EntryInfoWidgetBuilder.buildWidgets(config, screen.widgets);
List<String> erroredEntries = new LinkedList<>();
List<WidgetState<?>> 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));

View File

@ -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<WidgetState<?>> buildWidgets(ConfigCategory config, List<WidgetState<?>> knownStates) {
public static List<WidgetState<?>> buildWidgets(ConfigCategory config, List<WidgetState<?>> knownStates, List<String> erroredEntries) {
List<WidgetState<?>> 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;
}

View File

@ -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

View File

@ -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")