Skip to content

Commit b167511

Browse files
authored
feat: Add support for specifying custom controls on config properties (#252)
* feat: Add support for specifying custom controls on config properties #251 * test: Add custom control usage in example config * refactor: Change BalmCustomConfigControlRegistrar to be backed by an interface, move ConfigControlRegistry to internal package * refactor: Cleanup some review findings * refactor: Log unsupported properties on NeoForge config screen, use Balm i18n key * refactor: ConfigControlContext -> ConfigControlBinding, and make the property private to force controls to go through the getter/setter since not every config screen makes immediate changes * refactor: Rename displayName -> label * refactor: Fallback to regular default if createElement failed, since our fallback is more precise than ValueEntry * refactor: Fallback to default instead of throwing if custom control is invalid for a screen * refactor: Inline IConfigEntry fallback creation * refactor: Simplify ClothConfigSupport save consumers to skip unnecessary ConfigControlBinding * refactor: Make Configured range validation hint translatable * refactor: Use set instead of setRaw like before * refactor: Fix i18n * refactor: Rename context -> binding * refactor: Revert formatting changes * refactor: Revert unnecessary routing through binding/control in fallback Configured IConfigValue * refactor: context -> binding * feat: Add ConfigControlContext for access to things like sizing info
1 parent 0dc5379 commit b167511

25 files changed

Lines changed: 678 additions & 40 deletions

common-example/src/main/java/com/example/balm/ExampleConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.balm;
22

3+
import net.blay09.mods.balm.platform.config.reflection.CustomControl;
34
import net.blay09.mods.balm.platform.config.reflection.Comment;
45
import net.blay09.mods.balm.platform.config.reflection.Config;
56
import net.blay09.mods.balm.platform.config.reflection.Range;
@@ -9,4 +10,8 @@ public class ExampleConfig {
910
@Comment("Example ranged integer config value.")
1011
@Range(min = "0", max = "10")
1112
public int rangedValue = 5;
13+
14+
@Comment("Example custom widget config value.")
15+
@CustomControl("fancy_button")
16+
public boolean fancyBoolean;
1217
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
package com.example.balm.client;
22

33
import net.blay09.mods.balm.client.BalmClientRegistrars;
4+
import net.blay09.mods.balm.client.platform.config.ConfigControl;
5+
import net.blay09.mods.balm.client.platform.config.ConfigControlContext;
6+
import net.blay09.mods.balm.platform.config.schema.ConfigControlBinding;
7+
import net.minecraft.client.gui.components.Button;
8+
import net.minecraft.network.chat.Component;
49

510
public class BalmExampleClient {
611
public static void initialize(BalmClientRegistrars registrars) {
712
System.out.println("Hello client");
13+
registrars.customConfigControls(configControls -> configControls.register("fancy_button", ConfigControl.<Boolean>builder()
14+
.element(FancyConfigButton::new)
15+
.build()));
816
registrars.registerModule(new ClientCommandTestModule());
917
}
18+
19+
private static class FancyConfigButton extends Button.Plain {
20+
private final ConfigControlBinding<Boolean> binding;
21+
22+
public FancyConfigButton(ConfigControlBinding<Boolean> binding, ConfigControlContext context) {
23+
super(0, 0, context.entryWidth(), context.entryHeight(), Component.empty(), (_) -> binding.set(!binding.get()), DEFAULT_NARRATION);
24+
this.binding = binding;
25+
}
26+
27+
@Override
28+
public Component getMessage() {
29+
return Component.literal("Custom: " + binding.get());
30+
}
31+
}
1032
}

common/src/main/java/net/blay09/mods/balm/client/BalmClientRegistrars.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import net.blay09.mods.balm.client.gui.screens.inventory.BalmMenuScreenRegistrar;
88
import net.blay09.mods.balm.client.model.geom.BalmModelLayerRegistrar;
99
import net.blay09.mods.balm.client.particle.BalmParticleProviderRegistrar;
10+
import net.blay09.mods.balm.client.platform.config.BalmCustomConfigControlRegistrar;
11+
import net.blay09.mods.balm.client.platform.config.internal.BalmCustomConfigControlRegistrarImpl;
1012
import net.blay09.mods.balm.client.renderer.block.model.BalmBlockStateModelRegistrar;
1113
import net.blay09.mods.balm.client.renderer.blockentity.BalmBlockEntityRendererRegistrar;
1214
import net.blay09.mods.balm.client.renderer.entity.BalmEntityRendererRegistrar;
@@ -128,6 +130,15 @@ public void rangeSelectItemModelProperties(Consumer<BalmRangeSelectItemModelProp
128130
runtime.rangeSelectItemModelProperties(namespace, initializer);
129131
}
130132

133+
/**
134+
* Use this to register client-side config controls for schema properties that reference a control id.
135+
*
136+
* @param initializer Callback that receives a scoped registrar for config controls.
137+
*/
138+
public void customConfigControls(Consumer<BalmCustomConfigControlRegistrar> initializer) {
139+
initializer.accept(new BalmCustomConfigControlRegistrarImpl(namespace));
140+
}
141+
131142
public void registerModule(BalmClientModule module) {
132143
runtime.registerModule(this, module);
133144
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package net.blay09.mods.balm.client.platform.config;
2+
3+
import net.minecraft.resources.Identifier;
4+
5+
public interface BalmCustomConfigControlRegistrar {
6+
<T> void register(String path, ConfigControl<T> control);
7+
8+
<T> void register(Identifier identifier, ConfigControl<T> control);
9+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package net.blay09.mods.balm.client.platform.config;
2+
3+
import com.mojang.serialization.DataResult;
4+
import net.blay09.mods.balm.platform.config.schema.ConfigControlBinding;
5+
import net.minecraft.network.chat.Component;
6+
import org.jspecify.annotations.Nullable;
7+
8+
import java.util.Optional;
9+
import java.util.function.BiFunction;
10+
import java.util.function.Function;
11+
12+
public class ConfigControl<T> {
13+
private final ElementFactory<T> elementFactory;
14+
private final BiFunction<ConfigControlBinding<T>, T, DataResult<T>> validator;
15+
private final Function<ConfigControlBinding<T>, Optional<Component>> validationHint;
16+
17+
private ConfigControl(ElementFactory<T> elementFactory, BiFunction<ConfigControlBinding<T>, T, DataResult<T>> validator, Function<ConfigControlBinding<T>, Optional<Component>> validationHint) {
18+
this.elementFactory = elementFactory;
19+
this.validator = validator;
20+
this.validationHint = validationHint;
21+
}
22+
23+
private static <T> @Nullable Object noElement(ConfigControlBinding<T> binding, ConfigControlContext context) {
24+
return null;
25+
}
26+
27+
public Optional<Object> createElement(ConfigControlBinding<T> binding, ConfigControlContext context) {
28+
return Optional.ofNullable(elementFactory.create(binding, context));
29+
}
30+
31+
public DataResult<T> validate(ConfigControlBinding<T> binding, T value) {
32+
return validator.apply(binding, value);
33+
}
34+
35+
public Optional<Component> getValidationHint(ConfigControlBinding<T> binding) {
36+
return validationHint.apply(binding);
37+
}
38+
39+
public static <T> Builder<T> builder() {
40+
return new Builder<>();
41+
}
42+
43+
public static final class Builder<T> {
44+
private ElementFactory<T> elementFactory = ConfigControl::noElement;
45+
private BiFunction<ConfigControlBinding<T>, T, DataResult<T>> validator = (context, value) -> context.validateValue(value);
46+
private Function<ConfigControlBinding<T>, Optional<Component>> validationHint = _ -> Optional.empty();
47+
48+
public Builder<T> element(ElementFactory<T> elementFactory) {
49+
this.elementFactory = elementFactory;
50+
return this;
51+
}
52+
53+
public Builder<T> validator(BiFunction<ConfigControlBinding<T>, T, DataResult<T>> validator) {
54+
this.validator = validator;
55+
return this;
56+
}
57+
58+
public Builder<T> validationHint(Component validationHint) {
59+
this.validationHint = context -> Optional.of(validationHint);
60+
return this;
61+
}
62+
63+
public Builder<T> validationHint(Function<ConfigControlBinding<T>, Optional<Component>> validationHint) {
64+
this.validationHint = validationHint;
65+
return this;
66+
}
67+
68+
public ConfigControl<T> build() {
69+
return new ConfigControl<>(elementFactory, validator, validationHint);
70+
}
71+
}
72+
73+
@FunctionalInterface
74+
public interface ElementFactory<T> {
75+
@Nullable
76+
Object create(ConfigControlBinding<T> binding, ConfigControlContext context);
77+
}
78+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package net.blay09.mods.balm.client.platform.config;
2+
3+
import net.blay09.mods.balm.platform.config.schema.ConfiguredProperty;
4+
import net.minecraft.network.chat.Component;
5+
6+
public interface ConfigControlContext {
7+
8+
int entryWidth();
9+
10+
int entryHeight();
11+
12+
Component label(ConfiguredProperty<?> property);
13+
14+
Component tooltip(ConfiguredProperty<?> property);
15+
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package net.blay09.mods.balm.client.platform.config.internal;
2+
3+
import net.blay09.mods.balm.client.platform.config.BalmCustomConfigControlRegistrar;
4+
import net.blay09.mods.balm.client.platform.config.ConfigControl;
5+
import net.minecraft.resources.Identifier;
6+
7+
public class BalmCustomConfigControlRegistrarImpl implements BalmCustomConfigControlRegistrar {
8+
private final String namespace;
9+
10+
public BalmCustomConfigControlRegistrarImpl(String namespace) {
11+
this.namespace = namespace;
12+
}
13+
14+
@Override
15+
public <T> void register(String path, ConfigControl<T> control) {
16+
register(Identifier.fromNamespaceAndPath(namespace, path), control);
17+
}
18+
19+
@Override
20+
public <T> void register(Identifier identifier, ConfigControl<T> control) {
21+
ConfigControlRegistry.register(identifier, control);
22+
}
23+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package net.blay09.mods.balm.client.platform.config.internal;
2+
3+
import net.blay09.mods.balm.client.platform.config.ConfigControlContext;
4+
import net.blay09.mods.balm.platform.config.schema.ConfiguredProperty;
5+
import net.blay09.mods.balm.platform.config.util.ConfigLocalization;
6+
import net.minecraft.network.chat.Component;
7+
8+
public class ConfigControlContextImpl implements ConfigControlContext {
9+
private final int entryWidth;
10+
private final int entryHeight;
11+
12+
public ConfigControlContextImpl(int entryWidth, int entryHeight) {
13+
this.entryWidth = entryWidth;
14+
this.entryHeight = entryHeight;
15+
}
16+
17+
@Override
18+
public int entryWidth() {
19+
return entryWidth;
20+
}
21+
22+
@Override
23+
public int entryHeight() {
24+
return entryHeight;
25+
}
26+
27+
@Override
28+
public Component label(ConfiguredProperty<?> property) {
29+
return Component.translatable(ConfigLocalization.forProperty(property));
30+
}
31+
32+
@Override
33+
public Component tooltip(ConfiguredProperty<?> property) {
34+
return Component.translatable(ConfigLocalization.forPropertyTooltip(property));
35+
}
36+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package net.blay09.mods.balm.client.platform.config.internal;
2+
3+
import net.blay09.mods.balm.client.platform.config.ConfigControl;
4+
import net.blay09.mods.balm.client.platform.config.ConfigControlContext;
5+
import net.blay09.mods.balm.platform.config.schema.ConfigControlBinding;
6+
import net.minecraft.resources.Identifier;
7+
8+
import java.util.Map;
9+
import java.util.Optional;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
12+
public final class ConfigControlRegistry {
13+
private static final Map<Identifier, ConfigControl<?>> controls = new ConcurrentHashMap<>();
14+
15+
private ConfigControlRegistry() {
16+
}
17+
18+
public static <T> void register(Identifier identifier, ConfigControl<T> control) {
19+
controls.put(identifier, control);
20+
}
21+
22+
@SuppressWarnings("unchecked")
23+
public static <T> Optional<ConfigControl<T>> get(Identifier identifier) {
24+
return Optional.ofNullable((ConfigControl<T>) controls.get(identifier));
25+
}
26+
27+
public static <T> Optional<Object> createElement(Identifier identifier, ConfigControlBinding<T> binding, ConfigControlContext context) {
28+
return ConfigControlRegistry.<T>get(identifier).flatMap(control -> control.createElement(binding, context));
29+
}
30+
}

common/src/main/java/net/blay09/mods/balm/client/platform/module/BalmClientModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import net.blay09.mods.balm.client.BalmRangeSelectItemModelPropertyRegistrar;
88
import net.blay09.mods.balm.client.commands.BalmClientCommands;
99
import net.blay09.mods.balm.client.gui.screens.inventory.BalmMenuScreenRegistrar;
10+
import net.blay09.mods.balm.client.platform.config.BalmCustomConfigControlRegistrar;
1011
import net.blay09.mods.balm.client.renderer.blockentity.BalmBlockEntityRendererRegistrar;
1112
import net.blay09.mods.balm.client.renderer.entity.BalmEntityRendererRegistrar;
1213
import net.blay09.mods.balm.client.renderer.block.model.BalmBlockStateModelRegistrar;
@@ -61,6 +62,9 @@ default void registerClientTooltipComponents(BalmClientTooltipComponentRegistrar
6162
default void registerRangeSelectItemModelProperties(BalmRangeSelectItemModelPropertyRegistrar rangeSelectItemModelProperties) {
6263
}
6364

65+
default void registerCustomConfigControls(BalmCustomConfigControlRegistrar customConfigControls) {
66+
}
67+
6468
default void registerClientCommands(BalmClientCommands commands) {
6569
}
6670

0 commit comments

Comments
 (0)