Skip to content

Commit

Permalink
Replace having multiple dependencies with groups
Browse files Browse the repository at this point in the history
Instead of each config entry having a collection of dependencies, create a new special DependencyGroup that can contain multiple other dependencies.

`DependencyGroup` implements `Dependency`, so it can be nested if desired. However, the annotation equivalent (`@DependsOnGroup`) cannot be nested due to annotation design limitations.

In other words, Autoconfig can only have one group per field.

Conditions are effectively logical operators, allowing for more advanced dependencies. E.g. `ALL` requires all dependencies be met, while `ANY` only requires one-or-more.
  • Loading branch information
MattSturgeon committed Mar 13, 2023
1 parent ef53fed commit eed9e56
Show file tree
Hide file tree
Showing 33 changed files with 404 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package me.shedaniel.autoconfig.annotation;

import me.shedaniel.clothconfig2.api.dependencies.BooleanDependency;
import me.shedaniel.clothconfig2.api.dependencies.DependencyGroup;
import me.shedaniel.clothconfig2.api.dependencies.SelectionDependency;

import java.lang.annotation.ElementType;
Expand Down Expand Up @@ -157,21 +158,6 @@ enum EnumDisplayOption {
}
}


/**
* Define multiple dependencies.
* <br><br>
* All dependencies must be met for the annotated field to be enabled.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DependOnEach {
/**
* An array of dependencies to depend on
*/
DependsOn[] value();
}

/**
* Depends on the referenced field
*/
Expand Down Expand Up @@ -208,5 +194,25 @@ enum EnumDisplayOption {
*/
boolean hiddenWhenNotMet() default false;
}

/**
* Defines a group of dependencies, with a {@link DependencyGroup.Condition} to be met.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DependsOnGroup {

/**
* The condition for this group to be met.
*
* @see DependencyGroup.Condition
*/
DependencyGroup.Condition value();

/**
* The dependencies to be included in the group.
*/
DependsOn[] dependencies();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
import me.shedaniel.autoconfig.ConfigManager;
import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.DependOnEach;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.DependsOn;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.DependsOnGroup;
import me.shedaniel.autoconfig.gui.registry.api.GuiRegistryAccess;
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
import me.shedaniel.clothconfig2.api.ConfigBuilder;
Expand Down Expand Up @@ -97,6 +97,36 @@ public Screen get() {
T defaults = manager.getSerializer().createDefault();

String i18n = i18nFunction.apply(manager);

// Keep references to all GUI entries in this map.
// So that we can access them later when adding dependencies
Map<String, AbstractConfigListEntry<?>> i18nMap = new HashMap<>();

// Function to build a Dependency from a DependsOn annotation, using i18nMao
Function<DependsOn, Dependency> buildDependsOn = (annotation) -> {
String dependencyI18n = annotation.value();
AbstractConfigListEntry<?> dependency = i18nMap.get(dependencyI18n);
if (dependency == null)
throw new RuntimeException("Specified dependency not found: \"%s\"".formatted(dependencyI18n));

return buildDependency(annotation, dependency);
};

// Function to build a DependencyGroup from a DependsOnGroup annotation
Function<DependsOnGroup, Dependency> buildDependsOnGroup = (annotation) -> {
// Build each dependency as defined in DependsOn annotations
Dependency[] dependencies = Arrays.stream(annotation.dependencies())
.map(buildDependsOn)
.toArray(Dependency[]::new);

// Return the appropriate DependencyGroup variant
return switch (annotation.value()) {
case ALL -> Dependency.all(dependencies);
case NONE -> Dependency.none(dependencies);
case ANY -> Dependency.any(dependencies);
case ONE -> Dependency.one(dependencies);
};
};

ConfigBuilder builder = ConfigBuilder.create().setParentScreen(parent).setTitle(Component.translatable(String.format("%s.title", i18n))).setSavingRunnable(manager::save);

Expand All @@ -113,18 +143,10 @@ public Screen get() {

Map<String, ResourceLocation> categoryBackgrounds =
Arrays.stream(configClass.getAnnotationsByType(Config.Gui.CategoryBackground.class))
.collect(
toMap(
Config.Gui.CategoryBackground::category,
ann -> new ResourceLocation(ann.background())
)
);

// Keep references to all GUI entries in this map.
// So that we can access them later when adding dependencies
Map<String, AbstractConfigListEntry<?>> i18nMap = new HashMap<>();
.collect(toMap(Config.Gui.CategoryBackground::category,
ann -> new ResourceLocation(ann.background())));

LinkedHashMap<ConfigCategory, List<Field>> categoryMap = Arrays.stream(configClass.getDeclaredFields())
Map<ConfigCategory, List<Field>> categoryMap = Arrays.stream(configClass.getDeclaredFields())
.collect(groupingBy(
field -> getOrCreateCategoryForField(field, builder, categoryBackgrounds, i18n),
LinkedHashMap::new,
Expand All @@ -147,30 +169,23 @@ public Screen get() {
// This time, look for fields that have defined dependencies and generate them.
// Apply the generated dependencies to the config entries referenced by the i18n map.
categoryMap.forEach((category, fields) -> fields.stream()
.filter(field -> field.isAnnotationPresent(DependsOn.class) || field.isAnnotationPresent(DependOnEach.class))
.filter(field -> field.isAnnotationPresent(DependsOn.class) || field.isAnnotationPresent(DependsOnGroup.class))
.forEach(field -> {
// Get all the DependsOn annotations in a list, so we can iterate over them
List<DependsOn> annotations = new ArrayList<>();
if (field.isAnnotationPresent(DependOnEach.class))
Collections.addAll(annotations, field.getAnnotation(DependOnEach.class).value());
else if (field.isAnnotationPresent(DependsOn.class))
annotations.add(field.getAnnotation(DependsOn.class));
else
throw new RuntimeException("Neither DependsOn nor DependsOnAll annotation is present. This shouldn't be possible!");

annotations.forEach(annotation -> {
String optionI18n = optionFunction.apply(i18n, field);
String dependencyI18n = annotation.value();
AbstractConfigListEntry<?> entry = i18nMap.get(optionI18n);
AbstractConfigListEntry<?> dependency = i18nMap.get(dependencyI18n);
String optionI18n = optionFunction.apply(i18n, field);
AbstractConfigListEntry<?> entry = i18nMap.get(optionI18n);
if (entry == null)
throw new IllegalStateException("Specified entry not found: \"%s\"".formatted(optionI18n));

if (entry == null)
throw new IllegalStateException("Specified entry not found: \"%s\"".formatted(optionI18n));
if (dependency == null)
throw new RuntimeException("Specified dependency not found: \"%s\"".formatted(dependencyI18n));

entry.addDependency(buildDependency(annotation, dependency));
});
Dependency dependency;
if (field.isAnnotationPresent(DependsOnGroup.class)) {
dependency = buildDependsOnGroup.apply(field.getAnnotation(DependsOnGroup.class));
} else if (field.isAnnotationPresent(DependsOn.class)) {
dependency = buildDependsOn.apply(field.getAnnotation(DependsOn.class));
} else {
throw new RuntimeException("Neither DependsOn nor DependsOnGroup annotation is present.");
}

entry.setDependency(dependency);
}));

return buildFunction.apply(builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,31 @@ enum DependencyDemoEnum {
SubCategoryBuilder depends = entryBuilder.startSubCategory(Component.literal("Dependencies")).setExpanded(true);
BooleanListEntry dependency = entryBuilder.startBooleanToggle(Component.literal("A cool toggle"), false).setTooltip(Component.literal("Toggle me...")).build();
depends.add(dependency);
depends.add(entryBuilder.startBooleanToggle(Component.literal("I only work when cool is toggled..."), true).addDependency(Dependency.disabledWhenNotMet(dependency)).build());
depends.add(entryBuilder.startBooleanToggle(Component.literal("I only appear when cool is toggled..."), true).addDependency(Dependency.hiddenWhenNotMet(dependency)).build());
SubCategoryBuilder dependantSub = entryBuilder.startSubCategory(Component.literal("How do deps work with sub-categories?")).addDependency(Dependency.disabledWhenNotMet(dependency));
depends.add(entryBuilder.startBooleanToggle(Component.literal("I only work when cool is toggled..."), true)
.withDependency(Dependency.disabledWhenNotMet(dependency)).build());
depends.add(entryBuilder.startBooleanToggle(Component.literal("I only appear when cool is toggled..."), true)
.withDependency(Dependency.hiddenWhenNotMet(dependency)).build());
SubCategoryBuilder dependantSub = entryBuilder.startSubCategory(Component.literal("How do deps work with sub-categories?"))
.withDependency(Dependency.disabledWhenNotMet(dependency));
dependantSub.add(entryBuilder.startTextDescription(Component.literal("This sub category depends on Cool being toggled")).build());
dependantSub.add(entryBuilder.startBooleanToggle(Component.literal("Example entry"), true).build());
dependantSub.add(entryBuilder.startBooleanToggle(Component.literal("Another example..."), true).build());
depends.add(dependantSub.build());
depends.add(entryBuilder.startLongList(Component.literal("A list of Longs"), Arrays.asList(1L, 2L, 3L)).setDefaultValue(Arrays.asList(1L, 2L, 3L)).addDependency(Dependency.disabledWhenNotMet(dependency)).build());
depends.add(entryBuilder.startLongList(Component.literal("A list of Longs"), Arrays.asList(1L, 2L, 3L)).setDefaultValue(Arrays.asList(1L, 2L, 3L))
.withDependency(Dependency.disabledWhenNotMet(dependency)).build());
EnumListEntry<DependencyDemoEnum> enumDependency = entryBuilder.startEnumSelector(Component.literal("Select a good or bad option"), DependencyDemoEnum.class, DependencyDemoEnum.OKAY).build();
depends.add(enumDependency);
depends.add(entryBuilder.startBooleanToggle(Component.literal("I only work when a good option is chosen..."), true).setTooltip(Component.literal("Select good or better above")).addDependency(Dependency.disabledWhenNotMet(enumDependency, DependencyDemoEnum.EXCELLENT, DependencyDemoEnum.GOOD)).build());
depends.add(entryBuilder.startBooleanToggle(Component.literal("I only work when a good option is chosen..."), true).setTooltip(Component.literal("Select good or better above"))
.withDependency(Dependency.disabledWhenNotMet(enumDependency, DependencyDemoEnum.EXCELLENT, DependencyDemoEnum.GOOD)).build());
depends.add(entryBuilder.startBooleanToggle(Component.literal("I need a good option AND a cool toggle!"), true).setTooltip(Component.literal("Select good or better and also toggle cool"))
.withDependency(Dependency.all(
Dependency.disabledWhenNotMet(dependency),
Dependency.disabledWhenNotMet(enumDependency, DependencyDemoEnum.EXCELLENT, DependencyDemoEnum.GOOD)))
.build());

testing.addEntry(depends.build());
testing.addEntry(entryBuilder.startBooleanToggle(Component.literal("I appear when bad option is chosen..."), true).addDependency(Dependency.hiddenWhenNotMet(enumDependency, DependencyDemoEnum.HORRIBLE, DependencyDemoEnum.BAD)).setTooltip(Component.literal("Hopefully I keep my index")).build());
testing.addEntry(entryBuilder.startBooleanToggle(Component.literal("I appear when bad option is chosen..."), true)
.withDependency(Dependency.hiddenWhenNotMet(enumDependency, DependencyDemoEnum.HORRIBLE, DependencyDemoEnum.BAD)).setTooltip(Component.literal("Hopefully I keep my index")).build());

testing.addEntry(entryBuilder.startTextDescription(
Component.translatable("text.cloth-config.testing.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;

Expand All @@ -52,8 +55,8 @@ public abstract class AbstractConfigEntry<T> extends DynamicElementListWidget.El
private List<String> cachedTags = null;
private Iterable<String> additionalSearchTags = null;

@NotNull
private final Collection<Dependency> dependencies = new ArrayList<>();
@Nullable
private Dependency dependency = null;

public final void setReferenceProviderEntries(@Nullable List<ReferenceProvider<?>> referencableEntries) {
this.referencableEntries = referencableEntries;
Expand Down Expand Up @@ -99,13 +102,12 @@ public Component getDisplayedFieldName() {
}

/**
* True if no dependencies exist, otherwise all dependencies must be met.
* True if no dependency exists, otherwise true if the dependency conditions are currently met.
*
* @return whether all dependencies are met.
* @return whether dependency conditions are met.
*/
public boolean dependenciesMet() {
// allMatch() returns true if there are no dependencies
return dependencies.stream().allMatch(Dependency::check);
return dependency == null || dependency.check();
}

/**
Expand All @@ -115,37 +117,27 @@ public boolean dependenciesMet() {
* @return whether the config entry should be hidden.
*/
public boolean hidden() {
// anyMatch() returns false if there are no dependencies
return dependencies.stream()
.filter(Dependency::hiddenWhenNotMet)
.anyMatch(dependency -> !dependency.check());
}

/**
* Add dependencies to the entry. If any dependency is unmet, the entry will be disabled.
*
* @param dependencies one or more dependencies to be added.
*/
public void addDependency(Dependency... dependencies) {
addDependencies(Arrays.asList(dependencies));
return dependency != null && dependency.hidden();
}

/**
* Add dependencies to the entry. If any dependency is unmet, the entry will be disabled.
* Sets the entry's dependency. Whenever the dependency is unmet, the entry will be disabled.
* <br>
* Passing in a {@code null} value will remove the entry's dependency.
*
* @param dependencies a {@link Collection} of dependencies to be added.
* @param dependency the new dependency.
*/
public void addDependencies(Collection<Dependency> dependencies) {
this.dependencies.addAll(dependencies);
public void setDependency(@Nullable Dependency dependency) {
this.dependency = dependency;
}

/**
* Get the entry's dependencies.
* Get the entry's dependency.
*
* @return a {@link Collection} of {@link Dependency}s
* @return the {@link Dependency}
*/
public @NotNull Collection<Dependency> getDependencies() {
return dependencies;
public @Nullable Dependency getDependency() {
return dependency;
}

public Iterator<String> getSearchTags() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ public class BooleanDependency extends ConfigEntryDependency<Boolean, BooleanLis
protected Component getConditionText(Boolean condition) {
return getEntry().getYesNoText(condition);
}

@Override
public Component getShortDescription() {
Component condition = getConditionText(this.getConditions().stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException("BooleanDependency requires exactly one condition")));
return Component.translatable("text.cloth-config.boolean_dependency.short_description", getEntry().getFieldName(), condition);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,14 @@ public Optional<Component[]> getTooltip() {

// If many conditions, print them as a list
if (conditionTexts.size() > 2) {
conditionTexts.forEach(text -> tooltip.add(Component.translatable("text.cloth-config.dependencies.list_entry", text)));
tooltip.addAll(conditionTexts.stream()
.map(text -> Component.translatable("text.cloth-config.dependencies.list_entry", text))
.toList());
}

if (tooltip.isEmpty())
return Optional.empty();

return Optional.of(tooltip.toArray(new Component[0]));
return Optional.of(tooltip.toArray(Component[]::new));
}
}
Loading

0 comments on commit eed9e56

Please sign in to comment.