diff --git a/common/src/main/java/com/seibel/lod/common/Config.java b/common/src/main/java/com/seibel/lod/common/Config.java index 12304169d..aafba7302 100644 --- a/common/src/main/java/com/seibel/lod/common/Config.java +++ b/common/src/main/java/com/seibel/lod/common/Config.java @@ -20,6 +20,7 @@ package com.seibel.lod.common; import com.seibel.lod.common.wrappers.config.ConfigGui; +import com.seibel.lod.common.wrappers.config.TinyConfig; import com.seibel.lod.core.ModInfo; import com.seibel.lod.core.enums.config.*; import com.seibel.lod.core.enums.rendering.*; @@ -33,6 +34,7 @@ import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton.I * @version 12-12-2021 */ public class Config extends ConfigGui +//public class Config extends TinyConfig { // CONFIG STRUCTURE // -> Client diff --git a/common/src/main/java/com/seibel/lod/common/LodCommonMain.java b/common/src/main/java/com/seibel/lod/common/LodCommonMain.java index 06ad16c23..014f9e7fa 100644 --- a/common/src/main/java/com/seibel/lod/common/LodCommonMain.java +++ b/common/src/main/java/com/seibel/lod/common/LodCommonMain.java @@ -3,6 +3,7 @@ package com.seibel.lod.common; import com.seibel.lod.common.forge.LodForgeMethodCaller; import com.seibel.lod.common.wrappers.DependencySetup; import com.seibel.lod.common.wrappers.config.ConfigGui; +import com.seibel.lod.common.wrappers.config.TinyConfig; import com.seibel.lod.core.ModInfo; /** @@ -28,4 +29,17 @@ public class LodCommonMain { public static void initConfig() { ConfigGui.init(ModInfo.ID, Config.class); } +// public static void initConfig() { +// TinyConfig.init(ModInfo.ID, Config.class); +// TinyConfig.init(ModInfo.ID, Config.Client.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Advanced.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Advanced.Buffers.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Advanced.Debugging.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Advanced.Threading.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Graphics.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Graphics.AdvancedGraphics.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Graphics.FogQuality.class); +// TinyConfig.init(ModInfo.ID, Config.Client.Graphics.Quality.class); +// TinyConfig.init(ModInfo.ID, Config.Client.WorldGenerator.class); +// } } diff --git a/common/src/main/java/com/seibel/lod/common/wrappers/config/ConfigGui.java b/common/src/main/java/com/seibel/lod/common/wrappers/config/ConfigGui.java index e770d424c..a2a5a2111 100644 --- a/common/src/main/java/com/seibel/lod/common/wrappers/config/ConfigGui.java +++ b/common/src/main/java/com/seibel/lod/common/wrappers/config/ConfigGui.java @@ -137,6 +137,7 @@ public abstract class ConfigGui { String id; // ModID TranslatableComponent name; int index; + boolean hideOption = false; // Hides the button boolean button = false; // This asks if it is a button to goto a new screen String gotoScreen = ""; // This is only called if button is true String category; @@ -171,7 +172,6 @@ public abstract class ConfigGui { private static void initClass(String modid, Class config, String category) { String e = modid + (category != "" ? "." + category : ""); configClass.put(e, config); -// nestedClasses.add(e); for (Field field : config.getFields()) { EntryInfo info = new EntryInfo(); if (field.isAnnotationPresent(Entry.class) || field.isAnnotationPresent(Comment.class) || field.isAnnotationPresent(ScreenEntry.class)) @@ -278,7 +278,6 @@ public abstract class ConfigGui { try { if (!Files.exists(path)) Files.createFile(path); - new TomlWriter().write(configClass.get(modid).getDeclaredConstructor().newInstance(), path.toFile()); } catch (Exception e) { e.printStackTrace(); @@ -354,7 +353,7 @@ public abstract class ConfigGui { if (this.minecraft != null && this.minecraft.level != null) this.list.setRenderBackground(false); this.addWidget(this.list); for (EntryInfo info : entries) { - if (info.id.equals(modid) && info.category.matches(category)) { + if (info.id.equals(modid) && info.category.matches(category) && !info.hideOption) { TranslatableComponent name = Objects.requireNonNullElseGet(info.name, () -> new TranslatableComponent(translationPrefix + (info.category != "" ? info.category + "." : "") + info.field.getName())); Button resetButton = new Button(this.width - ConfigScreenConfigs.SpaceFromRightScreen - info.width - ConfigScreenConfigs.ButtonWidthSpacing - ConfigScreenConfigs.ResetButtonWidth, 0, ConfigScreenConfigs.ResetButtonWidth, 20, new TextComponent("Reset").withStyle(ChatFormatting.RED), (button -> { info.value = info.defaultValue; diff --git a/common/src/main/java/com/seibel/lod/common/wrappers/config/TinyConfig.java b/common/src/main/java/com/seibel/lod/common/wrappers/config/TinyConfig.java new file mode 100644 index 000000000..348068d58 --- /dev/null +++ b/common/src/main/java/com/seibel/lod/common/wrappers/config/TinyConfig.java @@ -0,0 +1,414 @@ +package com.seibel.lod.common.wrappers.config; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mojang.blaze3d.vertex.PoseStack; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Based on https://github.com/Minenash/TinyConfig + * Credits to Minenash + * + * @author coolGi2007 + */ + +// Original tiny config updated to use mojang mappings +// This is only used for testing and should be deleted once testing is done +@SuppressWarnings("unchecked") +public abstract class TinyConfig { + private static final Pattern INTEGER_ONLY = Pattern.compile("(-?[0-9]*)"); + private static final Pattern DECIMAL_ONLY = Pattern.compile("-?([\\d]+\\.?[\\d]*|[\\d]*\\.?[\\d]+|\\.)"); + + private static final List entries = new ArrayList<>(); + + protected static class EntryInfo { + Field field; + Object widget; + int width; + int max; + Map.Entry error; + Object defaultValue; + Object value; + String tempValue; + boolean inLimits = true; + String id; + TranslatableComponent name; + int index; + } + + public static final Map> configClass = new HashMap<>(); + private static Path path; + + private static final Gson gson = new GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).excludeFieldsWithModifiers(Modifier.PRIVATE).addSerializationExclusionStrategy(new HiddenAnnotationExclusionStrategy()).setPrettyPrinting().create(); + + public static void init(String modid, Class config) { + path = FabricLoader.getInstance().getConfigDir().resolve(modid + ".json"); + configClass.put(modid, config); + + for (Field field : config.getFields()) { + EntryInfo info = new EntryInfo(); + if (field.isAnnotationPresent(Entry.class) || field.isAnnotationPresent(Comment.class)) + if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) initClient(modid, field, info); + if (field.isAnnotationPresent(Entry.class)) + try { + info.defaultValue = field.get(null); + } catch (IllegalAccessException ignored) {} + } + try { gson.fromJson(Files.newBufferedReader(path), config); } + catch (Exception e) { write(modid); } + + for (EntryInfo info : entries) { + if (info.field.isAnnotationPresent(Entry.class)) + try { + info.value = info.field.get(null); + info.tempValue = info.value.toString(); + } catch (IllegalAccessException ignored) { + } + } + } + @Environment(EnvType.CLIENT) + private static void initClient(String modid, Field field, EntryInfo info) { + Class type = field.getType(); + Entry e = field.getAnnotation(Entry.class); + info.width = e != null ? e.width() : 0; + info.field = field; + info.id = modid; + + if (e != null) { + if (!e.name().equals("")) info.name = new TranslatableComponent(e.name()); + if (type == int.class) textField(info, Integer::parseInt, INTEGER_ONLY, e.min(), e.max(), true); + else if (type == double.class) textField(info, Double::parseDouble, DECIMAL_ONLY, e.min(), e.max(), false); + else if (type == String.class || type == List.class) { + info.max = e.max() == Double.MAX_VALUE ? Integer.MAX_VALUE : (int) e.max(); + textField(info, String::length, null, Math.min(e.min(), 0), Math.max(e.max(), 1), true); + } else if (type == boolean.class) { + Function func = value -> new TextComponent((Boolean) value ? "True" : "False").withStyle((Boolean) value ? ChatFormatting.GREEN : ChatFormatting.RED); + info.widget = new AbstractMap.SimpleEntry>(button -> { + info.value = !(Boolean) info.value; + button.setMessage(func.apply(info.value)); + }, func); + } else if (type.isEnum()) { + List values = Arrays.asList(field.getType().getEnumConstants()); + Function func = value -> new TranslatableComponent(modid + ".config." + "enum." + type.getSimpleName() + "." + info.value.toString()); + info.widget = new AbstractMap.SimpleEntry>(button -> { + int index = values.indexOf(info.value) + 1; + info.value = values.get(index >= values.size() ? 0 : index); + button.setMessage(func.apply(info.value)); + }, func); + } + } + entries.add(info); + } + + private static void textField(EntryInfo info, Function f, Pattern pattern, double min, double max, boolean cast) { + boolean isNumber = pattern != null; + info.widget = (BiFunction>) (t, b) -> s -> { + s = s.trim(); + if (!(s.isEmpty() || !isNumber || pattern.matcher(s).matches())) return false; + + Number value = 0; + boolean inLimits = false; + info.error = null; + if (!(isNumber && s.isEmpty()) && !s.equals("-") && !s.equals(".")) { + value = f.apply(s); + inLimits = value.doubleValue() >= min && value.doubleValue() <= max; + info.error = inLimits? null : new AbstractMap.SimpleEntry<>(t, new TextComponent(value.doubleValue() < min ? + "§cMinimum " + (isNumber? "value" : "length") + (cast? " is " + (int)min : " is " + min) : + "§cMaximum " + (isNumber? "value" : "length") + (cast? " is " + (int)max : " is " + max))); + } + + info.tempValue = s; + t.setTextColor(inLimits? 0xFFFFFFFF : 0xFFFF7777); + info.inLimits = inLimits; + b.active = entries.stream().allMatch(e -> e.inLimits); + + if (inLimits && info.field.getType() != List.class) + info.value = isNumber? value : s; + else if (inLimits) { + if (((List) info.value).size() == info.index) ((List) info.value).add(""); + ((List) info.value).set(info.index, Arrays.stream(info.tempValue.replace("[", "").replace("]", "").split(", ")).toList().get(0)); + } + + return true; + }; + } + + public static void write(String modid) { + path = FabricLoader.getInstance().getConfigDir().resolve(modid + ".json"); + try { + if (!Files.exists(path)) Files.createFile(path); + Files.write(path, gson.toJson(configClass.get(modid).getDeclaredConstructor().newInstance()).getBytes()); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Environment(EnvType.CLIENT) + public static Screen getScreen(Screen parent, String modid, String a) { + return new ConfigScreen(parent, modid); + } + @Environment(EnvType.CLIENT) + private static class ConfigScreen extends Screen { + protected ConfigScreen(Screen parent, String modid) { + super(new TranslatableComponent(modid + ".config." + "title")); + this.parent = parent; + this.modid = modid; + this.translationPrefix = modid + ".config."; + } + private final String translationPrefix; + private final Screen parent; + private final String modid; + private MidnightConfigListWidget list; + private boolean reload = false; + + // Real Time config update // + @Override + public void tick() { + super.tick(); + for (EntryInfo info : entries) { + try {info.field.set(null, info.value);} catch (IllegalAccessException ignored) {} + } + } + private void loadValues() { + try { gson.fromJson(Files.newBufferedReader(path), configClass.get(modid)); } + catch (Exception e) { write(modid); } + + for (EntryInfo info : entries) { + if (info.field.isAnnotationPresent(Entry.class)) + try { + info.value = info.field.get(null); + info.tempValue = info.value.toString(); + } catch (IllegalAccessException ignored) {} + } + } + @Override + protected void init() { + super.init(); + if (!reload) loadValues(); + + this.addRenderableWidget(new Button(this.width / 2 - 154, this.height - 28, 150, 20, CommonComponents.GUI_CANCEL, button -> { + loadValues(); + Objects.requireNonNull(minecraft).setScreen(parent); + })); + + Button done = this.addRenderableWidget(new Button(this.width / 2 + 4, this.height - 28, 150, 20, CommonComponents.GUI_DONE, (button) -> { + for (EntryInfo info : entries) + if (info.id.equals(modid)) { + try { + info.field.set(null, info.value); + } catch (IllegalAccessException ignored) {} + } + write(modid); + Objects.requireNonNull(minecraft).setScreen(parent); + })); + + this.list = new MidnightConfigListWidget(this.minecraft, this.width, this.height, 32, this.height - 32, 25); + if (this.minecraft != null && this.minecraft.level != null) this.list.setRenderBackground(false); + this.addWidget(this.list); + for (EntryInfo info : entries) { + if (info.id.equals(modid)) { + TranslatableComponent name = Objects.requireNonNullElseGet(info.name, () -> new TranslatableComponent(translationPrefix + info.field.getName())); + Button resetButton = new Button(width - 205, 0, 40, 20, new TextComponent("Reset").withStyle(ChatFormatting.RED), (button -> { + info.value = info.defaultValue; + info.tempValue = info.defaultValue.toString(); + info.index = 0; + double scrollAmount = list.getScrollAmount(); + this.reload = true; + Objects.requireNonNull(minecraft).setScreen(this); + list.setScrollAmount(scrollAmount); + })); + + if (info.widget instanceof Map.Entry) { + Map.Entry> widget = (Map.Entry>) info.widget; + if (info.field.getType().isEnum()) widget.setValue(value -> new TranslatableComponent(translationPrefix + "enum." + info.field.getType().getSimpleName() + "." + info.value.toString())); + this.list.addButton(new Button(width - 160, 0,150, 20, widget.getValue().apply(info.value), widget.getKey()),resetButton, null,name); + } else if (info.field.getType() == List.class) { + if (!reload) info.index = 0; + EditBox widget = new EditBox(font, width - 160, 0, 150, 20, null); + widget.setMaxLength(info.width); + if (info.index < ((List)info.value).size()) widget.setValue((String.valueOf(((List)info.value).get(info.index)))); + else widget.setValue(""); + Predicate processor = ((BiFunction>) info.widget).apply(widget, done); + widget.setFilter(processor); + resetButton.setWidth(20); + resetButton.setMessage(new TextComponent("R").withStyle(ChatFormatting.RED)); + Button cycleButton = new Button(width - 185, 0, 20, 20, new TextComponent(String.valueOf(info.index)).withStyle(ChatFormatting.GOLD), (button -> { + ((List)info.value).remove(""); + double scrollAmount = list.getScrollAmount(); + this.reload = true; + info.index = info.index + 1; + if (info.index > ((List)info.value).size()) info.index = 0; + Objects.requireNonNull(minecraft).setScreen(this); + list.setScrollAmount(scrollAmount); + })); + this.list.addButton(widget, resetButton, cycleButton, name); + } else if (info.widget != null) { + EditBox widget = new EditBox(font, width - 160, 0, 150, 20, null); + widget.setMaxLength(info.width); + widget.insertText(info.tempValue); + Predicate processor = ((BiFunction>) info.widget).apply(widget, done); + widget.setFilter(processor); + this.list.addButton(widget, resetButton, null, name); + } else { + this.list.addButton(null,null,null,name); + } + } + } + + } + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + this.list.render(matrices, mouseX, mouseY, delta); + drawCenteredString(matrices, font, title, width / 2, 15, 0xFFFFFF); + + for (EntryInfo info : entries) { + if (info.id.equals(modid)) { + if (list.getHoveredButton(mouseX,mouseY).isPresent()) { + AbstractWidget buttonWidget = list.getHoveredButton(mouseX,mouseY).get(); + Component text = ButtonEntry.buttonsWithText.get(buttonWidget); + TranslatableComponent name = new TranslatableComponent(this.translationPrefix + info.field.getName()); + String key = translationPrefix + info.field.getName() + ".tooltip"; + + if (info.error != null && text.equals(name)) renderTooltip(matrices, info.error.getValue(), mouseX, mouseY); + else if (I18n.exists(key) && text.equals(name)) { + List list = new ArrayList<>(); + for (String str : I18n.get(key).split("\n")) + list.add(new TextComponent(str)); + renderTooltip(matrices, (Component) list, mouseX, mouseY); + } + } + } + } + super.render(matrices,mouseX,mouseY,delta); + } + } + @Environment(EnvType.CLIENT) + public static class MidnightConfigListWidget extends ContainerObjectSelectionList { + Font textRenderer; + + public MidnightConfigListWidget(Minecraft minecraftClient, int i, int j, int k, int l, int m) { + super(minecraftClient, i, j, k, l, m); + this.centerListVertically = false; + textRenderer = minecraftClient.font; + } +// @Override + public int getScrollbarPositionX() { return this.width -7; } + + public void addButton(AbstractWidget button, AbstractWidget resetButton, AbstractWidget indexButton, Component text) { + this.addEntry(ButtonEntry.create(button, text, resetButton, indexButton)); + } + @Override + public int getRowWidth() { return 10000; } + public Optional getHoveredButton(double mouseX, double mouseY) { + for (ButtonEntry buttonEntry : this.children()) { + if (buttonEntry.button != null && buttonEntry.button.isMouseOver(mouseX, mouseY)) { + return Optional.of(buttonEntry.button); + } + } + return Optional.empty(); + } + } + public static class ButtonEntry extends ContainerObjectSelectionList.Entry { + private static final Font textRenderer = Minecraft.getInstance().font; + public final AbstractWidget button; + private final AbstractWidget resetButton; + private final AbstractWidget indexButton; + private final Component text; + private final List children = new ArrayList<>(); + public static final Map buttonsWithText = new HashMap<>(); + + private ButtonEntry(AbstractWidget button, Component text, AbstractWidget resetButton, AbstractWidget indexButton) { + buttonsWithText.put(button,text); + this.button = button; + this.resetButton = resetButton; + this.text = text; + this.indexButton = indexButton; + if (button != null) children.add(button); + if (resetButton != null) children.add(resetButton); + if (indexButton != null) children.add(indexButton); + } + public static ButtonEntry create(AbstractWidget button, Component text, AbstractWidget resetButton, AbstractWidget indexButton) { + return new ButtonEntry(button, text, resetButton, indexButton); + } + public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + if (button != null) { + button.y = y; + button.render(matrices, mouseX, mouseY, tickDelta); + } + if (resetButton != null) { + resetButton.y = y; + resetButton.render(matrices, mouseX, mouseY, tickDelta); + } + if (indexButton != null) { + indexButton.y = y; + indexButton.render(matrices, mouseX, mouseY, tickDelta); + } + if (text != null && (!text.getString().contains("spacer") || button != null)) + GuiComponent.drawString(matrices,textRenderer, text,12,y+5,0xFFFFFF); + } + @Override + public List children() {return children;} + @Override + public List narratables() {return children;} + } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Entry { + int width() default 100; + double min() default Double.MIN_NORMAL; + double max() default Double.MAX_VALUE; + String name() default ""; + } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface ScreenEntry { + String to() default ""; + } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Category { + String value(); + } + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Comment {} + + public static class HiddenAnnotationExclusionStrategy implements ExclusionStrategy { + public boolean shouldSkipClass(Class clazz) { return false; } + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + return fieldAttributes.getAnnotation(Entry.class) == null; + } + } +} diff --git a/common/src/main/java/com/seibel/lod/common/wrappers/minecraft/MinecraftRenderWrapper.java b/common/src/main/java/com/seibel/lod/common/wrappers/minecraft/MinecraftRenderWrapper.java index 4077b23ea..670a7e5b7 100644 --- a/common/src/main/java/com/seibel/lod/common/wrappers/minecraft/MinecraftRenderWrapper.java +++ b/common/src/main/java/com/seibel/lod/common/wrappers/minecraft/MinecraftRenderWrapper.java @@ -38,7 +38,7 @@ import net.minecraft.world.phys.Vec3; * related to rendering in Minecraft. * * @author James Seibel - * @version 12-12-2021 + * @version 12-14-2021 */ public class MinecraftRenderWrapper implements IMinecraftRenderWrapper { @@ -143,36 +143,27 @@ public class MinecraftRenderWrapper implements IMinecraftRenderWrapper * will be incorrectly added, even though they are outside render range). */ @Override - public HashSet getRenderedChunks() + public HashSet getVanillaRenderedChunks() { HashSet loadedPos = new HashSet<>(); - // TODO James needs to allow for circular references in the SingletonHandler - IReflectionHandler reflectionHandler = ReflectionHandler.instance; + // Wow, those are some long names! - if (reflectionHandler.sodiumPresent()) + // go through every RenderInfo to get the compiled chunks + LevelRenderer renderer = MC.levelRenderer; + for (LevelRenderer.RenderChunkInfo worldRenderer$LocalRenderInformationContainer : renderer.renderChunks) { - loadedPos = reflectionHandler.getSodiumRenderedChunks(); - } - else - { - // Wow, those are some long names! - - // go through every RenderInfo to get the compiled chunks - LevelRenderer renderer = MC.levelRenderer; - for (LevelRenderer.RenderChunkInfo worldRenderer$LocalRenderInformationContainer : renderer.renderChunks) + CompiledChunk compiledChunk = worldRenderer$LocalRenderInformationContainer.chunk.getCompiledChunk(); + if (!compiledChunk.hasNoRenderableLayers()) { - CompiledChunk compiledChunk = worldRenderer$LocalRenderInformationContainer.chunk.getCompiledChunk(); - if (!compiledChunk.hasNoRenderableLayers()) - { - // add the ChunkPos for every rendered chunk - BlockPos bpos = worldRenderer$LocalRenderInformationContainer.chunk.getOrigin(); + // add the ChunkPos for every rendered chunk + BlockPos bpos = worldRenderer$LocalRenderInformationContainer.chunk.getOrigin(); - loadedPos.add(new ChunkPosWrapper(bpos)); - } + loadedPos.add(new ChunkPosWrapper(bpos)); } } + return loadedPos; } diff --git a/core b/core index c389f3b39..5709a4c66 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit c389f3b3910992cbc3f10d1ba26150d6eb219a85 +Subproject commit 5709a4c660d4dc8fd912afb45c6e658ad52943a6