Przeglądaj źródła

Use lazy translation key initialization

Lortseam 4 lat temu
rodzic
commit
15ec6c88a2
23 zmienionych plików z 212 dodań i 200 usunięć
  1. 3 14
      lib/src/main/java/me/lortseam/completeconfig/data/BaseCollection.java
  2. 32 23
      lib/src/main/java/me/lortseam/completeconfig/data/BooleanEntry.java
  3. 0 1
      lib/src/main/java/me/lortseam/completeconfig/data/BoundedEntry.java
  4. 30 32
      lib/src/main/java/me/lortseam/completeconfig/data/Collection.java
  5. 4 13
      lib/src/main/java/me/lortseam/completeconfig/data/CollectionSet.java
  6. 0 1
      lib/src/main/java/me/lortseam/completeconfig/data/ColorEntry.java
  7. 14 6
      lib/src/main/java/me/lortseam/completeconfig/data/Config.java
  8. 1 2
      lib/src/main/java/me/lortseam/completeconfig/data/DataSet.java
  9. 0 1
      lib/src/main/java/me/lortseam/completeconfig/data/DropdownEntry.java
  10. 51 64
      lib/src/main/java/me/lortseam/completeconfig/data/Entry.java
  11. 8 4
      lib/src/main/java/me/lortseam/completeconfig/data/EntryOrigin.java
  12. 3 4
      lib/src/main/java/me/lortseam/completeconfig/data/EntrySet.java
  13. 1 2
      lib/src/main/java/me/lortseam/completeconfig/data/EnumEntry.java
  14. 8 9
      lib/src/main/java/me/lortseam/completeconfig/data/Registry.java
  15. 11 14
      lib/src/main/java/me/lortseam/completeconfig/data/SliderEntry.java
  16. 1 0
      lib/src/main/java/me/lortseam/completeconfig/data/entry/Transformation.java
  17. 1 0
      lib/src/main/java/me/lortseam/completeconfig/data/entry/Transformer.java
  18. 12 0
      lib/src/main/java/me/lortseam/completeconfig/data/structure/DataPart.java
  19. 22 0
      lib/src/main/java/me/lortseam/completeconfig/data/structure/TooltipSupplier.java
  20. 3 3
      lib/src/main/java/me/lortseam/completeconfig/gui/cloth/ClothConfigScreenBuilder.java
  21. 0 4
      lib/src/main/java/me/lortseam/completeconfig/io/ConfigSource.java
  22. 6 1
      lib/src/test/java/me/lortseam/completeconfig/data/BaseCollectionTest.java
  23. 1 2
      lib/src/test/java/me/lortseam/completeconfig/data/EntryTest.java

+ 3 - 14
lib/src/main/java/me/lortseam/completeconfig/data/BaseCollection.java

@@ -10,7 +10,6 @@ import me.lortseam.completeconfig.exception.IllegalAnnotationTargetException;
 import me.lortseam.completeconfig.util.ReflectionUtils;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
-import net.minecraft.text.Text;
 import org.apache.commons.lang3.ArrayUtils;
 
 import java.lang.reflect.InvocationTargetException;
@@ -20,20 +19,10 @@ import java.util.Collections;
 
 abstract class BaseCollection implements ParentDataPart {
 
-    protected final TranslationKey translation;
-    private final EntrySet entries;
-    private final CollectionSet collections;
-
-    BaseCollection(TranslationKey translation) {
-        this.translation = translation;
-        entries = new EntrySet(translation);
-        collections = new CollectionSet(translation);
-    }
-
     @Environment(EnvType.CLIENT)
-    public Text getText() {
-        return translation.toText();
-    }
+    protected TranslationKey translation;
+    private final EntrySet entries = new EntrySet(this);
+    private final CollectionSet collections = new CollectionSet(this);
 
     public java.util.Collection<Entry> getEntries() {
         return Collections.unmodifiableCollection(entries);

+ 32 - 23
lib/src/main/java/me/lortseam/completeconfig/data/BooleanEntry.java

@@ -1,48 +1,57 @@
 package me.lortseam.completeconfig.data;
 
 import me.lortseam.completeconfig.api.ConfigEntry;
-import me.lortseam.completeconfig.data.entry.EntryOrigin;
 import me.lortseam.completeconfig.data.text.TranslationKey;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
-import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.text.Text;
 import org.apache.commons.lang3.StringUtils;
 
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
 import java.util.function.Function;
 
 public class BooleanEntry extends Entry<Boolean> {
 
-    private final Function<Boolean, TranslationKey> valueTranslationSupplier;
+    @Environment(EnvType.CLIENT)
+    private Map<Boolean, TranslationKey> valueTranslations;
 
     BooleanEntry(EntryOrigin origin) {
         super(origin);
-        valueTranslationSupplier = FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT ? origin.getOptionalAnnotation(ConfigEntry.Boolean.class).map(annotation -> {
-            if (StringUtils.isBlank(annotation.trueTranslationKey()) && StringUtils.isBlank(annotation.falseTranslationKey())) {
-                return null;
-            } else {
-                return (Function<Boolean, TranslationKey>) value -> {
-                    String key = value ? annotation.trueTranslationKey() : annotation.falseTranslationKey();
-                    if (!StringUtils.isBlank(key)) {
-                        return translation.root().append(key);
-                    }
-                    return translation.append(value ? "true" : "false");
-                };
+    }
+
+    @Environment(EnvType.CLIENT)
+    private Map<Boolean, TranslationKey> getValueTranslations() {
+        if (valueTranslations == null) {
+            valueTranslations = new HashMap<>();
+            Optional<ConfigEntry.Boolean> annotation = origin.getOptionalAnnotation(ConfigEntry.Boolean.class);
+            if (annotation.isPresent()) {
+                if (!StringUtils.isBlank(annotation.get().trueTranslationKey())) {
+                    valueTranslations.put(true, getTranslation().root().append(annotation.get().trueTranslationKey()));
+                }
+                if (!StringUtils.isBlank(annotation.get().falseTranslationKey())) {
+                    valueTranslations.put(false, getTranslation().root().append(annotation.get().falseTranslationKey()));
+                }
             }
-        }).orElse(null) : null;
+            TranslationKey defaultTrueTranslation = getTranslation().append("true");
+            if (defaultTrueTranslation.exists()) {
+                valueTranslations.putIfAbsent(true, defaultTrueTranslation);
+            }
+            TranslationKey defaultFalseTranslation = getTranslation().append("false");
+            if (defaultFalseTranslation.exists()) {
+                valueTranslations.putIfAbsent(false, defaultFalseTranslation);
+            }
+        }
+        return valueTranslations;
     }
 
     @Environment(EnvType.CLIENT)
     public Function<Boolean, Text> getValueTextSupplier() {
-        if (valueTranslationSupplier != null) {
-            return bool -> valueTranslationSupplier.apply(bool).toText();
-        }
-        TranslationKey defaultTrueTranslation = translation.append("true");
-        TranslationKey defaultFalseTranslation = translation.append("false");
-        if (defaultTrueTranslation.exists() || defaultFalseTranslation.exists()) {
-            return bool -> (bool ? defaultTrueTranslation : defaultFalseTranslation).toText();
+        if (valueTranslations.isEmpty()) {
+            return null;
         }
-        return null;
+        return value -> valueTranslations.get(value).toText();
     }
 
 }

+ 0 - 1
lib/src/main/java/me/lortseam/completeconfig/data/BoundedEntry.java

@@ -2,7 +2,6 @@ package me.lortseam.completeconfig.data;
 
 import lombok.Getter;
 import lombok.extern.log4j.Log4j2;
-import me.lortseam.completeconfig.data.entry.EntryOrigin;
 
 import java.math.BigDecimal;
 

+ 30 - 32
lib/src/main/java/me/lortseam/completeconfig/data/Collection.java

@@ -1,62 +1,60 @@
 package me.lortseam.completeconfig.data;
 
-import lombok.Getter;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
+import me.lortseam.completeconfig.api.ConfigGroup;
 import me.lortseam.completeconfig.data.structure.Identifiable;
+import me.lortseam.completeconfig.data.structure.TooltipSupplier;
 import me.lortseam.completeconfig.data.text.TranslationKey;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
-import net.fabricmc.loader.api.FabricLoader;
-import net.minecraft.text.Text;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.spongepowered.configurate.CommentedConfigurationNode;
 
 import java.util.Arrays;
-import java.util.Optional;
 
 @Log4j2(topic = "CompleteConfig")
-public class Collection extends BaseCollection implements Identifiable {
-
-    private final String id;
-    private final TranslationKey[] customTooltipTranslation;
-    @Getter
-    private final String comment;
-
-    Collection(String id, TranslationKey translation, String[] customTooltipTranslationKeys, String comment) {
-        super(translation);
-        this.id = id;
-        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {
-            customTooltipTranslation = ArrayUtils.isNotEmpty(customTooltipTranslationKeys) ? Arrays.stream(customTooltipTranslationKeys).map(key -> translation.root().append(key)).toArray(TranslationKey[]::new) : null;
-        } else {
-            customTooltipTranslation = null;
-        }
-        this.comment = !StringUtils.isBlank(comment) ? comment : null;
-    }
+@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
+public class Collection extends BaseCollection implements Identifiable, TooltipSupplier {
 
-    @Environment(EnvType.SERVER)
-    Collection(String id, String comment) {
-        this(id, null, null, comment);
+    private final BaseCollection parent;
+    private final ConfigGroup group;
+    @Environment(EnvType.CLIENT)
+    private TranslationKey[] tooltipTranslation;
+
+    @Override
+    public TranslationKey getTranslation() {
+        if (translation == null) {
+            translation = parent.getTranslation().append(group.getId());
+        }
+        return translation;
     }
 
-    @Environment(EnvType.CLIENT)
-    public Optional<Text[]> getTooltipTranslation() {
-        return (customTooltipTranslation != null ? Optional.of(customTooltipTranslation) : translation.appendTooltip()).map(lines -> {
-            return Arrays.stream(lines).map(TranslationKey::toText).toArray(Text[]::new);
-        });
+    @Override
+    public TranslationKey[] getTooltipTranslation() {
+        if (tooltipTranslation == null) {
+            if (ArrayUtils.isNotEmpty(group.getTooltipTranslationKeys())) {
+                tooltipTranslation = Arrays.stream(group.getTooltipTranslationKeys()).map(key -> getTranslation().root().append(key)).toArray(TranslationKey[]::new);
+            } else {
+                tooltipTranslation = getTranslation().appendTooltip().orElse(new TranslationKey[0]);
+            }
+        }
+        return tooltipTranslation;
     }
 
     @Override
     public void fetch(CommentedConfigurationNode node) {
-        if (comment != null) {
-            node.comment(comment);
+        if (!StringUtils.isEmpty(group.getComment())) {
+            node.comment(group.getComment());
         }
         super.fetch(node);
     }
 
     @Override
     public String getId() {
-        return id;
+        return group.getId();
     }
 
 }

+ 4 - 13
lib/src/main/java/me/lortseam/completeconfig/data/CollectionSet.java

@@ -2,28 +2,19 @@ package me.lortseam.completeconfig.data;
 
 import lombok.extern.log4j.Log4j2;
 import me.lortseam.completeconfig.api.ConfigGroup;
-import me.lortseam.completeconfig.data.text.TranslationKey;
-import net.fabricmc.api.EnvType;
-import net.fabricmc.loader.api.FabricLoader;
 
 @Log4j2(topic = "CompleteConfig")
 public class CollectionSet extends DataSet<Collection> {
 
-    protected CollectionSet(TranslationKey translation) {
-        super(translation);
+    protected CollectionSet(BaseCollection parent) {
+        super(parent);
     }
 
     void resolve(ConfigGroup group) {
-        String groupId = group.getId();
-        Collection collection;
-        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {
-            collection = new Collection(groupId, translation.append(groupId), group.getTooltipTranslationKeys(), group.getComment());
-        } else {
-            collection = new Collection(groupId, group.getComment());
-        }
+        Collection collection = new Collection(parent, group);
         collection.resolveContainer(group);
         if (collection.isEmpty()) {
-            logger.warn("Empty group: " + groupId);
+            logger.warn("Empty group: " + collection.getId());
             return;
         }
         add(collection);

+ 0 - 1
lib/src/main/java/me/lortseam/completeconfig/data/ColorEntry.java

@@ -2,7 +2,6 @@ package me.lortseam.completeconfig.data;
 
 import lombok.Getter;
 import me.lortseam.completeconfig.api.ConfigEntry;
-import me.lortseam.completeconfig.data.entry.EntryOrigin;
 
 public class ColorEntry<T> extends Entry<T> {
 

+ 14 - 6
lib/src/main/java/me/lortseam/completeconfig/data/Config.java

@@ -43,10 +43,11 @@ public final class Config extends BaseCollection {
 
     @Getter(AccessLevel.PACKAGE)
     private final ConfigSource source;
+    @Environment(EnvType.CLIENT)
+    private TranslationKey branchedTranslation;
     private final boolean saveOnExit;
 
     private Config(ConfigSource source, ConfigContainer[] children, boolean saveOnExit) {
-        super(FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT ? TranslationKey.from(source) : null);
         this.source = source;
         this.saveOnExit = saveOnExit;
         resolve(children);
@@ -56,13 +57,20 @@ public final class Config extends BaseCollection {
         return FabricLoader.getInstance().getModContainer(source.getModId()).get().getMetadata();
     }
 
+    @Override
+    public TranslationKey getTranslation() {
+        if (translation == null) {
+            translation = TranslationKey.from(source);
+        }
+        return translation;
+    }
+
     @Environment(EnvType.CLIENT)
-    public TranslationKey getTranslation(boolean includeBranch) {
-        if (includeBranch) {
-            return translation.append(source.getBranch());
-        } else {
-            return translation;
+    public TranslationKey getBranchedTranslation() {
+        if (branchedTranslation == null) {
+            branchedTranslation = getTranslation().append(source.getBranch());
         }
+        return branchedTranslation;
     }
 
     private void load() {

+ 1 - 2
lib/src/main/java/me/lortseam/completeconfig/data/DataSet.java

@@ -4,7 +4,6 @@ import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
 import me.lortseam.completeconfig.data.structure.DataPart;
 import me.lortseam.completeconfig.data.structure.Identifiable;
-import me.lortseam.completeconfig.data.text.TranslationKey;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.AbstractSet;
@@ -15,8 +14,8 @@ import java.util.Map;
 @RequiredArgsConstructor(access = AccessLevel.PROTECTED)
 abstract class DataSet<T extends DataPart & Identifiable> extends AbstractSet<T> {
 
+    protected final BaseCollection parent;
     private final Map<String, T> map = new LinkedHashMap<>();
-    protected final TranslationKey translation;
 
     @Override
     public @NotNull Iterator<T> iterator() {

+ 0 - 1
lib/src/main/java/me/lortseam/completeconfig/data/DropdownEntry.java

@@ -2,7 +2,6 @@ package me.lortseam.completeconfig.data;
 
 import lombok.Getter;
 import me.lortseam.completeconfig.api.ConfigEntry;
-import me.lortseam.completeconfig.data.entry.EntryOrigin;
 
 public class DropdownEntry<T extends Enum<?>> extends EnumEntry<T> {
 

+ 51 - 64
lib/src/main/java/me/lortseam/completeconfig/data/Entry.java

@@ -1,6 +1,5 @@
 package me.lortseam.completeconfig.data;
 
-import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.NonNull;
 import lombok.experimental.Accessors;
@@ -8,19 +7,17 @@ import lombok.extern.log4j.Log4j2;
 import me.lortseam.completeconfig.CompleteConfig;
 import me.lortseam.completeconfig.api.ConfigContainer;
 import me.lortseam.completeconfig.api.ConfigEntry;
-import me.lortseam.completeconfig.data.entry.EntryOrigin;
 import me.lortseam.completeconfig.data.entry.Transformation;
 import me.lortseam.completeconfig.data.entry.Transformer;
 import me.lortseam.completeconfig.data.structure.DataPart;
 import me.lortseam.completeconfig.data.structure.Identifiable;
+import me.lortseam.completeconfig.data.structure.TooltipSupplier;
 import me.lortseam.completeconfig.data.text.TranslationKey;
 import me.lortseam.completeconfig.exception.IllegalAnnotationParameterException;
 import me.lortseam.completeconfig.extensions.CompleteConfigExtension;
 import me.lortseam.completeconfig.util.ReflectionUtils;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
-import net.fabricmc.loader.api.FabricLoader;
-import net.minecraft.text.Text;
 import org.apache.commons.lang3.StringUtils;
 import org.spongepowered.configurate.CommentedConfigurationNode;
 import org.spongepowered.configurate.serialize.SerializationException;
@@ -33,12 +30,10 @@ import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
 
 @Log4j2(topic = "CompleteConfig")
-@EqualsAndHashCode(onlyExplicitlyIncluded = true)
-public class Entry<T> implements DataPart, Identifiable {
+public class Entry<T> implements DataPart, Identifiable, TooltipSupplier {
 
     private static final Transformer DEFAULT_TRANSFORMER = Entry::new;
 
@@ -48,27 +43,26 @@ public class Entry<T> implements DataPart, Identifiable {
         }
     }
 
-    static Entry<?> of(Field field, ConfigContainer parentObject, TranslationKey parentTranslation) {
-        EntryOrigin origin = new EntryOrigin(field, parentObject, parentTranslation);
-        Entry<?> entry = Registry.getTransformations().stream().filter(transformation -> transformation.test(origin)).findFirst().map(Transformation::getTransformer).orElse(DEFAULT_TRANSFORMER).transform(origin);
-        Registry.register(entry);
-        return entry;
+    static Entry<?> of(BaseCollection parent, Field field, ConfigContainer object) {
+        EntryOrigin origin = new EntryOrigin(parent, field, object);
+        return Registry.getTransformations().stream().filter(transformation -> {
+            return transformation.test(origin);
+        }).findFirst().map(Transformation::getTransformer).orElse(DEFAULT_TRANSFORMER).transform(origin);
     }
 
-    @EqualsAndHashCode.Include
-    private final Field field;
+    protected final EntryOrigin origin;
     @Getter
     private final Type type;
     @Getter
     private final Class<T> typeClass;
-    @EqualsAndHashCode.Include
-    private final ConfigContainer parentObject;
     @Getter
     private final String id;
     @Getter
     private final T defaultValue;
-    protected final TranslationKey translation;
-    private final Supplier<TranslationKey[]> tooltipTranslationSupplier;
+    @Environment(EnvType.CLIENT)
+    private TranslationKey translation;
+    @Environment(EnvType.CLIENT)
+    private TranslationKey[] tooltipTranslation;
     @Accessors(fluent = true)
     @Getter
     private final boolean requiresRestart;
@@ -76,40 +70,19 @@ public class Entry<T> implements DataPart, Identifiable {
     private final UnaryOperator<T> valueModifier;
 
     protected Entry(EntryOrigin origin, UnaryOperator<T> valueModifier) {
-        field = origin.getField();
-        if (!field.isAccessible()) {
-            field.setAccessible(true);
+        Registry.register(origin);
+        this.origin = origin;
+        if (!origin.getField().isAccessible()) {
+            origin.getField().setAccessible(true);
         }
         type = origin.getType();
         typeClass = (Class<T>) ReflectionUtils.getTypeClass(type);
-        parentObject = origin.getParentObject();
         this.valueModifier = valueModifier;
         defaultValue = getValue();
-        ConfigEntry annotation = field.getDeclaredAnnotation(ConfigEntry.class);
-        id = annotation != null && !StringUtils.isBlank(annotation.value()) ? annotation.value() : field.getName();
-        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {
-            if (annotation != null && !StringUtils.isBlank(annotation.translationKey())) {
-                translation = origin.getParentTranslation().append(annotation.translationKey());
-            } else {
-                translation = origin.getParentTranslation().append(id);
-            }
-            tooltipTranslationSupplier = () -> {
-                if (annotation != null && annotation.tooltipTranslationKeys().length > 0) {
-                    return Arrays.stream(annotation.tooltipTranslationKeys()).map(key -> {
-                        if (StringUtils.isBlank(key)) {
-                            throw new IllegalAnnotationParameterException("Tooltip translation key of entry " + this + " may not be blank");
-                        }
-                        return translation.root().append(key);
-                    }).toArray(TranslationKey[]::new);
-                }
-                return translation.appendTooltip().orElse(null);
-            };
-        } else {
-            translation = null;
-            tooltipTranslationSupplier = null;
-        }
-        requiresRestart = annotation != null && annotation.requiresRestart();
-        comment = annotation != null && !StringUtils.isBlank(annotation.comment()) ? annotation.comment() : null;
+        Optional<ConfigEntry> annotation = origin.getOptionalAnnotation(ConfigEntry.class);
+        id = annotation.isPresent() && !StringUtils.isBlank(annotation.get().value()) ? annotation.get().value() : origin.getField().getName();
+        requiresRestart = annotation.isPresent() && annotation.get().requiresRestart();
+        comment = annotation.isPresent() && !StringUtils.isBlank(annotation.get().comment()) ? annotation.get().comment() : null;
     }
 
     protected Entry(EntryOrigin origin) {
@@ -125,7 +98,7 @@ public class Entry<T> implements DataPart, Identifiable {
 
     private T getFieldValue() {
         try {
-            return (T) Objects.requireNonNull(field.get(parentObject), field.toString());
+            return (T) Objects.requireNonNull(origin.getField().get(origin.getObject()), origin.getField().toString());
         } catch (IllegalAccessException e) {
             throw new RuntimeException("Failed to get entry value", e);
         }
@@ -152,27 +125,46 @@ public class Entry<T> implements DataPart, Identifiable {
 
     private void set(T value) {
         try {
-            Optional<Method> writeMethod = ReflectionUtils.getWriteMethod(field);
+            Optional<Method> writeMethod = ReflectionUtils.getWriteMethod(origin.getField());
             if (writeMethod.isPresent()) {
-                writeMethod.get().invoke(parentObject, value);
+                writeMethod.get().invoke(origin.getObject(), value);
             } else {
-                field.set(parentObject, value);
+                origin.getField().set(origin.getObject(), value);
             }
         } catch (IntrospectionException | IllegalAccessException | InvocationTargetException e) {
             throw new RuntimeException("Failed to set entry value", e);
         }
     }
 
-    @Environment(EnvType.CLIENT)
-    public Text getText() {
-        return translation.toText();
+    @Override
+    public TranslationKey getTranslation() {
+        if (translation == null) {
+            Optional<ConfigEntry> annotation = origin.getOptionalAnnotation(ConfigEntry.class);
+            if (annotation.isPresent() && !StringUtils.isBlank(annotation.get().translationKey())) {
+                translation = origin.getParentTranslation().append(annotation.get().translationKey());
+            } else {
+                translation = origin.getParentTranslation().append(id);
+            }
+        }
+        return translation;
     }
 
-    @Environment(EnvType.CLIENT)
-    public Optional<Text[]> getTooltip() {
-        return Optional.ofNullable(tooltipTranslationSupplier.get()).map(lines -> {
-            return Arrays.stream(lines).map(TranslationKey::toText).toArray(Text[]::new);
-        });
+    @Override
+    public TranslationKey[] getTooltipTranslation() {
+        if (tooltipTranslation == null) {
+            Optional<ConfigEntry> annotation = origin.getOptionalAnnotation(ConfigEntry.class);
+            if (annotation.isPresent() && annotation.get().tooltipTranslationKeys().length > 0) {
+                tooltipTranslation = Arrays.stream(annotation.get().tooltipTranslationKeys()).map(key -> {
+                    if (StringUtils.isBlank(key)) {
+                        throw new IllegalAnnotationParameterException("Tooltip translation key of entry " + origin.getField() + " may not be blank");
+                    }
+                    return getTranslation().root().append(key);
+                }).toArray(TranslationKey[]::new);
+            } else {
+                tooltipTranslation = getTranslation().appendTooltip().orElse(new TranslationKey[0]);
+            }
+        }
+        return tooltipTranslation;
     }
 
     @Override
@@ -196,9 +188,4 @@ public class Entry<T> implements DataPart, Identifiable {
         }
     }
 
-    @Override
-    public String toString() {
-        return field.toString();
-    }
-
 }

+ 8 - 4
lib/src/main/java/me/lortseam/completeconfig/data/entry/EntryOrigin.java → lib/src/main/java/me/lortseam/completeconfig/data/EntryOrigin.java

@@ -1,5 +1,6 @@
-package me.lortseam.completeconfig.data.entry;
+package me.lortseam.completeconfig.data;
 
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import me.lortseam.completeconfig.api.ConfigContainer;
@@ -14,17 +15,20 @@ import java.lang.reflect.Type;
 import java.util.Optional;
 
 @RequiredArgsConstructor
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
 public final class EntryOrigin {
 
+    private final BaseCollection parent;
     @Getter
+    @EqualsAndHashCode.Include
     protected final Field field;
     @Getter
-    private final ConfigContainer parentObject;
-    private final TranslationKey parentTranslation;
+    @EqualsAndHashCode.Include
+    private final ConfigContainer object;
 
     @Environment(EnvType.CLIENT)
     public TranslationKey getParentTranslation() {
-        return parentTranslation;
+        return parent.getTranslation();
     }
 
     public Type getType() {

+ 3 - 4
lib/src/main/java/me/lortseam/completeconfig/data/EntrySet.java

@@ -4,7 +4,6 @@ import lombok.extern.log4j.Log4j2;
 import me.lortseam.completeconfig.api.ConfigContainer;
 import me.lortseam.completeconfig.api.ConfigEntries;
 import me.lortseam.completeconfig.api.ConfigEntry;
-import me.lortseam.completeconfig.data.text.TranslationKey;
 import me.lortseam.completeconfig.exception.IllegalModifierException;
 
 import java.lang.reflect.Modifier;
@@ -13,8 +12,8 @@ import java.util.Arrays;
 @Log4j2(topic = "CompleteConfig")
 public class EntrySet extends DataSet<Entry> {
 
-    EntrySet(TranslationKey translation) {
-        super(translation);
+    EntrySet(BaseCollection parent) {
+        super(parent);
     }
 
     void resolve(ConfigContainer container) {
@@ -31,7 +30,7 @@ public class EntrySet extends DataSet<Entry> {
                 if (Modifier.isFinal(field.getModifiers())) {
                     throw new IllegalModifierException("Entry field " + field + " must not be final");
                 }
-                return Entry.of(field, Modifier.isStatic(field.getModifiers()) ? null : container, translation);
+                return Entry.of(parent, field, Modifier.isStatic(field.getModifiers()) ? null : container);
             }).forEach(this::add);
         }
     }

+ 1 - 2
lib/src/main/java/me/lortseam/completeconfig/data/EnumEntry.java

@@ -1,7 +1,6 @@
 package me.lortseam.completeconfig.data;
 
 import com.google.common.base.CaseFormat;
-import me.lortseam.completeconfig.data.entry.EntryOrigin;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.text.Text;
@@ -16,7 +15,7 @@ public class EnumEntry<T extends Enum<?>> extends Entry<T> {
 
     @Environment(EnvType.CLIENT)
     public final Function<Enum, Text> getValueTextSupplier() {
-        return enumValue -> translation.append(CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, enumValue.name())).toText();
+        return enumValue -> getTranslation().append(CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, enumValue.name())).toText();
     }
 
 }

+ 8 - 9
lib/src/main/java/me/lortseam/completeconfig/data/Registry.java

@@ -6,7 +6,6 @@ import lombok.experimental.UtilityClass;
 import me.lortseam.completeconfig.api.ConfigEntry;
 import me.lortseam.completeconfig.data.entry.Transformation;
 import me.lortseam.completeconfig.gui.ConfigScreenBuilder;
-import me.lortseam.completeconfig.io.ConfigSource;
 import me.lortseam.completeconfig.util.ReflectionUtils;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
@@ -21,7 +20,7 @@ import java.util.stream.Collectors;
 public final class Registry {
 
     private static final Map<String, ModConfigSet> configs = new HashMap<>();
-    private static final Set<Entry> entries = new HashSet<>();
+    private static final Set<EntryOrigin> origins = new HashSet<>();
     private static final List<Transformation> transformations = Lists.newArrayList(
             Transformation.builder().byType(boolean.class, Boolean.class).byAnnotation(ConfigEntry.Boolean.class, true).transforms(BooleanEntry::new),
             Transformation.builder().byType(int.class, Integer.class).byAnnotation(ConfigEntry.BoundedInteger.class).transforms(origin -> {
@@ -55,13 +54,17 @@ public final class Registry {
     );
 
     static void register(Config config, boolean main) {
+        if (getConfigs().stream().map(Config::getSource).collect(Collectors.toSet()).contains(config.getSource())) {
+            throw new UnsupportedOperationException("A config of " + config.getSource() + " already exists");
+        }
         getConfigs(config.getMod().getId()).add(config, main);
     }
 
-    static void register(Entry<?> entry) {
-        if (!entries.add(entry)) {
-            throw new UnsupportedOperationException(entry + " was already resolved");
+    static void register(EntryOrigin origin) {
+        if (origins.contains(origin)) {
+            throw new UnsupportedOperationException(origin.getField() + " was already resolved");
         }
+        origins.add(origin);
     }
 
     static void register(Transformation... transformations) {
@@ -80,10 +83,6 @@ public final class Registry {
         getConfigs(modId).screenBuilder = screenBuilder;
     }
 
-    public boolean hasSource(ConfigSource source) {
-        return getConfigs().stream().map(Config::getSource).collect(Collectors.toSet()).contains(source);
-    }
-
     private static ModConfigSet getConfigs(String modId) {
         return configs.computeIfAbsent(modId, key -> new ModConfigSet());
     }

+ 11 - 14
lib/src/main/java/me/lortseam/completeconfig/data/SliderEntry.java

@@ -1,11 +1,9 @@
 package me.lortseam.completeconfig.data;
 
 import me.lortseam.completeconfig.api.ConfigEntry;
-import me.lortseam.completeconfig.data.entry.EntryOrigin;
 import me.lortseam.completeconfig.data.text.TranslationKey;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
-import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.text.Text;
 import org.apache.commons.lang3.StringUtils;
 
@@ -13,26 +11,25 @@ import java.util.function.Function;
 
 public class SliderEntry<T extends Number> extends BoundedEntry<T> {
 
-    private final TranslationKey valueTranslation;
+    @Environment(EnvType.CLIENT)
+    private TranslationKey valueTranslation;
 
     public SliderEntry(EntryOrigin origin, T min, T max) {
         super(origin, min, max);
-        ConfigEntry.Slider slider = origin.getAnnotation(ConfigEntry.Slider.class);
-        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT && !StringUtils.isBlank(slider.valueTranslationKey())) {
-            valueTranslation = translation.root().append(slider.valueTranslationKey());
-        } else {
-            valueTranslation = null;
-        }
     }
 
     @Environment(EnvType.CLIENT)
     public Function<T, Text> getValueTextSupplier() {
-        if (valueTranslation != null) {
-            return valueTranslation::toText;
+        if (valueTranslation == null) {
+            ConfigEntry.Slider slider = origin.getAnnotation(ConfigEntry.Slider.class);
+            if (!StringUtils.isBlank(slider.valueTranslationKey())) {
+                valueTranslation = getTranslation().root().append(slider.valueTranslationKey());
+            } else {
+                valueTranslation = getTranslation().append("value");
+            }
         }
-        TranslationKey defaultValueTranslation = translation.append("value");
-        if (defaultValueTranslation.exists()) {
-            return defaultValueTranslation::toText;
+        if (valueTranslation.exists()) {
+            return value -> valueTranslation.toText(value);
         }
         return null;
     }

+ 1 - 0
lib/src/main/java/me/lortseam/completeconfig/data/entry/Transformation.java

@@ -4,6 +4,7 @@ import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.RequiredArgsConstructor;
+import me.lortseam.completeconfig.data.EntryOrigin;
 import org.apache.commons.lang3.ArrayUtils;
 
 import java.lang.annotation.Annotation;

+ 1 - 0
lib/src/main/java/me/lortseam/completeconfig/data/entry/Transformer.java

@@ -1,6 +1,7 @@
 package me.lortseam.completeconfig.data.entry;
 
 import me.lortseam.completeconfig.data.Entry;
+import me.lortseam.completeconfig.data.EntryOrigin;
 
 @FunctionalInterface
 public interface Transformer {

+ 12 - 0
lib/src/main/java/me/lortseam/completeconfig/data/structure/DataPart.java

@@ -1,5 +1,9 @@
 package me.lortseam.completeconfig.data.structure;
 
+import me.lortseam.completeconfig.data.text.TranslationKey;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.text.Text;
 import org.spongepowered.configurate.CommentedConfigurationNode;
 
 public interface DataPart {
@@ -8,4 +12,12 @@ public interface DataPart {
 
     void fetch(CommentedConfigurationNode node);
 
+    @Environment(EnvType.CLIENT)
+    TranslationKey getTranslation();
+
+    @Environment(EnvType.CLIENT)
+    default Text getText() {
+        return getTranslation().toText();
+    }
+
 }

+ 22 - 0
lib/src/main/java/me/lortseam/completeconfig/data/structure/TooltipSupplier.java

@@ -0,0 +1,22 @@
+package me.lortseam.completeconfig.data.structure;
+
+import me.lortseam.completeconfig.data.text.TranslationKey;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.text.Text;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+public interface TooltipSupplier {
+
+    @Environment(EnvType.CLIENT)
+    TranslationKey[] getTooltipTranslation();
+
+    @Environment(EnvType.CLIENT)
+    default Optional<Text[]> getTooltip() {
+        if (getTooltipTranslation().length == 0) return Optional.empty();
+        return Optional.of(Arrays.stream(getTooltipTranslation()).map(TranslationKey::toText).toArray(Text[]::new));
+    }
+
+}

+ 3 - 3
lib/src/main/java/me/lortseam/completeconfig/gui/cloth/ClothConfigScreenBuilder.java

@@ -46,7 +46,7 @@ public final class ClothConfigScreenBuilder extends ConfigScreenBuilder {
         ConfigBuilder builder = supplier.get()
                 .setParentScreen(parentScreen)
                 .setSavingRunnable(config::save);
-        TranslationKey customTitle = config.getTranslation(true).append("title");
+        TranslationKey customTitle = config.getBranchedTranslation().append("title");
         builder.setTitle(customTitle.exists() ? customTitle.toText() : new TranslatableText("completeconfig.gui.defaultTitle", config.getMod().getName()));
         if (!config.getEntries().isEmpty()) {
             ConfigCategory category = builder.getOrCreateCategory(config.getText());
@@ -56,7 +56,7 @@ public final class ClothConfigScreenBuilder extends ConfigScreenBuilder {
         }
         for(Collection collection : config.getCollections()) {
             ConfigCategory category = builder.getOrCreateCategory(collection.getText());
-            category.setDescription(() -> collection.getTooltipTranslation().map(lines -> Arrays.stream(lines).map(line -> (StringVisitable) line).toArray(StringVisitable[]::new)));
+            category.setDescription(() -> collection.getTooltip().map(lines -> Arrays.stream(lines).map(line -> (StringVisitable) line).toArray(StringVisitable[]::new)));
             for (AbstractConfigListEntry<?> entry : buildCollection(collection)) {
                 category.addEntry(entry);
             }
@@ -78,7 +78,7 @@ public final class ClothConfigScreenBuilder extends ConfigScreenBuilder {
         for (Collection subCollection : collection.getCollections()) {
             SubCategoryBuilder subBuilder = ConfigEntryBuilder.create()
                     .startSubCategory(subCollection.getText())
-                    .setTooltip(subCollection.getTooltipTranslation());
+                    .setTooltip(subCollection.getTooltip());
             subBuilder.addAll(buildCollection(subCollection));
             collectionGui.add(subBuilder.build());
         }

+ 0 - 4
lib/src/main/java/me/lortseam/completeconfig/io/ConfigSource.java

@@ -5,7 +5,6 @@ import lombok.Getter;
 import lombok.extern.log4j.Log4j2;
 import me.lortseam.completeconfig.CompleteConfig;
 import me.lortseam.completeconfig.data.Config;
-import me.lortseam.completeconfig.data.Registry;
 import me.lortseam.completeconfig.extensions.CompleteConfigExtension;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.loader.api.FabricLoader;
@@ -42,9 +41,6 @@ public final class ConfigSource {
     public ConfigSource(String modId, String[] branch) {
         this.modId = modId;
         this.branch = branch;
-        if (Registry.hasSource(this)) {
-            throw new IllegalArgumentException("A config of " + this + " already exists");
-        }
         Path path = FabricLoader.getInstance().getConfigDir();
         String[] subPath = ArrayUtils.add(branch, 0, modId);
         subPath[subPath.length - 1] = subPath[subPath.length - 1] + ".conf";

+ 6 - 1
lib/src/test/java/me/lortseam/completeconfig/data/BaseCollectionTest.java

@@ -26,7 +26,12 @@ public class BaseCollectionTest {
 
     @BeforeEach
     public void beforeEach() {
-        baseCollection = new BaseCollection(TranslationKey.from(new ConfigSource(MOD_ID, new String[0]))) {};
+        baseCollection = new BaseCollection() {
+            @Override
+            public TranslationKey getTranslation() {
+                return TranslationKey.from(new ConfigSource(MOD_ID, new String[0]));
+            }
+        };
     }
 
     @AfterEach

+ 1 - 2
lib/src/test/java/me/lortseam/completeconfig/data/EntryTest.java

@@ -2,7 +2,6 @@ package me.lortseam.completeconfig.data;
 
 import me.lortseam.completeconfig.api.ConfigContainer;
 import me.lortseam.completeconfig.api.ConfigEntry;
-import me.lortseam.completeconfig.data.text.TranslationKey;
 import net.minecraft.text.TextColor;
 import org.junit.jupiter.api.Test;
 
@@ -37,7 +36,7 @@ public class EntryTest implements ConfigContainer {
 
     private <E extends Entry<?>> void assertTransformation(String fieldName, Class<E> entryType) {
         try {
-            assertTrue(entryType.isInstance(Entry.of(getClass().getDeclaredField((fieldName)), this, mock(TranslationKey.class))));
+            assertTrue(entryType.isInstance(Entry.of(mock(BaseCollection.class), getClass().getDeclaredField((fieldName)), this)));
         } catch (NoSuchFieldException e) {
             throw new RuntimeException(e);
         }