From 2752dc6b1aaab37de01d2c30d8930121e12ee5db Mon Sep 17 00:00:00 2001 From: Lyft <127234178+Lyfts@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:06:17 +0100 Subject: [PATCH] Easily sync config values between server & client (#102) --- .../gtnewhorizon/gtnhlib/config/Config.java | 17 ++ .../gtnhlib/config/ConfigFieldParser.java | 179 ++++++++++++++++++ .../gtnhlib/config/ConfigSyncHandler.java | 67 +++++++ .../gtnhlib/config/ConfigurationManager.java | 31 ++- .../gtnhlib/config/PacketSyncConfig.java | 57 ++++++ .../gtnhlib/config/SyncedConfigElement.java | 44 +++++ .../gtnhlib/network/NetworkHandler.java | 2 + 7 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigSyncHandler.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/config/PacketSyncConfig.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/config/SyncedConfigElement.java diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java index a0bf1ca..f633e6d 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java @@ -224,4 +224,21 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @interface ExcludeFromAutoGui {} + + /** + * Fields or classes annotated with this will automatically be synced from server -> client. If applied to a class, + * all fields (including subcategories) in the class will be synced. All fields are restored to their original value + * when the player disconnects. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.TYPE }) + @interface Sync { + + /** + * Can be used to overwrite the sync behavior for fields in classes annotated with {@link Sync}. + * + * @return Whether the field should be synced. Defaults to true. + */ + boolean value() default true; + } } diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java index 65050ff..cda52d1 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java @@ -142,6 +142,39 @@ private static boolean isModDetected(Config.ModDetectedDefault modDefault) { }); } + static String getValueAsString(@Nullable Object instance, Field field) throws ConfigException { + try { + val parser = getParser(field); + return parser.getAsString(instance, field); + } catch (Exception e) { + throw new ConfigException( + "Failed to get value as string for field " + field.getName() + + " of type " + + field.getType().getSimpleName() + + " in class " + + field.getDeclaringClass().getName() + + ". Caused by: " + + e); + } + } + + static void setValueFromString(@Nullable Object instance, Field field, String value) throws ConfigException { + try { + val parser = getParser(field); + parser.setFromString(instance, value, field); + } catch (Exception e) { + throw new ConfigException( + "Failed to set value from string for field " + field.getName() + + " of type " + + field.getType().getSimpleName() + + " in class " + + field.getDeclaringClass().getName() + + ". Caused by: " + + e); + } + + } + @SneakyThrows private static Field extractField(Class clazz, String field) { return clazz.getDeclaredField(field); @@ -158,6 +191,10 @@ void load(@Nullable Object instance, @Nullable String defValueString, Field fiel String category, String name, String comment, String langKey); void save(@Nullable Object instance, Field field, Configuration config, String category, String name); + + void setFromString(@Nullable Object instance, String value, Field field); + + String getAsString(@Nullable Object instance, Field field); } private static class BooleanParser implements Parser { @@ -194,6 +231,25 @@ private boolean fromStringOrDefault(@Nullable Object instance, @Nullable String return Boolean.parseBoolean(defValueString); } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + val boxed = field.getType().equals(Boolean.class); + if (boxed) { + field.set(instance, Boolean.parseBoolean(value)); + return; + } + + field.setBoolean(instance, Boolean.parseBoolean(value)); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + val boxed = field.getType().equals(Boolean.class); + return Boolean.toString(boxed ? (Boolean) field.get(instance) : field.getBoolean(instance)); + } } private static class IntParser implements Parser { @@ -228,6 +284,25 @@ private int fromStringOrDefault(@Nullable Object instance, @Nullable String defV return Integer.parseInt(defValueString); } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + val boxed = field.getType().equals(Integer.class); + if (boxed) { + field.set(instance, Integer.parseInt(value)); + return; + } + + field.setInt(instance, Integer.parseInt(value)); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + val boxed = field.getType().equals(Integer.class); + return Integer.toString(boxed ? (Integer) field.get(instance) : field.getInt(instance)); + } } private static class FloatParser implements Parser { @@ -262,6 +337,25 @@ private float fromStringOrDefault(@Nullable Object instance, @Nullable String de return Float.parseFloat(defValueString); } + + @Override + @SneakyThrows + public void setFromString(@org.jetbrains.annotations.Nullable Object instance, String value, Field field) { + val boxed = field.getType().equals(Float.class); + if (boxed) { + field.set(instance, Float.parseFloat(value)); + return; + } + + field.setFloat(instance, Float.parseFloat(value)); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + val boxed = field.getType().equals(Float.class); + return Float.toString(boxed ? (Float) field.get(instance) : field.getFloat(instance)); + } } private static class DoubleParser implements Parser { @@ -301,6 +395,25 @@ private double fromStringOrDefault(@Nullable Object instance, @Nullable String d return Double.parseDouble(defValueString); } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + val boxed = field.getType().equals(Double.class); + if (boxed) { + field.set(instance, Double.parseDouble(value)); + return; + } + + field.setDouble(instance, Double.parseDouble(value)); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + val boxed = field.getType().equals(Double.class); + return Double.toString(boxed ? (Double) field.get(instance) : field.getDouble(instance)); + } } private static class StringParser implements Parser { @@ -331,6 +444,18 @@ private String fromStringOrDefault(@Nullable Object instance, @Nullable String d return defValueString; } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + field.set(instance, value); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + return (String) field.get(instance); + } } private static class EnumParser implements Parser { @@ -415,6 +540,22 @@ private Enum fromStringOrDefault(@Nullable Object instance, @Nullable String } return null; } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + Field enumField = field.getType().getDeclaredField(value); + if (!enumField.isEnumConstant()) { + throw new NoSuchFieldException(); + } + field.set(instance, enumField.get(instance)); + } + + @Override + @SneakyThrows + public String getAsString(@org.jetbrains.annotations.Nullable Object instance, Field field) { + return ((Enum) field.get(instance)).name(); + } } private static class StringArrayParser implements Parser { @@ -446,6 +587,18 @@ private String[] fromStringOrDefault(@Nullable Object instance, @Nullable String } return value == null ? new String[0] : value; } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + field.set(instance, value.split("|||")); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + return String.join("|||", (String[]) field.get(instance)); + } } private static class DoubleArrayParser implements Parser { @@ -486,6 +639,19 @@ private double[] fromStringOrDefault(@Nullable Object instance, @Nullable String return value == null ? new double[0] : value; } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + field.set(instance, Arrays.stream(value.split(",")).mapToDouble(Double::parseDouble).toArray()); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + return Arrays.stream((double[]) field.get(instance)).mapToObj(Double::toString) + .collect(Collectors.joining(",")); + } } private static class IntArrayParser implements Parser { @@ -524,5 +690,18 @@ private int[] fromStringOrDefault(@Nullable Object instance, @Nullable String de return value == null ? new int[0] : value; } + + @Override + @SneakyThrows + public void setFromString(@Nullable Object instance, String value, Field field) { + field.set(instance, Arrays.stream(value.split(",")).mapToInt(Integer::parseInt).toArray()); + } + + @Override + @SneakyThrows + public String getAsString(@Nullable Object instance, Field field) { + return Arrays.stream((int[]) field.get(instance)).mapToObj(Integer::toString) + .collect(Collectors.joining(",")); + } } } diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigSyncHandler.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigSyncHandler.java new file mode 100644 index 0000000..82a0add --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigSyncHandler.java @@ -0,0 +1,67 @@ +package com.gtnewhorizon.gtnhlib.config; + +import static com.gtnewhorizon.gtnhlib.config.ConfigurationManager.LOGGER; + +import java.util.Map; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.integrated.IntegratedServer; + +import com.gtnewhorizon.gtnhlib.network.NetworkHandler; + +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.PlayerEvent; +import cpw.mods.fml.common.network.FMLNetworkEvent; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +@SuppressWarnings("unused") +public final class ConfigSyncHandler { + + static final Map syncedElements = new Object2ObjectOpenHashMap<>(); + private static boolean hasSyncedValues = false; + + static { + FMLCommonHandler.instance().bus().register(new ConfigSyncHandler()); + } + + @SubscribeEvent + public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { + if (!(event.player instanceof EntityPlayerMP playerMP)) return; + MinecraftServer server = MinecraftServer.getServer(); + // no point in syncing in from client -> client. + if (server.isSinglePlayer() && !((IntegratedServer) server).getPublic()) { + return; + } + NetworkHandler.instance.sendTo(new PacketSyncConfig(syncedElements.values()), playerMP); + } + + @SubscribeEvent + public void onClientDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) { + if (!hasSyncedValues) return; + hasSyncedValues = false; + for (SyncedConfigElement element : syncedElements.values()) { + element.restoreValue(); + } + } + + static void onSync(PacketSyncConfig packet) { + for (Object2ObjectMap.Entry entry : packet.syncedElements.object2ObjectEntrySet()) { + SyncedConfigElement element = syncedElements.get(entry.getKey()); + if (element != null) { + try { + hasSyncedValues = true; + element.setSyncValue(entry.getValue()); + } catch (ConfigException e) { + LOGGER.error("Failed to sync element {}", element, e); + } + } + } + } + + static boolean hasSyncedValues() { + return hasSyncedValues; + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java index 4bbc23c..e5e5a00 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java @@ -38,6 +38,7 @@ * Class for controlling the loading of configuration files. */ @NoArgsConstructor(access = AccessLevel.PRIVATE) +@SuppressWarnings("unused") public class ConfigurationManager { static final Logger LOGGER = LogManager.getLogger("GTNHLibConfig"); @@ -122,6 +123,11 @@ private static void save(Class configClass, Object instance, Configuration ra continue; } + if (ConfigSyncHandler.hasSyncedValues()) { + SyncedConfigElement element = ConfigSyncHandler.syncedElements.get(field.toString()); + if (element != null && element.isSynced()) continue; + } + field.setAccessible(true); if (!ConfigFieldParser.canParse(field)) { @@ -172,9 +178,12 @@ private static void processConfigInternal(Class configClass, String category, NoSuchFieldException, ConfigException { boolean foundCategory = !category.isEmpty(); ConfigCategory cat = foundCategory ? rawConfig.getCategory(category) : null; + Config.Sync sync = getClassOrBaseAnnotation(configClass, Config.Sync.class); + boolean syncCategory = sync != null && sync.value(); boolean requiresMcRestart = getClassOrBaseAnnotation(configClass, Config.RequiresMcRestart.class) != null || foundCategory && cat.requiresMcRestart(); - boolean requiresWorldRestart = getClassOrBaseAnnotation(configClass, Config.RequiresWorldRestart.class) != null + boolean requiresWorldRestart = syncCategory + || getClassOrBaseAnnotation(configClass, Config.RequiresWorldRestart.class) != null || foundCategory && cat.requiresWorldRestart(); List observedValues = new ArrayList<>(); @@ -216,6 +225,26 @@ private static void processConfigInternal(Class configClass, String category, ConfigFieldParser.loadField(instance, field, rawConfig, category, langKey); observedValues.add(fieldName); + Config.Sync fieldSync = field.getAnnotation(Config.Sync.class); + if (fieldSync != null && fieldSync.value() || syncCategory && fieldSync == null) { + SyncedConfigElement element = new SyncedConfigElement(instance, field, () -> { + try { + ConfigFieldParser.loadField(instance, field, rawConfig, category, langKey); + } catch (ConfigException e) { + LOGGER.error( + "Failed to restore synced field {} in class {}", + fieldName, + field.getDeclaringClass(), + e); + } + }); + ConfigSyncHandler.syncedElements.put(field.toString(), element); + + if (!requiresWorldRestart) { + cat.get(fieldName).setRequiresWorldRestart(true); + } + } + if (!requiresMcRestart) { requiresMcRestart = field.isAnnotationPresent(Config.RequiresMcRestart.class); } diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/PacketSyncConfig.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/PacketSyncConfig.java new file mode 100644 index 0000000..ce8dce0 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/PacketSyncConfig.java @@ -0,0 +1,57 @@ +package com.gtnewhorizon.gtnhlib.config; + +import java.util.Collection; + +import com.gtnewhorizon.gtnhlib.GTNHLib; + +import cpw.mods.fml.common.network.ByteBufUtils; +import cpw.mods.fml.common.network.simpleimpl.IMessage; +import cpw.mods.fml.common.network.simpleimpl.IMessageHandler; +import cpw.mods.fml.common.network.simpleimpl.MessageContext; +import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +public class PacketSyncConfig implements IMessage { + + public final Object2ObjectMap syncedElements = new Object2ObjectOpenHashMap<>(); + + @SuppressWarnings("unused") + public PacketSyncConfig() {} + + PacketSyncConfig(Collection elements) { + for (SyncedConfigElement element : elements) { + try { + syncedElements.put(element.toString(), element.getValue()); + } catch (ConfigException e) { + GTNHLib.LOG.error("Failed to serialize config element: {}", element.toString(), e); + } + } + } + + @Override + public void fromBytes(ByteBuf buf) { + int size = buf.readInt(); + for (int i = 0; i < size; i++) { + syncedElements.put(ByteBufUtils.readUTF8String(buf), ByteBufUtils.readUTF8String(buf)); + } + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(syncedElements.size()); + for (Object2ObjectMap.Entry entry : syncedElements.object2ObjectEntrySet()) { + ByteBufUtils.writeUTF8String(buf, entry.getKey()); + ByteBufUtils.writeUTF8String(buf, entry.getValue()); + } + } + + public static class Handler implements IMessageHandler { + + @Override + public IMessage onMessage(PacketSyncConfig message, MessageContext ctx) { + ConfigSyncHandler.onSync(message); + return null; + } + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/SyncedConfigElement.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/SyncedConfigElement.java new file mode 100644 index 0000000..7d69c3d --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/SyncedConfigElement.java @@ -0,0 +1,44 @@ +package com.gtnewhorizon.gtnhlib.config; + +import java.lang.reflect.Field; + +import javax.annotation.Nullable; + +import org.jetbrains.annotations.NotNull; + +public final class SyncedConfigElement { + + private final Object instance; + private final Field field; + private final Runnable restore; + private boolean synced = false; + + SyncedConfigElement(@Nullable Object instance, @NotNull Field field, @NotNull Runnable restore) { + this.instance = instance; + this.field = field; + this.restore = restore; + } + + void setSyncValue(String value) throws ConfigException { + ConfigFieldParser.setValueFromString(instance, field, value); + synced = true; + } + + void restoreValue() { + restore.run(); + synced = false; + } + + boolean isSynced() { + return synced; + } + + public String getValue() throws ConfigException { + return ConfigFieldParser.getValueAsString(instance, field); + } + + @Override + public String toString() { + return field.toString(); + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java b/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java index 7294946..f2dbc47 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/network/NetworkHandler.java @@ -1,6 +1,7 @@ package com.gtnewhorizon.gtnhlib.network; import com.gtnewhorizon.gtnhlib.GTNHLib; +import com.gtnewhorizon.gtnhlib.config.PacketSyncConfig; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper; @@ -16,5 +17,6 @@ public static void init() { PacketMessageAboveHotbar.class, 0, Side.CLIENT); + instance.registerMessage(PacketSyncConfig.Handler.class, PacketSyncConfig.class, 1, Side.CLIENT); } }