Suddenly: mod compat

- Config fields default to "default" and can be either disabled or force
  enabled via the vanillin config file
- Alternatively, mods can set custom properties to disable specific
  visuals
- Emit a warning when a visual is disabled due to mod intervention
- Emit a warning when a visual is force enabled despite mod intervention
This commit is contained in:
Jozufozu 2025-01-24 11:49:59 -06:00
parent 6d2aab7716
commit 94d99af4b0
11 changed files with 413 additions and 133 deletions

View file

@ -1,82 +0,0 @@
package dev.engine_room.vanillin;
import java.util.HashMap;
import java.util.Map;
import dev.engine_room.flywheel.api.visualization.BlockEntityVisualizer;
import dev.engine_room.flywheel.api.visualization.EntityVisualizer;
import dev.engine_room.flywheel.api.visualization.VisualizerRegistry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
public class Configurator {
public final Map<BlockEntityType<?>, ConfiguredBlockEntity<?>> blockEntities = new HashMap<>();
public final Map<EntityType<?>, ConfiguredEntity<?>> entities = new HashMap<>();
public <T extends BlockEntity> void register(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
blockEntities.put(type, new ConfiguredBlockEntity<>(type, visualizer, enabledByDefault));
}
public <T extends Entity> void register(EntityType<T> type, EntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
entities.put(type, new ConfiguredEntity<>(type, visualizer, enabledByDefault));
}
public static class ConfiguredBlockEntity<T extends BlockEntity> {
public final BlockEntityType<T> type;
public final BlockEntityVisualizer<? super T> visualizer;
private final boolean enabledByDefault;
private ConfiguredBlockEntity(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
this.type = type;
this.visualizer = visualizer;
this.enabledByDefault = enabledByDefault;
}
public String configKey() {
return BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(type).toString();
}
public boolean enabledByDefault() {
return enabledByDefault;
}
public void set(boolean enabled) {
if (enabled) {
VisualizerRegistry.setVisualizer(type, visualizer);
} else {
VisualizerRegistry.setVisualizer(type, null);
}
}
}
public static class ConfiguredEntity<T extends Entity> {
public final EntityType<T> type;
public final EntityVisualizer<? super T> visualizer;
private final boolean enabledByDefault;
private ConfiguredEntity(EntityType<T> type, EntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
this.type = type;
this.visualizer = visualizer;
this.enabledByDefault = enabledByDefault;
}
public String configKey() {
return BuiltInRegistries.ENTITY_TYPE.getKey(type).toString();
}
public boolean defaultEnabled() {
return enabledByDefault;
}
public void set(boolean enabled) {
if (enabled) {
VisualizerRegistry.setVisualizer(type, visualizer);
} else {
VisualizerRegistry.setVisualizer(type, null);
}
}
}
}

View file

@ -1,11 +1,9 @@
package dev.engine_room.vanillin;
import dev.engine_room.vanillin.visuals.BellVisual;
import dev.engine_room.vanillin.visuals.ChestVisual;
import dev.engine_room.vanillin.visuals.MinecartVisual;
import dev.engine_room.vanillin.visuals.ShulkerBoxVisual;
import dev.engine_room.vanillin.visuals.SignVisual;
import dev.engine_room.vanillin.visuals.TntMinecartVisual;
import dev.engine_room.vanillin.config.BlockEntityVisualizerBuilder;
import dev.engine_room.vanillin.config.Configurator;
import dev.engine_room.vanillin.config.EntityVisualizerBuilder;
import dev.engine_room.vanillin.visuals.*;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;

View file

@ -1,4 +1,4 @@
package dev.engine_room.vanillin;
package dev.engine_room.vanillin.config;
import java.util.Objects;
import java.util.function.Predicate;

View file

@ -0,0 +1,154 @@
package dev.engine_room.vanillin.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import dev.engine_room.flywheel.api.visualization.BlockEntityVisualizer;
import dev.engine_room.flywheel.api.visualization.EntityVisualizer;
import dev.engine_room.flywheel.api.visualization.VisualizerRegistry;
import dev.engine_room.vanillin.Vanillin;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
public class Configurator {
public final Map<BlockEntityType<?>, ConfiguredBlockEntity<?>> blockEntities = new HashMap<>();
public final Map<EntityType<?>, ConfiguredEntity<?>> entities = new HashMap<>();
public <T extends BlockEntity> void register(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
blockEntities.put(type, new ConfiguredBlockEntity<>(type, visualizer, enabledByDefault));
}
public <T extends Entity> void register(EntityType<T> type, EntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
entities.put(type, new ConfiguredEntity<>(type, visualizer, enabledByDefault));
}
public static abstract class ConfiguredVisual {
private final boolean enabledByDefault;
protected ConfiguredVisual(boolean enabledByDefault) {
this.enabledByDefault = enabledByDefault;
}
public void set(VisualConfigValue configValue, @Nullable List<VisualOverride> overrides) {
if (configValue == VisualConfigValue.DISABLE) {
disable();
} else if (configValue == VisualConfigValue.FORCE_ENABLE) {
enable();
maybeWarnEnabledDespiteOverrides(overrides);
} else if (configValue == VisualConfigValue.DEFAULT) {
if (disableAndWarnDueToOverrides(overrides)) {
disable();
} else {
if (enabledByDefault) {
enable();
} else {
disable();
}
}
}
}
private boolean disableAndWarnDueToOverrides(@Nullable List<VisualOverride> overrides) {
if (overrides == null || overrides.isEmpty()) {
return false;
}
var modIds = disablingModIds(overrides);
if (modIds.isEmpty()) {
return false;
} else {
Vanillin.CONFIG_LOGGER.warn("Disabling {} visual due to overrides from mods: {}", configKey(), String.join(", ", modIds));
return true;
}
}
private void maybeWarnEnabledDespiteOverrides(@Nullable List<VisualOverride> overrides) {
if (overrides == null || overrides.isEmpty()) {
return;
}
var modIds = disablingModIds(overrides);
if (!modIds.isEmpty()) {
Vanillin.CONFIG_LOGGER.warn("Enabling {} visual despite overrides from mods: {}", configKey(), String.join(", ", modIds));
}
}
public abstract String configKey();
protected abstract void enable();
protected abstract void disable();
private static List<String> disablingModIds(List<VisualOverride> overrides) {
List<String> out = new ArrayList<>();
for (VisualOverride override : overrides) {
if (override.value() == VisualOverrideValue.DISABLE) {
out.add(override.modId());
}
}
return out;
}
}
public static class ConfiguredBlockEntity<T extends BlockEntity> extends ConfiguredVisual {
public final BlockEntityType<T> type;
public final BlockEntityVisualizer<? super T> visualizer;
private ConfiguredBlockEntity(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
super(enabledByDefault);
this.type = type;
this.visualizer = visualizer;
}
@Override
public String configKey() {
return BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(type).toString();
}
@Override
protected void enable() {
VisualizerRegistry.setVisualizer(type, visualizer);
}
@Override
protected void disable() {
VisualizerRegistry.setVisualizer(type, null);
}
}
public static class ConfiguredEntity<T extends Entity> extends ConfiguredVisual {
public final EntityType<T> type;
public final EntityVisualizer<? super T> visualizer;
private ConfiguredEntity(EntityType<T> type, EntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
super(enabledByDefault);
this.type = type;
this.visualizer = visualizer;
}
@Override
public String configKey() {
return BuiltInRegistries.ENTITY_TYPE.getKey(type).toString();
}
@Override
protected void enable() {
VisualizerRegistry.setVisualizer(type, visualizer);
}
@Override
protected void disable() {
VisualizerRegistry.setVisualizer(type, null);
}
}
}

View file

@ -1,4 +1,4 @@
package dev.engine_room.vanillin;
package dev.engine_room.vanillin.config;
import java.util.Objects;
import java.util.function.Predicate;

View file

@ -0,0 +1,15 @@
package dev.engine_room.vanillin.config;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public record ModOverrides(Map<String, List<VisualOverride>> blockEntities, Map<String, List<VisualOverride>> entities) {
public ModOverrides(List<VisualOverride> blockEntities, List<VisualOverride> entities) {
this(sort(blockEntities), sort(entities));
}
public static Map<String, List<VisualOverride>> sort(List<VisualOverride> list) {
return list.stream().collect(Collectors.groupingBy(VisualOverride::name));
}
}

View file

@ -0,0 +1,12 @@
package dev.engine_room.vanillin.config;
import com.google.gson.annotations.SerializedName;
public enum VisualConfigValue {
@SerializedName("default")
DEFAULT,
@SerializedName("disable")
DISABLE,
@SerializedName("force_enable")
FORCE_ENABLE,
}

View file

@ -0,0 +1,4 @@
package dev.engine_room.vanillin.config;
public record VisualOverride(String name, String modId, VisualOverrideValue value) {
}

View file

@ -0,0 +1,19 @@
package dev.engine_room.vanillin.config;
import org.jetbrains.annotations.Nullable;
public enum VisualOverrideValue {
DEFAULT,
DISABLE,
;
@Nullable
public static VisualOverrideValue parse(String string) {
if (string.equals("default")) {
return DEFAULT;
} else if (string.equals("disable")) {
return DISABLE;
}
return null;
}
}

View file

@ -4,14 +4,24 @@ import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import dev.engine_room.vanillin.config.Configurator;
import dev.engine_room.vanillin.config.ModOverrides;
import dev.engine_room.vanillin.config.VisualConfigValue;
import dev.engine_room.vanillin.config.VisualOverride;
import dev.engine_room.vanillin.config.VisualOverrideValue;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.metadata.CustomValue;
import net.fabricmc.loader.api.metadata.ModMetadata;
public class FabricVanillinConfig {
public static final Path PATH = FabricLoader.getInstance()
@ -21,10 +31,12 @@ public class FabricVanillinConfig {
public static final FabricVanillinConfig INSTANCE = new FabricVanillinConfig(PATH.toFile());
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static final String VANILLIN_OVERRIDES = "vanillin:overrides";
private final File file;
private Config config;
private ModOverrides overrides;
private Config config = new Config();
public FabricVanillinConfig(File file) {
this.file = file;
@ -36,23 +48,34 @@ public class FabricVanillinConfig {
config = GSON.fromJson(reader, Config.class);
} catch (Exception e) {
Vanillin.CONFIG_LOGGER.warn("Could not load config from file '{}'", file.getAbsolutePath(), e);
config = new Config();
}
}
overrides = modOverrides();
}
public void apply(Configurator configurator) {
for (Configurator.ConfiguredBlockEntity<?> configuredBlockEntity : configurator.blockEntities.values()) {
boolean enabled = config.blockEntities.computeIfAbsent(configuredBlockEntity.configKey(), $ -> configuredBlockEntity.enabledByDefault());
configuredBlockEntity.set(enabled);
var blockEntities = config.blockEntities;
var blockEntityOverrides = this.overrides.blockEntities();
for (Configurator.ConfiguredVisual configured : configurator.blockEntities.values()) {
apply(configured, blockEntities, blockEntityOverrides);
}
for (Configurator.ConfiguredEntity<?> configured : configurator.entities.values()) {
boolean enabled = config.entities.computeIfAbsent(configured.configKey(), $ -> configured.defaultEnabled());
configured.set(enabled);
var entities = config.entities;
var entityOverrides = this.overrides.entities();
for (Configurator.ConfiguredVisual configured : configurator.entities.values()) {
apply(configured, entities, entityOverrides);
}
}
private static void apply(Configurator.ConfiguredVisual configured, Map<String, VisualConfigValue> config, Map<String, List<VisualOverride>> overrides) {
var key = configured.configKey();
var enabled = config.computeIfAbsent(key, $ -> VisualConfigValue.DEFAULT);
configured.set(enabled, overrides.get(key));
}
public void save() {
try (FileWriter writer = new FileWriter(file)) {
GSON.toJson(config, writer);
@ -61,16 +84,75 @@ public class FabricVanillinConfig {
}
}
public static ModOverrides modOverrides() {
var blockEntities = new ArrayList<VisualOverride>();
var entities = new ArrayList<VisualOverride>();
for (ModContainer container : FabricLoader.getInstance().getAllMods()) {
ModMetadata meta = container.getMetadata();
var modid = meta.getId();
if (meta.containsCustomValue(VANILLIN_OVERRIDES)) {
CustomValue overridesValue = meta.getCustomValue(VANILLIN_OVERRIDES);
if (overridesValue.getType() != CustomValue.CvType.OBJECT) {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override options with an invalid value, ignoring", modid);
continue;
}
var overrides = overridesValue.getAsObject();
readSection(blockEntities, modid, overrides, "block_entities", "block entity");
readSection(entities, modid, overrides, "entities", "entity");
}
}
return new ModOverrides(blockEntities, entities);
}
private static void readSection(List<VisualOverride> dst, String modid, CustomValue.CvObject overrides, String sectionName, String singular) {
if (!overrides.containsKey(sectionName)) {
return;
}
var section = overrides.get(sectionName);
if (section.getType() != CustomValue.CvType.OBJECT) {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} with an invalid value, ignoring", modid, sectionName);
return;
}
for (Map.Entry<String, CustomValue> entry : section.getAsObject()) {
var value = entry.getValue();
var key = entry.getKey();
if (value.getType() != CustomValue.CvType.STRING) {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value, ignoring", modid, singular, key);
continue;
}
var valueString = value.getAsString();
var parsed = VisualOverrideValue.parse(valueString);
if (parsed == null) {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value '{}', ignoring", modid, singular, key, valueString);
continue;
}
dst.add(new VisualOverride(key, modid, parsed));
}
}
public static class Config {
@SerializedName("block_entities")
public Map<String, Boolean> blockEntities;
public Map<String, Boolean> entities;
public Map<String, VisualConfigValue> blockEntities;
public Map<String, VisualConfigValue> entities;
public Config() {
this(new HashMap<>(), new HashMap<>());
}
public Config(Map<String, Boolean> blockEntities, Map<String, Boolean> entities) {
public Config(Map<String, VisualConfigValue> blockEntities, Map<String, VisualConfigValue> entities) {
this.blockEntities = blockEntities;
this.entities = entities;
}

View file

@ -1,62 +1,140 @@
package dev.engine_room.vanillin;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.electronwill.nightconfig.core.Config;
import dev.engine_room.vanillin.config.Configurator;
import dev.engine_room.vanillin.config.ModOverrides;
import dev.engine_room.vanillin.config.VisualConfigValue;
import dev.engine_room.vanillin.config.VisualOverride;
import dev.engine_room.vanillin.config.VisualOverrideValue;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.forgespi.language.IModInfo;
public class ForgeVanillinConfig {
public static final ForgeVanillinConfig INSTANCE = new ForgeVanillinConfig(VanillaVisuals.CONFIGURATOR);
public final Map<String, ForgeConfigSpec.BooleanValue> blockEntities = new HashMap<>();
public final Map<String, ForgeConfigSpec.BooleanValue> entities = new HashMap<>();
private final Configurator configurator;
private final ForgeConfigSpec clientSpec;
private final ConfigSection blockEntities;
private final ConfigSection entities;
private ForgeVanillinConfig(Configurator configurator) {
this.configurator = configurator;
var builder = new ForgeConfigSpec.Builder();
builder.push("block_entities");
// Seems like we need to register all field ahead of time so this constructor must run after VanillaVisuals#init
for (var configured : configurator.blockEntities.values()) {
var name = configured.configKey();
var config = builder.define(name, configured.enabledByDefault());
blockEntities.put(name, config);
}
builder.pop();
builder.push("entities");
for (var configured : configurator.entities.values()) {
var name = configured.configKey();
var config = builder.define(name, configured.defaultEnabled());
entities.put(name, config);
}
var blockEntities = setup(builder, configurator.blockEntities.values(), "block_entities");
var entities = setup(builder, configurator.entities.values(), "entities");
clientSpec = builder.build();
var modOverrides = modOverrides();
this.blockEntities = new ConfigSection(blockEntities, modOverrides.blockEntities());
this.entities = new ConfigSection(entities, modOverrides.entities());
}
public void apply() {
for (var configured : configurator.blockEntities.values()) {
var value = blockEntities.get(configured.configKey());
if (value != null) {
configured.set(value.get());
}
}
for (var configured : configurator.entities.values()) {
var value = entities.get(configured.configKey());
if (value != null) {
configured.set(value.get());
}
}
blockEntities.apply(configurator.blockEntities.values());
entities.apply(configurator.entities.values());
}
public void registerSpecs(ModLoadingContext context) {
context.registerConfig(ModConfig.Type.CLIENT, clientSpec);
}
private static ModOverrides modOverrides() {
var blockEntities = new ArrayList<VisualOverride>();
var entities = new ArrayList<VisualOverride>();
ModList.get()
.forEachModFile(file -> {
var info = file.getModFileInfo();
for (IModInfo mod : info.getMods()) {
var modId = mod.getModId();
var modProperties = mod.getModProperties()
.get("vanillin:overrides");
if (modProperties == null) {
continue;
}
// There's no well-defined API for custom properties like in fabric.
// It just returns an object, but internally it's represented with nightconfig.
if (modProperties instanceof Config config) {
readSection(blockEntities, modId, config, "block_entities", "block entity");
readSection(entities, modId, config, "entities", "entity");
} else {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override options with an invalid value, ignoring", modId);
}
}
});
return new ModOverrides(blockEntities, entities);
}
private static void readSection(List<VisualOverride> dst, String modId, Config config, String section, String singular) {
if (!config.contains(section)) {
return;
}
var sectionObject = config.getRaw(section);
if (sectionObject instanceof Config sectionConfig) {
for (var entry : sectionConfig.entrySet()) {
var key = entry.getKey();
var value = entry.getValue();
if (value instanceof String valueString) {
var parsed = VisualOverrideValue.parse(valueString);
if (parsed != null) {
dst.add(new VisualOverride(key, modId, parsed));
} else {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value '{}', ignoring", modId, singular, key, valueString);
}
} else {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value, ignoring", modId, singular, key);
}
}
} else {
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} with an invalid value, ignoring", modId, section);
}
}
private static Map<String, ForgeConfigSpec.EnumValue<VisualConfigValue>> setup(ForgeConfigSpec.Builder builder, Collection<? extends Configurator.ConfiguredVisual> configuredVisuals, String push) {
var out = new HashMap<String, ForgeConfigSpec.EnumValue<VisualConfigValue>>();
builder.push(push);
for (var configured : configuredVisuals) {
var name = configured.configKey();
var config = builder.defineEnum(name, VisualConfigValue.DEFAULT);
out.put(name, config);
}
builder.pop();
return out;
}
private record ConfigSection(Map<String, ForgeConfigSpec.EnumValue<VisualConfigValue>> config, Map<String, List<VisualOverride>> overrides) {
void apply(Collection<? extends Configurator.ConfiguredVisual> values) {
for (var configured : values) {
var key = configured.configKey();
var value = config.get(key);
if (value != null) {
configured.set(value.get(), overrides.get(key));
}
}
}
}
}