[coat-bridge] Implement first version of Tweed Coat bridge

This commit is contained in:
2025-10-31 14:07:38 +01:00
parent f514894c9c
commit 7c625e6cca
27 changed files with 1118 additions and 2 deletions

View File

@@ -4,5 +4,18 @@ plugins {
} }
dependencies { dependencies {
modImplementation(mcLibs.coat) compileOnly("de.siphalor.tweed5:tweed5-core")
compileOnly("de.siphalor.tweed5:tweed5-attributes-extension")
compileOnly("de.siphalor.tweed5:tweed5-default-extensions")
compileOnly("de.siphalor.tweed5:tweed5-weaver-pojo")
modCompileOnly(mcLibs.coat)
listOf("fabric-key-binding-api-v1", "fabric-resource-loader-v0").forEach {
modTestmodImplementation(fabricApi.module(it, mcLibs.versions.fabric.api.get()))
}
testmodImplementation(project(":tweed5-bundle", configuration = "minecraftModElements"))
modTestmodImplementation(mcLibs.coat)
modTestmodImplementation(mcLibs.amecs.api)
testmodImplementation(project(":tweed5-fabric-helper"))
testmodImplementation("de.siphalor.tweed5:tweed5-serde-hjson")
} }

View File

@@ -0,0 +1,2 @@
module.name = Tweed 5 Coat Bridge
module.description = Provides a system that allows to generate a Coat config screen for a Tweed config.

View File

@@ -0,0 +1,19 @@
package de.siphalor.tweed5.coat.bridge.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import lombok.Builder;
import lombok.Getter;
import net.minecraft.network.chat.Component;
import java.util.function.Consumer;
@Builder
@Getter
public class ConfigScreenCreateParams<T> {
private final ConfigEntry<T> rootEntry;
private final T currentValue;
private final T defaultValue;
private final Component title;
private final String translationKeyPrefix;
private final Consumer<T> saveHandler;
}

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.coat.bridge.api;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TweedCoatAttributes {
public static final String BACKGROUND_TEXTURE = "backgroundTexture";
public static final String TRANSLATION_KEY = "translationKey";
public static final String ENUM_TRANSLATION_KEY = "enumTranslationKey";
}

View File

@@ -0,0 +1,26 @@
package de.siphalor.tweed5.coat.bridge.api;
import de.siphalor.coat.screen.ConfigScreen;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatMapper;
import de.siphalor.tweed5.coat.bridge.impl.TweedCoatBridgeExtensionImpl;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
public interface TweedCoatBridgeExtension extends TweedExtension {
Class<? extends TweedCoatBridgeExtension> DEFAULT = TweedCoatBridgeExtensionImpl.class;
String EXTENSION_ID = "coat-bridge";
//static <T> Function<ConfigEntry<T>, ConfigScreen> createConfigScreen(T value) {
// return configEntry -> configEntry.container().extension(TweedCoatBridgeExtension.class)
// .map(extension -> extension.createConfigScreen(configEntry, value))
// .orElseThrow(() -> new IllegalStateException("No TweedCoatBridgeExtension present"));
//}
void addMapper(TweedCoatMapper<?> mapper);
<T> ConfigScreen createConfigScreen(ConfigScreenCreateParams<T> params);
@Override
default String getId() {
return EXTENSION_ID;
}
}

View File

@@ -0,0 +1,69 @@
package de.siphalor.tweed5.coat.bridge.api;
import de.siphalor.coat.util.EnumeratedMaterial;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatMapper;
import de.siphalor.tweed5.coat.bridge.impl.TweedCoatMappersImpl;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.function.Function;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TweedCoatMappers {
public static TweedCoatMapper<Byte> byteTextMapper() {
return TweedCoatMappersImpl.BYTE_TEXT_MAPPER;
}
public static TweedCoatMapper<Short> shortTextMapper() {
return TweedCoatMappersImpl.SHORT_TEXT_MAPPER;
}
public static TweedCoatMapper<Integer> integerTextMapper() {
return TweedCoatMappersImpl.INTEGER_TEXT_MAPPER;
}
public static TweedCoatMapper<Long> longTextMapper() {
return TweedCoatMappersImpl.LONG_TEXT_MAPPER;
}
public static TweedCoatMapper<Float> floatTextMapper() {
return TweedCoatMappersImpl.FLOAT_TEXT_MAPPER;
}
public static TweedCoatMapper<Double> doubleTextMapper() {
return TweedCoatMappersImpl.DOUBLE_TEXT_MAPPER;
}
public static TweedCoatMapper<Boolean> booleanCheckboxMapper() {
return TweedCoatMappersImpl.BOOLEAN_CHECKBOX_MAPPER;
}
public static TweedCoatMapper<String> stringTextMapper() {
return TweedCoatMappersImpl.STRING_TEXT_MAPPER;
}
public static <T extends Enum<T>> TweedCoatMapper<T> enumCycleButtonMapper() {
//noinspection unchecked
return (TweedCoatMapper<T>) TweedCoatMappersImpl.ENUM_CYCLE_BUTTON_MAPPER;
}
public static <T> TweedCoatMapper<T> enumeratedMaterialCycleButtonMapper(
Class<T> valueClass,
EnumeratedMaterial<T> material
) {
return new TweedCoatMappersImpl.EnumeratedMaterialCycleButtonMapper<>(valueClass, material);
}
public static TweedCoatMapper<Object> compoundCategoryMapper() {
return TweedCoatMappersImpl.COMPOUND_CATEGORY_MAPPER;
}
public static <T> TweedCoatMapper<T> convertingTextMapper(
Class<T> valueClass,
Function<T, String> textMapper,
Function<String, T> textParser
) {
//noinspection unchecked
return TweedCoatMappersImpl.convertingTextMapper(new Class[]{valueClass}, textMapper, textParser);
}
}

View File

@@ -0,0 +1,22 @@
package de.siphalor.tweed5.coat.bridge.api;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import org.jspecify.annotations.Nullable;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TweedCoatMappingUtils {
public static MutableComponent translatableComponentWithFallback(String translationKey, @Nullable String fallback) {
return Component.translatableWithFallback(translationKey, fallback == null ? "" : fallback);
// FIXME
//if (I18n.exists(translationKey)) {
// return Component.translatable(translationKey);
//} else if (fallback != null) {
// return Component.literal(fallback);
//} else {
// return Component.empty();
//}
}
}

View File

@@ -0,0 +1,20 @@
package de.siphalor.tweed5.coat.bridge.api.mapping;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
@Builder
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class TweedCoatEntryCreationContext<T> {
private final ConfigEntry<T> entry;
private final T currentValue;
private final T defaultValue;
private final @Nullable Consumer<T> parentSaveHandler;
}

View File

@@ -0,0 +1,50 @@
package de.siphalor.tweed5.coat.bridge.api.mapping;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jspecify.annotations.Nullable;
@RequiredArgsConstructor
@Getter
public
class TweedCoatEntryMappingContext {
@Getter(AccessLevel.NONE)
private final TweedCoatMapper<?> mappingDelegate;
private final String entryName;
private final String translationKeyPrefix;
private final @Nullable Class<?> parentWidgetClass;
public static Builder rootBuilder(TweedCoatMapper<?> mappingDelegate, String translationKeyPrefix) {
return new Builder(mappingDelegate, "root", translationKeyPrefix);
}
public <T> TweedCoatEntryMappingResult<T, ?> mapEntry(ConfigEntry<T> entry, TweedCoatEntryMappingContext context) {
//noinspection unchecked,rawtypes
return (TweedCoatEntryMappingResult<T, ?>) mappingDelegate.mapEntry((ConfigEntry) entry, context);
}
public Builder subContextBuilder(String entryName) {
return new Builder(mappingDelegate, entryName, translationKeyPrefix + "." + entryName);
}
@Setter
public static class Builder {
private final TweedCoatMapper<?> mappingDelegate;
private final String entryName;
private String translationKeyPrefix;
private @Nullable Class<?> parentWidgetClass;
private Builder(TweedCoatMapper<?> mappingDelegate, String entryName, String translationKeyPrefix) {
this.mappingDelegate = mappingDelegate;
this.entryName = entryName;
this.translationKeyPrefix = translationKeyPrefix;
}
public TweedCoatEntryMappingContext build() {
return new TweedCoatEntryMappingContext(mappingDelegate, entryName, translationKeyPrefix, parentWidgetClass);
}
}
}

View File

@@ -0,0 +1,61 @@
package de.siphalor.tweed5.coat.bridge.api.mapping;
import de.siphalor.coat.handler.ConfigEntryHandler;
import de.siphalor.coat.input.ConfigInput;
import de.siphalor.coat.screen.ConfigContentWidget;
import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
/**
* The result of a {@link TweedCoatMapper}.
* <br />
* There are three types of results:
* <ol>
* <li>The mapper isn't applicable to the entry.</li>
* <li>The mapper is applicable and provides methods for creating
* {@link ConfigInput} and {@link ConfigEntryHandler} instances for the config entry.</li>
* FIXME
* </ol>
*
*
* @param <T> the actual (Tweed) type of the entry
* @param <U> the UI (coat) type of the entry
*/
public interface TweedCoatEntryMappingResult<T, U> {
TweedCoatEntryMappingResult<?, ?> NOT_APPLICABLE = new TweedCoatEntryMappingResult<Object, Object>() {
@Override
public boolean isApplicable() {
return false;
}
@Override
public @Nullable ConfigInput<Object> createInput(TweedCoatEntryCreationContext<Object> context) {
return null;
}
@Override
public @Nullable ConfigEntryHandler<Object> createHandler(TweedCoatEntryCreationContext<Object> context) {
return null;
}
@Override
public @Nullable ConfigContentWidget createContentWidget(TweedCoatEntryCreationContext<Object> context) {
return null;
}
};
static <T, U> TweedCoatEntryMappingResult<T, U> notApplicable() {
//noinspection unchecked
return (TweedCoatEntryMappingResult<T, U>) NOT_APPLICABLE;
}
boolean isApplicable();
@Nullable ConfigInput<U> createInput(TweedCoatEntryCreationContext<T> context);
@Nullable ConfigEntryHandler<U> createHandler(TweedCoatEntryCreationContext<T> context);
@Nullable ConfigContentWidget createContentWidget(TweedCoatEntryCreationContext<T> context);
}

View File

@@ -0,0 +1,8 @@
package de.siphalor.tweed5.coat.bridge.api.mapping;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
public interface TweedCoatMapper<T> {
TweedCoatEntryMappingResult<T, ?> mapEntry(ConfigEntry<T> entry, TweedCoatEntryMappingContext context);
}

View File

@@ -0,0 +1,84 @@
package de.siphalor.tweed5.coat.bridge.api.mapping.handler;
import de.siphalor.coat.handler.ConfigEntryHandler;
import de.siphalor.coat.handler.Message;
import de.siphalor.tweed5.coat.bridge.impl.TweedCoatMappersImpl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssueLevel;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssues;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import net.minecraft.network.chat.Component;
import org.jspecify.annotations.Nullable;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class BasicTweedCoatEntryHandler<T extends @Nullable Object> implements ConfigEntryHandler<T> {
protected final ConfigEntry<T> configEntry;
protected final T defaultValue;
protected final Consumer<T> parentSaveHandler;
protected final ValidationExtension validationExtension;
public BasicTweedCoatEntryHandler(ConfigEntry<T> configEntry, T defaultValue, Consumer<T> parentSaveHandler) {
this.configEntry = configEntry;
this.defaultValue = defaultValue;
this.parentSaveHandler = parentSaveHandler;
this.validationExtension = configEntry.container().extension(ValidationExtension.class)
.orElseThrow(() -> new IllegalStateException("No validation extension registered"));
}
@Override
public T getDefault() {
return defaultValue;
}
@Override
public Collection<Message> getMessages(T value) {
ValidationIssues issues = validationExtension.validate(configEntry, value);
return issues.issuesByPath().values().stream()
.flatMap(entryIssues -> entryIssues.issues().stream())
.map(issue -> new Message(
mapLevel(issue.level()),
Component.literal(issue.message())
))
.collect(Collectors.toList());
}
private static Message.Level mapLevel(ValidationIssueLevel level) {
switch (level) {
case INFO:
return Message.Level.INFO;
case WARN:
return Message.Level.WARNING;
case ERROR:
return Message.Level.ERROR;
default:
throw new IllegalStateException("Unknown validation issue level " + level);
}
}
@Override
public void save(T value) {
parentSaveHandler.accept(processSaveValue(value));
}
@Override
public Component asText(T value) {
return Component.literal(Objects.toString(value));
}
protected T processSaveValue(T value) {
ValidationResult<T> validationResult = validationExtension.validateValueFlat(configEntry, value);
if (validationResult.hasError()) {
TweedCoatMappersImpl.log.warn(
"Failed to save value " + value + " because of issues: " + validationResult.issues()
+ "; using default: " + defaultValue + " instead"
);
return defaultValue;
}
return validationResult.value();
}
}

View File

@@ -0,0 +1,65 @@
package de.siphalor.tweed5.coat.bridge.api.mapping.handler;
import de.siphalor.coat.handler.ConfigEntryHandler;
import de.siphalor.coat.handler.Message;
import lombok.RequiredArgsConstructor;
import lombok.extern.apachecommons.CommonsLog;
import net.minecraft.network.chat.Component;
import org.jspecify.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Function;
@RequiredArgsConstructor
@CommonsLog
public class ConvertingTweedCoatEntryHandler<T extends @Nullable Object, C> implements ConfigEntryHandler<C> {
private static final String CONVERSION_EXCEPTION_TEXT_KEY = "tweed5_coat_bridge.handler.conversion.exception";
private final ConfigEntryHandler<T> inner;
private final Function<T, C> toCoatMapper;
private final Function<C, T> fromCoatMapper;
@Override
public C getDefault() {
return toCoatMapper.apply(inner.getDefault());
}
@Override
public Collection<Message> getMessages(C value) {
try {
T innerValue = fromCoatMapper.apply(value);
return inner.getMessages(innerValue);
} catch (Exception e) {
return Collections.singletonList(new Message(
Message.Level.ERROR,
Component.translatable(CONVERSION_EXCEPTION_TEXT_KEY, e.getMessage())
));
}
}
@Override
public void save(C value) {
inner.save(convertSaveValue(value));
}
protected T convertSaveValue(C value) {
try {
return fromCoatMapper.apply(value);
} catch (Exception e) {
log.warn(
"Failed to convert value "
+ value
+ " for saving, using default: "
+ inner.getDefault(), e
);
return inner.getDefault();
}
}
@Override
public Component asText(C value) {
return Component.literal(Objects.toString(value));
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.coat.bridge.api.mapping.handler;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.coat.bridge.api.mapping;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.coat.bridge.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,28 @@
package de.siphalor.tweed5.coat.bridge.impl;
import de.siphalor.coat.util.EnumeratedMaterial;
import lombok.RequiredArgsConstructor;
import net.minecraft.network.chat.Component;
import java.util.Locale;
import static de.siphalor.tweed5.coat.bridge.api.TweedCoatMappingUtils.translatableComponentWithFallback;
@RequiredArgsConstructor
public class CoatEnumMaterial<T> implements EnumeratedMaterial<T> {
private final Class<T> enumClass;
private final String textTranslationKeyPrefix;
@Override
public T[] values() {
return enumClass.getEnumConstants();
}
@Override
public Component asText(T value) {
return translatableComponentWithFallback(
textTranslationKeyPrefix + "." + value.toString().toLowerCase(Locale.ROOT),
value.toString()
);
}
}

View File

@@ -0,0 +1,83 @@
package de.siphalor.tweed5.coat.bridge.impl;
import de.siphalor.coat.screen.ConfigContentWidget;
import de.siphalor.coat.screen.ConfigScreen;
import de.siphalor.tweed5.coat.bridge.api.ConfigScreenCreateParams;
import de.siphalor.tweed5.coat.bridge.api.TweedCoatBridgeExtension;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatEntryCreationContext;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatEntryMappingContext;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatEntryMappingResult;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatMapper;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import net.minecraft.client.Minecraft;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TweedCoatBridgeExtensionImpl implements TweedCoatBridgeExtension {
private final PatchworkPartAccess<CustomData> customDataAccess;
private final List<TweedCoatMapper<?>> mappers = new ArrayList<>();
public TweedCoatBridgeExtensionImpl(TweedExtensionSetupContext setupContext) {
customDataAccess = setupContext.registerEntryExtensionData(CustomData.class);
}
@Override
public void addMapper(TweedCoatMapper<?> mapper) {
mappers.add(mapper);
}
@Override
public <T> ConfigScreen createConfigScreen(ConfigScreenCreateParams<T> params) {
Minecraft minecraft = Minecraft.getInstance();
TweedCoatEntryMappingContext mappingContext = TweedCoatEntryMappingContext.rootBuilder(
this::mapEntry,
params.translationKeyPrefix()
).parentWidgetClass(ConfigScreen.class).build();
TweedCoatEntryMappingResult<T, ?> rootResult = mapEntry(params.rootEntry(), mappingContext);
if (!rootResult.isApplicable()) {
throw new IllegalStateException("Failed to map root entry");
}
T value = params.rootEntry().deepCopy(params.currentValue());
TweedCoatEntryCreationContext<T> creationContext = TweedCoatEntryCreationContext.<T>builder()
.entry(params.rootEntry())
.currentValue(value)
.defaultValue(params.defaultValue())
.build();
ConfigContentWidget contentWidget = rootResult.createContentWidget(creationContext);
if (contentWidget == null) {
throw new IllegalStateException("Failed to create root content widget");
}
ConfigScreen configScreen = new ConfigScreen(
minecraft.screen, params.title(), Collections.singletonList(contentWidget)
);
configScreen.setOnSave(() -> params.saveHandler().accept(value));
return configScreen;
}
private <T> TweedCoatEntryMappingResult<T, ?> mapEntry(
ConfigEntry<T> entry,
TweedCoatEntryMappingContext context
) {
for (TweedCoatMapper<?> mapper : mappers) {
//noinspection rawtypes,unchecked
TweedCoatEntryMappingResult<T, ?> result = mapper.mapEntry((ConfigEntry) entry, context);
if (result.isApplicable()) {
return result;
}
}
return TweedCoatEntryMappingResult.notApplicable();
}
private static class CustomData {
}
}

View File

@@ -0,0 +1,370 @@
package de.siphalor.tweed5.coat.bridge.impl;
import de.siphalor.coat.handler.ConfigEntryHandler;
import de.siphalor.coat.input.CheckBoxConfigInput;
import de.siphalor.coat.input.ConfigInput;
import de.siphalor.coat.input.CycleButtonConfigInput;
import de.siphalor.coat.input.TextConfigInput;
import de.siphalor.coat.list.complex.ConfigCategoryWidget;
import de.siphalor.coat.list.entry.ConfigCategoryConfigEntry;
import de.siphalor.coat.screen.ConfigContentWidget;
import de.siphalor.coat.util.EnumeratedMaterial;
import de.siphalor.tweed5.attributesextension.api.AttributesExtension;
import de.siphalor.tweed5.coat.bridge.api.TweedCoatAttributes;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatEntryCreationContext;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatEntryMappingContext;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatEntryMappingResult;
import de.siphalor.tweed5.coat.bridge.api.mapping.TweedCoatMapper;
import de.siphalor.tweed5.coat.bridge.api.mapping.handler.BasicTweedCoatEntryHandler;
import de.siphalor.tweed5.coat.bridge.api.mapping.handler.ConvertingTweedCoatEntryHandler;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.apachecommons.CommonsLog;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static de.siphalor.tweed5.coat.bridge.api.TweedCoatMappingUtils.translatableComponentWithFallback;
@CommonsLog
@SuppressWarnings("unchecked")
public class TweedCoatMappersImpl {
public static TweedCoatMapper<Byte> BYTE_TEXT_MAPPER = convertingTextMapper(
new Class[]{Byte.class, byte.class},
value -> Byte.toString(value),
Byte::parseByte
);
public static TweedCoatMapper<Short> SHORT_TEXT_MAPPER = convertingTextMapper(
new Class[]{Short.class, short.class},
value -> Short.toString(value),
Short::parseShort
);
public static TweedCoatMapper<Integer> INTEGER_TEXT_MAPPER = convertingTextMapper(
new Class[]{Integer.class, int.class},
value -> Integer.toString(value),
Integer::parseInt
);
public static TweedCoatMapper<Long> LONG_TEXT_MAPPER = convertingTextMapper(
new Class[]{Long.class, long.class},
value -> Long.toString(value),
Long::parseLong
);
public static TweedCoatMapper<Float> FLOAT_TEXT_MAPPER = convertingTextMapper(
new Class[]{Float.class, float.class},
value -> Float.toString(value),
Float::parseFloat
);
public static TweedCoatMapper<Double> DOUBLE_TEXT_MAPPER = convertingTextMapper(
new Class[]{Double.class, double.class},
value -> Double.toString(value),
Double::parseDouble
);
public static TweedCoatMapper<Boolean> BOOLEAN_CHECKBOX_MAPPER
= new SimpleMapper<Boolean>(new Class[]{Boolean.class, boolean.class}, CheckBoxConfigInput::new);
public static TweedCoatMapper<String> STRING_TEXT_MAPPER = new SimpleMapper<String>(
new Class[]{String.class},
TextConfigInput::new
);
public static TweedCoatMapper<Enum<?>> ENUM_CYCLE_BUTTON_MAPPER = new EnumCycleButtonMapper<>();
public static TweedCoatMapper<Object> COMPOUND_CATEGORY_MAPPER = new CompoundCategoryMapper<>();
public static <T> TweedCoatMapper<T> convertingTextMapper(
Class<T>[] valueClasses,
Function<T, String> textMapper,
Function<String, T> textParser
) {
return new ConvertingTextMapper<>(valueClasses, textMapper, textParser);
}
@RequiredArgsConstructor
public static class ConvertingTextMapper<T> implements TweedCoatMapper<T> {
private final Class<T>[] valueClasses;
private final Function<T, String> textMapper;
private final Function<String, T> textParser;
@Override
public TweedCoatEntryMappingResult<T, String> mapEntry(ConfigEntry<T> entry, TweedCoatEntryMappingContext context) {
boolean applicable = anyClassMatches(entry.valueClass(), valueClasses);
return new TweedCoatEntryMappingResult<T, String>() {
@Override
public boolean isApplicable() {
return applicable;
}
@Override
public ConfigInput<String> createInput(TweedCoatEntryCreationContext<T> context) {
return new TextConfigInput(textMapper.apply(context.currentValue()));
}
@Override
public ConfigEntryHandler<String> createHandler(TweedCoatEntryCreationContext<T> context) {
if (context.parentSaveHandler() == null) {
throw new IllegalArgumentException("No parent save handler provided");
}
return new ConvertingTweedCoatEntryHandler<>(
new BasicTweedCoatEntryHandler<>(
context.entry(), context.defaultValue(), context.parentSaveHandler()
),
textMapper,
textParser
);
}
@Override
public @Nullable ConfigContentWidget createContentWidget(TweedCoatEntryCreationContext<T> context) {
return null;
}
};
}
}
@RequiredArgsConstructor
public static class EnumeratedMaterialCycleButtonMapper<T> implements TweedCoatMapper<T> {
private final Class<T> valueClass;
private final EnumeratedMaterial<T> material;
@Override
public TweedCoatEntryMappingResult<T, ?> mapEntry(ConfigEntry<T> entry, TweedCoatEntryMappingContext context) {
if (!valueClass.isAssignableFrom(entry.valueClass())) {
return TweedCoatEntryMappingResult.notApplicable();
}
return new CycleButtonMappingResult<>(material);
}
}
private static class EnumCycleButtonMapper<T extends Enum<?>> implements TweedCoatMapper<T> {
@Override
public TweedCoatEntryMappingResult<T, ?> mapEntry(ConfigEntry<T> entry, TweedCoatEntryMappingContext context) {
if (!Enum.class.isAssignableFrom(entry.valueClass())) {
return TweedCoatEntryMappingResult.notApplicable();
}
Class<T> enumClass = entry.valueClass();
String translationKeyPrefix = entry.container().extension(AttributesExtension.class)
.map(extension -> extension.getAttributeValue(entry, TweedCoatAttributes.ENUM_TRANSLATION_KEY))
.orElse(enumClass.getPackage().getName());
CoatEnumMaterial<T> material = new CoatEnumMaterial<>(enumClass, translationKeyPrefix + ".");
return new CycleButtonMappingResult<>(material);
}
}
@RequiredArgsConstructor
private static class CycleButtonMappingResult<T> implements TweedCoatEntryMappingResult<T, T> {
private final EnumeratedMaterial<T> material;
@Override
public boolean isApplicable() {
return true;
}
@Override
public ConfigInput<T> createInput(TweedCoatEntryCreationContext<T> context) {
return new CycleButtonConfigInput<>(material, false, context.currentValue());
}
@Override
public ConfigEntryHandler<T> createHandler(TweedCoatEntryCreationContext<T> context) {
if (context.parentSaveHandler() == null) {
throw new IllegalArgumentException("No parent save handler provided");
}
return new BasicTweedCoatEntryHandler<>(
context.entry(), context.defaultValue(), context.parentSaveHandler()
);
}
@Override
public @Nullable ConfigContentWidget createContentWidget(TweedCoatEntryCreationContext<T> context) {
return null;
}
}
@RequiredArgsConstructor
private static class SimpleMapper<T> implements TweedCoatMapper<T> {
private final Class<T>[] valueClasses;
private final Function<T, ConfigInput<T>> inputFactory;
@Override
public TweedCoatEntryMappingResult<T, T> mapEntry(ConfigEntry<T> entry, TweedCoatEntryMappingContext context) {
matchingClass: {
for (Class<T> valueClass : valueClasses) {
if (entry.valueClass() == valueClass) {
break matchingClass;
}
}
return TweedCoatEntryMappingResult.notApplicable();
}
return new TweedCoatEntryMappingResult<T, T>() {
@Override
public boolean isApplicable() {
return true;
}
@Override
public ConfigInput<T> createInput(TweedCoatEntryCreationContext<T> context) {
return inputFactory.apply(context.currentValue());
}
@Override
public ConfigEntryHandler<T> createHandler(TweedCoatEntryCreationContext<T> context) {
if (context.parentSaveHandler() == null) {
throw new IllegalArgumentException("No parent save handler provided");
}
return new BasicTweedCoatEntryHandler<>(
context.entry(), context.defaultValue(), context.parentSaveHandler()
);
}
@Override
public @Nullable ConfigContentWidget createContentWidget(TweedCoatEntryCreationContext<T> context) {
return null;
}
};
}
}
private static class CompoundCategoryMapper<T> implements TweedCoatMapper<T> {
@Override
public TweedCoatEntryMappingResult<T, ?> mapEntry(ConfigEntry<T> entry, TweedCoatEntryMappingContext mappingContext) {
@Value
class MappedEntry<U> {
String name;
String translationKeyPrefix;
ConfigEntry<U> entry;
TweedCoatEntryMappingContext mappingContext;
TweedCoatEntryMappingResult<U, ?> mappingResult;
}
if (!(entry instanceof CompoundConfigEntry)) {
return TweedCoatEntryMappingResult.notApplicable();
}
CompoundConfigEntry<T> compoundEntry = (CompoundConfigEntry<T>) entry;
Optional<AttributesExtension> attributesExtension = entry.container().extension(AttributesExtension.class);
ResourceLocation backgroundTexture = attributesExtension
.map(extension -> extension.getAttributeValue(
entry,
TweedCoatAttributes.BACKGROUND_TEXTURE
))
.map(ResourceLocation::tryParse)
.orElse(null);
String translationKey = attributesExtension
.map(extension -> extension.getAttributeValue(
entry,
TweedCoatAttributes.TRANSLATION_KEY
))
.orElse(mappingContext.translationKeyPrefix());
List<MappedEntry<Object>> mappedEntries = compoundEntry.subEntries().entrySet().stream()
.map(mapEntry -> {
String subTranslationKeyPrefix = translationKey + "." + mapEntry.getKey();
TweedCoatEntryMappingContext subMappingContext = mappingContext.subContextBuilder(mapEntry.getKey())
.translationKeyPrefix(subTranslationKeyPrefix)
.parentWidgetClass(ConfigCategoryWidget.class)
.build();
return new MappedEntry<>(
mapEntry.getKey(),
subTranslationKeyPrefix,
(ConfigEntry<Object>) mapEntry.getValue(),
subMappingContext,
(TweedCoatEntryMappingResult<@NonNull Object, ?>) subMappingContext.mapEntry(
mapEntry.getValue(),
subMappingContext
)
);
})
.filter(mappedEntry -> mappedEntry.mappingResult.isApplicable())
.collect(Collectors.toList());
return new TweedCoatEntryMappingResult<T, T>() {
@Override
public boolean isApplicable() {
return true;
}
@Override
public @Nullable ConfigInput<T> createInput(TweedCoatEntryCreationContext<T> context) {
return null;
}
@Override
public @Nullable ConfigEntryHandler<T> createHandler(TweedCoatEntryCreationContext<T> context) {
return null;
}
@Override
public ConfigContentWidget createContentWidget(TweedCoatEntryCreationContext<T> context) {
ConfigCategoryWidget categoryWidget = new ConfigCategoryWidget(
Minecraft.getInstance(),
translatableComponentWithFallback(
translationKey,
mappingContext.entryName()
),
Collections.emptyList(),
backgroundTexture
);
for (MappedEntry<Object> mappedEntry : mappedEntries) {
TweedCoatEntryMappingResult<Object, ?> mappingResult = mappedEntry.mappingResult();
if (!mappingResult.isApplicable()) {
log.warn(
"Failed to resolve mapping for entry \"" + mappedEntry.name() + "\" at \""
+ translationKey + "\". Entry will be ignored in UI."
);
continue;
}
Object subEntryValue = compoundEntry.get(context.currentValue(), mappedEntry.name());
Object subEntryDefaultValue = compoundEntry.get(context.defaultValue(), mappedEntry.name());
TweedCoatEntryCreationContext<Object> creationContext = TweedCoatEntryCreationContext.builder()
.entry(mappedEntry.entry())
.currentValue(subEntryValue)
.defaultValue(subEntryDefaultValue)
.parentSaveHandler(value -> compoundEntry.set(context.currentValue(), mappedEntry.name(), value))
.build();
ConfigInput<?> input = mappingResult.createInput(creationContext);
if (input != null) {
ConfigCategoryConfigEntry<Object> entry = new ConfigCategoryConfigEntry<>(
translatableComponentWithFallback(mappedEntry.translationKeyPrefix(), mappedEntry.name()),
translatableComponentWithFallback(mappedEntry.translationKeyPrefix() + ".description", null),
(ConfigEntryHandler<Object>) mappingResult
.createHandler(creationContext),
(ConfigInput<Object>) input
);
categoryWidget.addEntry(entry);
continue;
}
ConfigContentWidget contentWidget = mappingResult.createContentWidget(creationContext);
if (contentWidget != null) {
categoryWidget.addSubTree(contentWidget);
}
}
return categoryWidget;
}
};
}
}
private static boolean anyClassMatches(Object value, Class<?>... classes) {
for (Class<?> clazz : classes) {
if (clazz.isInstance(value)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,6 @@
@NullMarked
@ApiStatus.Internal
package de.siphalor.tweed5.coat.bridge.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,3 @@
{
"tweed5_coat_bridge.handler.conversion.exception": "Failed to convert: %s"
}

View File

@@ -0,0 +1,89 @@
package de.siphalor.tweed5.coat.bridge.testmod;
import de.siphalor.amecs.api.PriorityKeyBinding;
import de.siphalor.coat.screen.ConfigScreen;
import de.siphalor.tweed5.coat.bridge.api.ConfigScreenCreateParams;
import de.siphalor.tweed5.coat.bridge.api.TweedCoatBridgeExtension;
import de.siphalor.tweed5.coat.bridge.api.TweedCoatMappers;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.data.hjson.HjsonSerde;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.fabric.helper.api.FabricConfigContainerHelper;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.TweedPojoWeaverBootstrapper;
import lombok.extern.apachecommons.CommonsLog;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.network.chat.Component;
import java.util.Arrays;
@CommonsLog
public class TweedCoatBridgeTestMod implements ClientModInitializer {
public static final String MOD_ID = "tweed5_coat_bridge_testmod";
private static final TweedCoatBridgeTestModConfig DEFAULT_CONFIG_VALUE = new TweedCoatBridgeTestModConfig();
private ConfigContainer<TweedCoatBridgeTestModConfig> configContainer;
private TweedCoatBridgeExtension configCoatBridgeExtension;
private FabricConfigContainerHelper<TweedCoatBridgeTestModConfig> configContainerHelper;
private TweedCoatBridgeTestModConfig config;
@Override
public void onInitializeClient() {
configContainer = TweedPojoWeaverBootstrapper.create(TweedCoatBridgeTestModConfig.class).weave();
configCoatBridgeExtension = configContainer.extension(TweedCoatBridgeExtension.class)
.orElseThrow(() -> new IllegalStateException("TweedCoatBridgeExtension not found"));
Arrays.asList(
TweedCoatMappers.compoundCategoryMapper(),
TweedCoatMappers.stringTextMapper(),
TweedCoatMappers.integerTextMapper()
).forEach(configCoatBridgeExtension::addMapper);
configContainer.initialize();
configContainerHelper = FabricConfigContainerHelper.create(
configContainer,
new HjsonSerde(new HjsonWriter.Options()),
MOD_ID
);
config = configContainerHelper.loadAndUpdateInConfigDirectory(() -> DEFAULT_CONFIG_VALUE);
KeyBindingHelper.registerKeyBinding(new ScreenKeyBinding(MOD_ID + ".config", 84, KeyMapping.Category.MISC));
log.info("current config");
}
private class ScreenKeyBinding extends KeyMapping implements PriorityKeyBinding {
public ScreenKeyBinding(String name, int key, Category category) {
super(name, key, category);
}
@Override
public boolean onPressedPriority() {
if (!(Minecraft.getInstance().screen instanceof TitleScreen)) {
return false;
}
ConfigScreen configScreen = configCoatBridgeExtension.createConfigScreen(
ConfigScreenCreateParams.<TweedCoatBridgeTestModConfig>builder()
.translationKeyPrefix(MOD_ID + ".config")
.title(Component.translatable(MOD_ID + ".title"))
.rootEntry(configContainer.rootEntry())
.currentValue(config)
.defaultValue(DEFAULT_CONFIG_VALUE)
.saveHandler(value -> {
config = value;
log.info("Updated config: " + config);
configContainerHelper.writeConfigInConfigDirectory(config);
})
.build()
);
Minecraft.getInstance().setScreen(configScreen);
return true;
}
}
}

View File

@@ -0,0 +1,54 @@
package de.siphalor.tweed5.coat.bridge.testmod;
import de.siphalor.tweed5.attributesextension.api.AttributesExtension;
import de.siphalor.tweed5.coat.bridge.api.TweedCoatAttributes;
import de.siphalor.tweed5.coat.bridge.api.TweedCoatBridgeExtension;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.DefaultWeavingExtensions;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeavingExtension;
import de.siphalor.tweed5.weaver.pojoext.attributes.api.Attribute;
import de.siphalor.tweed5.weaver.pojoext.attributes.api.AttributesPojoWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.AutoReadWritePojoWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.DefaultReadWriteMappings;
import de.siphalor.tweed5.weaver.pojoext.validation.api.Validator;
import de.siphalor.tweed5.weaver.pojoext.validation.api.ValidatorsPojoWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.validation.api.validators.WeavableNumberRangeValidator;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@PojoWeaving(extensions = {
ReadWriteExtension.class,
TweedCoatBridgeExtension.class,
ValidationExtension.class,
AttributesExtension.class,
})
@PojoWeavingExtension(AutoReadWritePojoWeavingProcessor.class)
@PojoWeavingExtension(ValidatorsPojoWeavingProcessor.class)
@PojoWeavingExtension(AttributesPojoWeavingProcessor.class)
@DefaultWeavingExtensions
@DefaultReadWriteMappings
@CompoundWeaving(namingFormat = "kebab_case")
@Data
public class TweedCoatBridgeTestModConfig {
private String test = "hello world";
private int someInteger = 123;
@Validator(value = WeavableNumberRangeValidator.class, config = "-10=..=10")
private int integerInRange = -5;
@Attribute(key = TweedCoatAttributes.BACKGROUND_TEXTURE, values = "textures/block/green_terracotta.png")
private Greeting serverGreeting = new Greeting("Hello server!", "Server");
@Attribute(key = TweedCoatAttributes.BACKGROUND_TEXTURE, values = "textures/block/red_terracotta.png")
private Greeting clientGreeting = new Greeting("Hello client!", "Client");
@NoArgsConstructor
@AllArgsConstructor
@CompoundWeaving
public static class Greeting {
public String greeting;
public String greeter;
}
}

View File

@@ -0,0 +1,8 @@
{
"tweed5_coat_bridge_testmod.title": "Test Mod",
"tweed5_coat_bridge_testmod.config": "Test Mod config",
"tweed5_coat_bridge_testmod.config.test": "Test entry",
"tweed5_coat_bridge_testmod.config.test.description": "Just a simple string entry",
"tweed5_coat_bridge_testmod.config.some-integer": "Some integer",
"tweed5_coat_bridge_testmod.config.integer-in-range": "Integer in range"
}

View File

@@ -0,0 +1,10 @@
{
"schemaVersion": 1,
"id": "tweed5_coat_bridge_testmod",
"version": "0.1.0",
"entrypoints": {
"client": [
"de.siphalor.tweed5.coat.bridge.testmod.TweedCoatBridgeTestMod"
]
}
}

View File

@@ -1,10 +1,12 @@
[versions] [versions]
coat = "1.0.0-beta.23" amecs-api = "1.6.2"
coat = "1.0.0-beta.24.local.3"
fabric-api = "0.136.0+1.21.10" fabric-api = "0.136.0+1.21.10"
minecraft = "1.21.10" minecraft = "1.21.10"
parchment = "2025.10.12" parchment = "2025.10.12"
[libraries] [libraries]
amecs-api = { group = "de.siphalor.amecs-api", name = "amecs-api-mc1.21.9", version.ref = "amecs-api" }
coat = { group = "de.siphalor.coat", name = "coat-mc1.21.10", version.ref = "coat" } coat = { group = "de.siphalor.coat", name = "coat-mc1.21.10", version.ref = "coat" }
fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric-api" } fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric-api" }
minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" } minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" }

View File

@@ -52,6 +52,7 @@ dependencyResolutionManagement {
includeBuild("../tweed5") includeBuild("../tweed5")
includeNormalModule("bundle") includeNormalModule("bundle")
includeNormalModule("coat-bridge")
includeNormalModule("fabric-helper") includeNormalModule("fabric-helper")
fun includeNormalModule(name: String) { fun includeNormalModule(name: String) {