Skip to content

Commit

Permalink
add annotation-based entrypoint loading on neoforge, refactored how m…
Browse files Browse the repository at this point in the history
…od metadata is retrieved, updated documentation
  • Loading branch information
douira committed Nov 3, 2024
1 parent fe70c25 commit a3a5b84
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -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();
}
24 changes: 21 additions & 3 deletions common/src/api/java/net/caffeinemc/mods/sodium/api/config/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
public interface ConfigBuilder {
ModOptionsBuilder registerModOptions(String namespace, String name, String version);

ModOptionsBuilder registerModOptions(String namespace);

ModOptionsBuilder registerOwnModOptions();

OptionOverrideBuilder createOptionOverride();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> configEntrypoint, String modId, String modName,
String modVersion) {
private record ConfigUser(
Supplier<ConfigEntryPoint> configEntrypoint,
String modId) {
}
public record ModMetadata(String modName, String modVersion) {
}

private static final Collection<ConfigUser> configUsers = new ArrayList<>();

public static Config CONFIG;
private static Function<String, ModMetadata> modInfoFunction;

public static void setModInfoFunction(Function<String, ModMetadata> 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);
Expand All @@ -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<ConfigEntryPoint> entryPoint, String modId, String modName, String modVersion) {
configUsers.add(new ConfigUser(entryPoint, modId, modName, modVersion));
public static void registerConfigEntryPoint(Supplier<ConfigEntryPoint> entryPoint, String modId) {
configUsers.add(new ConfigUser(entryPoint, modId));
}

public static void registerConfigsEarly() {
Expand All @@ -79,7 +88,7 @@ private static void registerConfigs(BiConsumer<ConfigEntryPoint, ConfigBuilder>
continue;
}

var builder = new ConfigBuilderImpl(configUser.modId, configUser.modName, configUser.modVersion);
var builder = new ConfigBuilderImpl(modInfoFunction, configUser.modId);
Collection<ModOptions> builtConfigs;
try {
registerMethod.accept(entryPoint, builder);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ModOptionsBuilderImpl> pendingModConfigBuilders = new ArrayList<>(1);

private final Function<String, ConfigManager.ModMetadata> modInfoFunction;
private final String defaultNamespace;
private final String defaultName;
private final String defaultVersion;

public ConfigBuilderImpl(String defaultNamespace, String defaultName, String defaultVersion) {
public ConfigBuilderImpl(Function<String, ConfigManager.ModMetadata> modInfoFunction, String defaultNamespace) {
this.modInfoFunction = modInfoFunction;
this.defaultNamespace = defaultNamespace;
this.defaultName = defaultName;
this.defaultVersion = defaultVersion;
}

public Collection<ModOptions> build() {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="https://github.com/KingContaria/sodium-fabric/blob/de61e59a369dd8906ddb54050f48c02a29e3f217/neoforge/src/main/java/net/caffeinemc/mods/sodium/neoforge/gui/SodiumConfigIntegrationAPIForge.java">Contaria's implementation of this class</a>.
*/
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;
}
Expand All @@ -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);
}
}
}
}
Expand Down

0 comments on commit a3a5b84

Please sign in to comment.