diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java new file mode 100644 index 0000000000..6c3432fc1b --- /dev/null +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/ConfigEntryPointForge.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.api.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ConfigEntryPointForge { + /** + * The mod id to associate this config entrypoint's "owner" with. + * + * @return the mod id + */ + String value(); +} diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md index ef34888ab3..efa82c4177 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md @@ -46,7 +46,11 @@ dependencies { ### Creating an Entrypoint -In order for Sodium to call your options registration code, you need to add a custom entrypoint to your mod's metadata file. It uses the key `sodium:config_api_user` and the value is the full reference to a class that implements the `net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint` interface. +Entrypoint classes that Sodium calls to run your options registration code can be declared either in your mod's metadata file, or on NeoForge with a special annotation. + +#### With a Metadata Entry + +Metadata-based entrypoints use the key `sodium:config_api_user` and the value is the full reference to a class that implements the `net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint` interface. Fabric `fabric.mod.json`: @@ -106,7 +110,7 @@ public class ExampleConfigUser implements ConfigEntryPoint { .setName(Component.literal("Example Page")) .addOptionGroup(builder.createOptionGroup() .setName(Component.literal("Example Group")) - .addOption(builder.createBooleanOption(ResourceLocation.parse("example:example_option")) + .addOption(builder.createBooleanOption(ResourceLocation.parse("examplemod:example_option")) .setName(Component.literal("Example Option")) // use translation keys here .setTooltip(Component.literal("Example tooltip")) .setStorageHandler(this.handler) @@ -119,6 +123,20 @@ public class ExampleConfigUser implements ConfigEntryPoint { } ``` +#### NeoForge: With an Annotation + +Since NeoForge has the convention of using annotations for entrypoints, this option is provided as an alternative. Any classes annotated with `@ConfigEntryPointForge("examplemod")` will be loaded as config entrypoints too. Note that the annotation must be given the mod id that should be associated as the default mod for which a config is registered with `ConfigBuilder.registerOwnModOptions`. This is necessary as it's otherwise impossible to uniquely determine which mod a class is associated with on NeoForge. + +```java +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint; +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPointForge; + +@ConfigEntryPointForge("examplemod") +public class ExampleConfigUser implements ConfigEntryPoint { + // class body identical to the above +} +``` + ### Registering Your Options Each mod adds a page for its options, within each page there are groups of options, and each group contains a list of options. Each option has an id, a name, a tooltip, a storage handler, a binding, and a default value. There are three types of options: boolean (tickbox), integer (slider), and enum. Optionally, all types of options can be disabled, while integer and enum options can have their allowed values restricted. Those two types also require you to set a function that assigns a label to each selected value. @@ -133,7 +151,7 @@ The API is largely self-explanatory and an example is provided above. Also see S ### Using `ConfigBuilder` and `ModOptions` -The `ConfigBuilder` instance passed to the registration method allows quick and easy registration of a mod's own options using `ConfigBuilder.registerOwnModOptions`. The mod's id, name, version or a formatter for the existing version, and the color theme can be configured on the returned `ModOptionsBuilder`. It's also possible to register options for additional mods using `ConfigBuilder.registerModOptions`. +The `ConfigBuilder` instance passed to the registration method allows quick and easy registration of a mod's own options using `ConfigBuilder.registerOwnModOptions`. The mod's id, name, version or a formatter for the existing version, and the color theme can be configured on the returned `ModOptionsBuilder`. It's also possible to register options for additional mods using `ConfigBuilder.registerModOptions`. Which mod is the "own" mod for `registerOwnModOptions` is determined by the mod that owns the metadata-based entrypoint or the mod id passed to the `@ConfigEntryPointForge("examplemod")` annotation. Each registered mod gets its own header in the page list. The color of the header and the corresponding entries is randomly selected from a predefined list by default, but can be customized using `ModOptionsBuilder.setColorTheme`. A color theme is created either by specifying three RGB colors or a single base color with the lighter and darker colors getting derived automatically. diff --git a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java index f653aeba49..25c975e8e2 100644 --- a/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java +++ b/common/src/api/java/net/caffeinemc/mods/sodium/api/config/structure/ConfigBuilder.java @@ -5,6 +5,8 @@ public interface ConfigBuilder { ModOptionsBuilder registerModOptions(String namespace, String name, String version); + ModOptionsBuilder registerModOptions(String namespace); + ModOptionsBuilder registerOwnModOptions(); OptionOverrideBuilder createOptionOverride(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java index 4309e7bbf6..4d87a93d90 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/ConfigManager.java @@ -18,20 +18,29 @@ import java.util.Collection; import java.util.Comparator; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.function.Supplier; public class ConfigManager { - public static final String JSON_KEY_SODIUM_CONFIG_INTEGRATIONS = "sodium:config_api_user"; + public static final String CONFIG_ENTRY_POINT_KEY = "sodium:config_api_user"; - private record ConfigUser(Supplier configEntrypoint, String modId, String modName, - String modVersion) { + private record ConfigUser( + Supplier configEntrypoint, + String modId) { + } + public record ModMetadata(String modName, String modVersion) { } private static final Collection configUsers = new ArrayList<>(); public static Config CONFIG; + private static Function modInfoFunction; + + public static void setModInfoFunction(Function modInfoFunction) { + ConfigManager.modInfoFunction = modInfoFunction; + } - public static void registerConfigEntryPoint(String className, String modId, String modName, String modVersion) { + public static void registerConfigEntryPoint(String className, String modId) { Class entryPointClass; try { entryPointClass = Class.forName(className); @@ -53,11 +62,11 @@ public static void registerConfigEntryPoint(String className, String modId, Stri SodiumClientMod.logger().warn("Mod '{}' provided a custom config integration but the class could not be constructed: {}", modId, entryPointClass); } return null; - }, modId, modName, modVersion); + }, modId); } - public static void registerConfigEntryPoint(Supplier entryPoint, String modId, String modName, String modVersion) { - configUsers.add(new ConfigUser(entryPoint, modId, modName, modVersion)); + public static void registerConfigEntryPoint(Supplier entryPoint, String modId) { + configUsers.add(new ConfigUser(entryPoint, modId)); } public static void registerConfigsEarly() { @@ -79,7 +88,7 @@ private static void registerConfigs(BiConsumer continue; } - var builder = new ConfigBuilderImpl(configUser.modId, configUser.modName, configUser.modVersion); + var builder = new ConfigBuilderImpl(modInfoFunction, configUser.modId); Collection builtConfigs; try { registerMethod.accept(entryPoint, builder); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/structure/ConfigBuilderImpl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/structure/ConfigBuilderImpl.java index 235e97caf7..5fe7fc9d06 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/config/structure/ConfigBuilderImpl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/config/structure/ConfigBuilderImpl.java @@ -1,23 +1,23 @@ package net.caffeinemc.mods.sodium.client.config.structure; import net.caffeinemc.mods.sodium.api.config.structure.*; +import net.caffeinemc.mods.sodium.client.config.ConfigManager; import net.minecraft.resources.ResourceLocation; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Function; public class ConfigBuilderImpl implements ConfigBuilder { private final List pendingModConfigBuilders = new ArrayList<>(1); + private final Function modInfoFunction; private final String defaultNamespace; - private final String defaultName; - private final String defaultVersion; - public ConfigBuilderImpl(String defaultNamespace, String defaultName, String defaultVersion) { + public ConfigBuilderImpl(Function modInfoFunction, String defaultNamespace) { + this.modInfoFunction = modInfoFunction; this.defaultNamespace = defaultNamespace; - this.defaultName = defaultName; - this.defaultVersion = defaultVersion; } public Collection build() { @@ -35,9 +35,15 @@ public ModOptionsBuilder registerModOptions(String namespace, String name, Strin return builder; } + @Override + public ModOptionsBuilder registerModOptions(String namespace) { + var metadata = this.modInfoFunction.apply(namespace); + return this.registerModOptions(namespace, metadata.modName(), metadata.modVersion()); + } + @Override public ModOptionsBuilder registerOwnModOptions() { - return this.registerModOptions(this.defaultNamespace, this.defaultName, this.defaultVersion); + return this.registerModOptions(this.defaultNamespace); } @Override diff --git a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/config/ConfigLoaderFabric.java b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/config/ConfigLoaderFabric.java index 736cf234b3..8f33190c7c 100644 --- a/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/config/ConfigLoaderFabric.java +++ b/fabric/src/main/java/net/caffeinemc/mods/sodium/fabric/config/ConfigLoaderFabric.java @@ -6,21 +6,20 @@ import net.fabricmc.loader.api.FabricLoader; public class ConfigLoaderFabric { - public static void collectConfigEntryPoints() { - var entryPointContainers = FabricLoader.getInstance().getEntrypointContainers(ConfigManager.JSON_KEY_SODIUM_CONFIG_INTEGRATIONS, ConfigEntryPoint.class); - for (var container : entryPointContainers) { - var mod = container.getProvider(); - var metadata = mod.getMetadata(); + private static ConfigManager.ModMetadata getModMetadata(String modId) { + var mod = FabricLoader.getInstance().getModContainer(modId).orElseThrow(NullPointerException::new); + var metadata = mod.getMetadata(); + return new ConfigManager.ModMetadata(metadata.getName(), metadata.getVersion().getFriendlyString()); + } - var modId = metadata.getId(); - var modName = metadata.getName(); - var modVersion = metadata.getVersion().getFriendlyString(); + public static void collectConfigEntryPoints() { + ConfigManager.setModInfoFunction(ConfigLoaderFabric::getModMetadata); - ConfigManager.registerConfigEntryPoint(container::getEntrypoint, modId, modName, modVersion); + var entryPointContainers = FabricLoader.getInstance().getEntrypointContainers(ConfigManager.CONFIG_ENTRY_POINT_KEY, ConfigEntryPoint.class); + for (var container : entryPointContainers) { + ConfigManager.registerConfigEntryPoint(container::getEntrypoint, container.getProvider().getMetadata().getId()); } - var sodiumMod = FabricLoader.getInstance().getModContainer("sodium").orElseThrow(NullPointerException::new); - var sodiumMetadata = sodiumMod.getMetadata(); - ConfigManager.registerConfigEntryPoint(SodiumConfigBuilder::new, sodiumMetadata.getId(), sodiumMetadata.getName(), sodiumMetadata.getVersion().getFriendlyString()); + ConfigManager.registerConfigEntryPoint(SodiumConfigBuilder::new, "sodium"); } } diff --git a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/config/ConfigLoaderForge.java b/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/config/ConfigLoaderForge.java index e94d6b1599..f0a4d5ba2f 100644 --- a/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/config/ConfigLoaderForge.java +++ b/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/config/ConfigLoaderForge.java @@ -1,25 +1,37 @@ package net.caffeinemc.mods.sodium.neoforge.config; +import net.caffeinemc.mods.sodium.api.config.ConfigEntryPointForge; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.config.ConfigManager; import net.caffeinemc.mods.sodium.client.gui.SodiumConfigBuilder; import net.neoforged.fml.ModList; import net.neoforged.neoforgespi.language.IModInfo; +import org.objectweb.asm.Type; + +import java.lang.annotation.ElementType; /** * Written with help from Contaria's implementation of this class. */ public class ConfigLoaderForge { + private static ConfigManager.ModMetadata getModMetadata(String modId) { + var mod = ModList.get().getModContainerById(modId).orElseThrow(() -> new + NullPointerException("Mod with id " + modId + " not found in ModList") + ).getModInfo(); + return new ConfigManager.ModMetadata(mod.getDisplayName(), mod.getVersion().toString()); + } + public static void collectConfigEntryPoints() { + ConfigManager.setModInfoFunction(ConfigLoaderForge::getModMetadata); + + // collect entry points from modes that specify it in their properties for (IModInfo mod : ModList.get().getMods()) { var modId = mod.getModId(); - var modName = mod.getDisplayName(); - var modVersion = mod.getVersion().toString(); if (modId.equals("sodium")) { - ConfigManager.registerConfigEntryPoint(SodiumConfigBuilder::new, modId, modName, modVersion); + ConfigManager.registerConfigEntryPoint(SodiumConfigBuilder::new, modId); } else { - Object modProperty = mod.getModProperties().get(ConfigManager.JSON_KEY_SODIUM_CONFIG_INTEGRATIONS); + Object modProperty = mod.getModProperties().get(ConfigManager.CONFIG_ENTRY_POINT_KEY); if (modProperty == null) { continue; } @@ -29,7 +41,30 @@ public static void collectConfigEntryPoints() { continue; } - ConfigManager.registerConfigEntryPoint((String) modProperty, modId, modName, modVersion); + ConfigManager.registerConfigEntryPoint((String) modProperty, modId); + } + } + + // collect entry points from mods that specify it as an annotation + var entryPointAnnotationType = Type.getType(ConfigEntryPointForge.class); + for (var scanData : ModList.get().getAllScanData()) { + for (var annotation : scanData.getAnnotations()) { + if (annotation.targetType() == ElementType.TYPE && annotation.annotationType().equals(entryPointAnnotationType)) { + var className = annotation.clazz().getClassName(); + var modIdData = annotation.annotationData().get("value"); + if (modIdData == null) { + SodiumClientMod.logger().warn("Class '{}' has a sodium config api entry point annotation but didn't specify which mod it belongs to with the annotation's default parameter.", className); + continue; + } + + var modId = modIdData.toString(); + if (ModList.get().getModContainerById(modId).isEmpty()) { + SodiumClientMod.logger().warn("The mod with id '{}' that was provided as the owner of a sodium config api entry point annotation on class '{}' doesn't exist.", modId, className); + continue; + } + + ConfigManager.registerConfigEntryPoint(className, modId); + } } } }