add javadocs to api classes, build javadoc & sources jars, allow multiple mixin configs to be declared, format

This commit is contained in:
moehreag 2024-06-03 23:46:30 +02:00
parent 4aec13abc2
commit 347ad2a58a
23 changed files with 535 additions and 173 deletions

View file

@ -18,6 +18,6 @@ jobs:
- name: Build
run: |
chmod +x ./gradlew
./gradlew publishMavenJavaPublicationToFrogMCSnapshotsMavenRepository \
./gradlew :publishMavenJavaPublicationToFrogMCSnapshotsMavenRepository \
-PFrogMCSnapshotsMavenUsername=${{ secrets.MAVEN_PUSH_USER }} \
-PFrogMCSnapshotsMavenPassword=${{ secrets.MAVEN_PUSH_TOKEN }}

View file

@ -38,6 +38,13 @@ dependencies {
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withJavadocJar()
withSourcesJar()
}
tasks.javadoc {
include("**/api/**")
}
tasks.processResources {

View file

@ -10,17 +10,6 @@ credits = [
{ name = "You", roles = ["author", "other_role"] }
]
[frog.dependencies]
depends = [
{ id = "other_mod", name = "Other Mod", versions = ">=0.2.0 <0.5.2 || 0.1.1 || 1.x || 3 || ~5 || ^6.x", link = "https://example.com" }
]
breaks = [
{ id = "old_mod", versions = "*" }
]
provides = [
{ id = "provided_mod", version = "that.version.aa" }
]
[frog.extensions]
pre_launch = "dev.frogmc.frogloader.example.ExamplePreLaunchExtension"
mixin_config = "example_mod.mixins.json"

View file

@ -1,7 +1,7 @@
package dev.frogmc.frogloader.api;
import java.nio.file.Path;
import java.util.List;
import java.util.Collection;
import java.util.Optional;
import dev.frogmc.frogloader.api.env.Env;
@ -9,26 +9,92 @@ import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
/**
* General API to interact with this loader.
*
* @see ModProperties
* @see FrogPlugin
* @see Env
*/
public interface FrogLoader {
/**
* Get an instance of this loader.
*
* @return An instance of this loader
*/
static FrogLoader getInstance() {
return FrogLoaderImpl.getInstance();
}
List<FrogPlugin> getPlugins();
/**
* Get all loaded plugins.
*
* @return A collection of all loaded plugins
*/
Collection<FrogPlugin> getPlugins();
/**
* Get the current (physical) environment.
*
* @return The current environment
* @see Env
*/
Env getEnv();
/**
* Get the current game directory.
*
* @return The current game directory
* <p>Note: Should always be the current working directory, but this method should</p>
* be preferred over using the working directory directly.
*/
Path getGameDir();
/**
* Get the current config directory.
*
* @return The current config directory
*/
Path getConfigDir();
/**
* Get the current mods directory.
*
* @return The current mods directory
*/
Path getModsDir();
/**
* Query whether this loader is currently running in a development environment.
*
* @return Whether this loader is currently running in a development environment
*/
boolean isDevelopment();
/**
* Query whether a specific mod is loaded.
*
* @param id The mod id to query for
* @return Whether a mod with this specific id is loaded
*/
boolean isModLoaded(String id);
/**
* Get a mod's properties.
*
* @param id The mod id to query for
* @return An Optional containing the mod's properties, if it is present
* @see ModProperties
*/
Optional<ModProperties> getModProperties(String id);
/**
* Get all loaded mods
*
* @return A collection of all loaded mods
* @see ModProperties
* @see FrogPlugin
*/
Collection<ModProperties> getMods();
}

View file

@ -1,14 +1,40 @@
package dev.frogmc.frogloader.api.env;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
/**
* Physical environment constants
*/
@AllArgsConstructor
public enum Env {
/**
* The physical client environment
*/
CLIENT("CLIENT", "client"),
/**
* The physical (dedicated) server environment
*/
SERVER("SERVER", "server"),
;
final String mixinName, identifier;
private final String mixinName, identifier;
/**
* Get this environment's name, in the format Mixin understands.
*
* @return This environment's mixin name
*/
public String getMixinName() {
return this.mixinName;
}
/**
* Get this environment's identifier
*
* @return This environment's identifier
*/
public String getIdentifier() {
return this.identifier;
}
}

View file

@ -1,13 +1,22 @@
package dev.frogmc.frogloader.api.extensions;
import dev.frogmc.frogloader.api.mod.ModExtensions;
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
/**
* The Pre-Launch Extension.
* <p>This Extension is run right before the game is launched. (provided the used plugin supports it :) )</p>
*
* @see ModExtensions
*/
public interface PreLaunchExtension {
/**
* This extension's id. This is the key to use in your frog.mod.toml.
*/
String ID = BuiltinExtensions.PRE_LAUNCH;
/**
* The initializer. This method will be invoked when this extension is run.
*/
void onPreLaunch();
}

View file

@ -2,10 +2,36 @@ package dev.frogmc.frogloader.api.mod;
import java.util.*;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
/**
* A mod's dependencies.
*
* <p>Mod dependencies are declared in four types:</p>
* <code>Type.DEPEND</code>, <code>Type.BREAK</code>, <code>Type.SUGGEST</code> and <code>Type.PROVIDE</code>
*
* <p>Note: The <code>Type.PROVIDE</code> is a bit of a special case in the way that it only supports a single SemVer version instead of a full version range.</p>
*
* @see ModDependencies.Type
* @see ModDependencies.Entry
* @see ModProperties
* @see SemVer
*/
public final class ModDependencies {
private final Map<Type, Collection<Entry>> entries = new HashMap<>();
/**
* Construct new mod dependencies for a mod.
* <p><strong>Internal use only.</strong></p>
*
* @param depends <code>Type.DEPEND</code> entries
* @param breaks <code>Type.BREAK</code> entries
* @param suggests <code>Type.SUGGEST</code> entries
* @param provides <code>Type.PROVIDE</code> entries
*/
@ApiStatus.Internal
public ModDependencies(Collection<Entry> depends, Collection<Entry> breaks, Collection<Entry> suggests, Collection<Entry> provides) {
entries.put(Type.DEPEND, depends);
entries.put(Type.BREAK, breaks);
@ -13,51 +39,120 @@ public final class ModDependencies {
entries.put(Type.PROVIDE, provides);
}
/**
* Get dependencies of a specific mod for a specific type
*
* @param type the dependency type to query for
* @return a collection of dependency entries
*/
public Collection<Entry> getForType(Type type) {
return entries.get(type);
}
public List<ModEntry> getForModId(String id) {
/**
* Get entries that depend on a specific mod's id
*
* @param id the mod id to find dependency entries for
* @return a collection of entries that depend on the given mod id
*/
public Collection<ModEntry> getForModId(String id) {
List<ModEntry> entries = new ArrayList<>();
for (Type type : Type.values()) {
for (Entry entry : getForType(type)) {
if (entry.id.equals(id)) {
entries.add(new ModEntry(type, entry.range, entry.link));
entries.add(new ModEntry(type, entry.range, entry.link, entry.name));
}
}
}
return entries;
}
/**
* Dependency types to distinguish their variants.
*/
public enum Type {
/**
* Depend on another mod
*/
DEPEND,
/**
* Declare another mod as breaking with your own
*/
BREAK,
/**
* Suggest a user to install another mod
*/
SUGGEST,
/**
* Declare your mod to provide another mod
*/
PROVIDE
}
/**
* A data class to handle entries depending on a specific mod id
*/
public static class ModEntry {
private final Type type;
private final String range;
private final String link;
private final String name;
private ModEntry(Type type, String range, String link) {
private ModEntry(Type type, String range, String link, String name) {
this.type = type;
this.range = range;
this.link = link;
this.name = name;
}
/**
* Get this dependency's type
*
* @return This dependency's type
*/
public Type type() {
return type;
}
/**
* Get this dependency's version range
*
* @return This dependency's version range
*/
public String range() {
return range;
}
public String link(){
/**
* Get this dependency's link
* <p>May be null.</p>
*
* @return This dependency's link
*/
public @Nullable String link() {
return link;
}
/**
* Get this dependency's (friendly) name
* <p>May be null.</p>
*
* @return This dependency's name
*/
public @Nullable String name() {
return name;
}
}
/**
* General storage for dependencies
*
* @param id the mod's id
* @param range the dependency's version range
* @param link an optional link for information about this dependency
* @param name an optional (friendly) name for this dependency
*/
public record Entry(String id, String range, String link, String name) {
}
public enum Type {
DEPEND, BREAK, SUGGEST, PROVIDE
}
}

View file

@ -9,30 +9,66 @@ import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class stores a mod's extensions.
* <p>An extension is a simple key-value mapping of a string name to any value.</p>
* <p>This class further provides utility methods to easily work with extension values of various types,
* especially for extensions providing some form of class or method reference.</p>
*
* @see ModProperties
*/
public final class ModExtensions {
private static final Logger LOGGER = LoggerFactory.getLogger(ModExtensions.class);
public static ModExtensions of(Map<String, Object> entries){
return new ModExtensions(entries);
}
private final Map<String, Object> extensions;
private ModExtensions(Map<String, Object> entries) {
extensions = entries;
}
/**
* Retrieve a new instance of this class.
* <p><strong>Internal use only.</strong></p>
*
* @param entries the entries read from the mod's properties file
* @return an instance of this class
*/
public static ModExtensions of(Map<String, Object> entries) {
return new ModExtensions(entries);
}
/**
* Get the value of the provided extension name.
*
* @param key The extension name to query
* @param <T> The type of the value of this extension
* @return The value of the extension, or null
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) extensions.get(key);
}
/**
* Get the value of the provided extension name.
*
* @param key The extension name to query
* @param defaultValue a default value
* @param <T> The type of the value of this extension
* @return The value of the extension, or the default value if it isn't present
*/
@SuppressWarnings("unchecked")
public <T> T getOrDefault(String key, T defaultValue) {
return (T) extensions.getOrDefault(key, defaultValue);
}
/**
* Run the given action on this extension if it is present.
*
* @param key the extension name to query
* @param action the action to run on the value of the extension if it is present
* @param <T> The type of the value of this extension
*/
public <T> void runIfPresent(String key, Consumer<T> action) {
T value = get(key);
if (value != null) {
@ -40,6 +76,18 @@ public final class ModExtensions {
}
}
/**
* Run the given action on this extension if it is present.
* <p>This method simplifies handling references to classes or methods.</p>
* It will first query the string value of the extension and, if it is found, create a new instance of the referenced class
* or invoke the referenced method to retrieve an instance of the provided class. Then the provided action is run on this
* object.
*
* @param key The name of the extension
* @param type The class type of the extension (The class the extension class is extending/implementing)
* @param action The action to run on the newly retrieved instance of the provided class
* @param <T> The type of the class
*/
@SuppressWarnings("unchecked")
public <T> void runIfPresent(String key, Class<T> type, Consumer<T> action) {
String value = get(key);

View file

@ -5,23 +5,75 @@ import java.util.Map;
import org.jetbrains.annotations.Nullable;
/**
* A mod's properties. This class represents a mod at runtime.
* It is read from each mod's <code>frog.mod.toml</code> file.
*
* @see ModDependencies
* @see ModExtensions
* @see SemVer
*/
public interface ModProperties {
/**
* Get this mod's id.
*
* @return The mod's id
*/
String id();
/**
* Get this mod's name.
*
* @return The mod's name
*/
String name();
/**
* Get this mod's icon.
* <p>May be null if the mod doesn't specify an icon.</p>
*
* @return The mod's icon
*/
@Nullable
String icon();
/**
* Get this mod's version.
*
* @return The mod's version
*/
SemVer version();
/**
* Get this mod's license.
*
* @return The mod's license
*/
String license();
/**
* Get credits for this mod.
* <p>This is a map containing people's names as keys and their roles in this mod as values.</p>
*
* @return The mod's credits.
*/
Map<String, Collection<String>> credits();
/**
* Get this mod's dependencies.
*
* @return The mod's dependencies
* @see ModDependencies
*/
ModDependencies dependencies();
/**
* Get this mod's declared extensions.
*
* @return The mod's extensions
* @see ModExtensions
*/
ModExtensions extensions();

View file

@ -1,11 +1,46 @@
package dev.frogmc.frogloader.api.mod;
/**
* Simple access to SemVer-style versions. This is used for versioning mods.
*
* @see ModProperties
*/
public interface SemVer extends Comparable<SemVer> {
/**
* Get this version's major version component.
*
* @return This version's major version component
*/
int major();
/**
* Get this version's minor version component.
*
* @return This version's minor version component
*/
int minor();
/**
* Get this version's patch version component.
*
* @return This version's patch version component
*/
int patch();
/**
* Get this version's pre-release version component.
*
* @return This version's pre-release version component
*/
String prerelease();
/**
* Get this version's build version component.
*
* @return This version's build version component
*/
String build();
@Override
boolean equals(Object other);
}

View file

@ -6,22 +6,51 @@ import java.util.Collections;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties;
/**
* A Plugin that may load mods for a specific game and environment
*
* @see FrogLoader
*/
public interface FrogPlugin {
/**
* General run method of this plugin. This method is run after all plugins have been initialized.
*
* @see FrogPlugin#init(FrogLoader)
* @see FrogPlugin#getMods()
*/
default void run() {
}
/**
* Check whether this plugin is applicable for the current environment
*
* @return Whether this plugin is applicable to be loaded in the current environment
*/
default boolean isApplicable() {
return false;
}
/**
* Initialization method for this plugin. This method will be called after <code>isApplicable()</code>
* if it returns true to initialize is plugin.
*
* @param loader the loader loading this plugin
* @throws Exception This method may throw any exception, it will be handled by the loader.
* @see FrogPlugin#isApplicable()
* @see FrogPlugin#getMods()
*/
default void init(FrogLoader loader) throws Exception {
}
/**
* This method should return all mods loaded by this plugin. It will be queried after <code>init(FrogLoader)</code>
* and before <code>run()</code>.
*
* @return A collection of mods loaded by this plugin.
* @see FrogPlugin#init(FrogLoader)
* @see FrogPlugin#run()
*/
default Collection<ModProperties> getMods() {
return Collections.emptySet();
}

View file

@ -27,9 +27,9 @@ import org.spongepowered.asm.mixin.MixinEnvironment;
public class FrogLoaderImpl implements FrogLoader {
public static final String MOD_FILE_EXTENSION = ".frogmod";
private static final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
@Getter
private static FrogLoaderImpl instance;
private final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
@Getter
private final String[] args;
@Getter
@ -164,6 +164,7 @@ public class FrogLoaderImpl implements FrogLoader {
return mods.keySet();
}
@Override
public Collection<ModProperties> getMods() {
return mods.values();
}

View file

@ -26,43 +26,13 @@ import org.jetbrains.annotations.Nullable;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class LoaderGui {
private final JFrame frame;
private final boolean keepRunning;
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Path report;
private Consumer<JFrame> contentFunc;
private boolean keepRunning = false;
public <T> Builder setContent(ContentType<T> type, T argument){
contentFunc = f -> type.contentSetter.accept(f, argument);
return this;
}
public Builder addReport(Path report){
this.report = report;
return this;
}
public Builder keepRunning(){
keepRunning = true;
return this;
}
public LoaderGui build(){
JFrame frame = getFrame(report);
if (contentFunc != null) {
contentFunc.accept(frame);
}
return new LoaderGui(frame, keepRunning);
}
}
private final JFrame frame;
private final boolean keepRunning;
private static JFrame getFrame(Path report) {
var frame = new JFrame();
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
@ -103,6 +73,49 @@ public class LoaderGui {
return frame;
}
private static String printVersionRange(ModDependencyResolver.VersionRange range) {
return "range: \n" + range.toString().replace("||", "or");
}
private static JPanel getEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon, List<JButton> actions) {
JPanel entry = new JPanel(new BorderLayout());
entry.setBorder(BasicBorders.getInternalFrameBorder());
Box text = Box.createVerticalBox();
text.setBorder(BorderFactory.createEmptyBorder());
JTextPane desc = new JTextPane();
desc.setContentType("text/html");
desc.setEditable(false);
desc.setBackground(background);
desc.setText("<html>" + description.replace("<", "&lt;").replace("\n", "<br>") + "</html>");
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
actions.forEach(actionPanel::add);
if (icon != null) {
URL location = LoaderGui.class.getResource(icon);
if (location != null) {
entry.add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
}
}
text.add(desc);
JCheckBox showAdvanced = new JCheckBox("Show raw version range");
JTextPane advanced = new JTextPane();
advanced.setEditable(false);
advanced.setBackground(background);
advanced.setText(range.toString());
advanced.setVisible(false);
text.add(advanced);
showAdvanced.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
advanced.setVisible(showAdvanced.isSelected());
}
});
entry.add(text);
Box bottom = Box.createHorizontalBox();
bottom.add(showAdvanced, actionPanel);
entry.add(bottom, BorderLayout.SOUTH);
return entry;
}
@SuppressWarnings("BusyWait")
public void show() {
frame.setVisible(true);
@ -119,6 +132,36 @@ public class LoaderGui {
}
}
public static class Builder {
private Path report;
private Consumer<JFrame> contentFunc;
private boolean keepRunning = false;
public <T> Builder setContent(ContentType<T> type, T argument) {
contentFunc = f -> type.contentSetter.accept(f, argument);
return this;
}
public Builder addReport(Path report) {
this.report = report;
return this;
}
public Builder keepRunning() {
keepRunning = true;
return this;
}
public LoaderGui build() {
JFrame frame = getFrame(report);
if (contentFunc != null) {
contentFunc.accept(frame);
}
return new LoaderGui(frame, keepRunning);
}
}
@AllArgsConstructor
public static class ContentType<T> {
public static final ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> {
@ -235,47 +278,4 @@ public class LoaderGui {
private final BiConsumer<JFrame, T> contentSetter;
}
private static String printVersionRange(ModDependencyResolver.VersionRange range){
return "range: \n"+ range.toString().replace("||", "or");
}
private static JPanel getEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon, List<JButton> actions){
JPanel entry = new JPanel(new BorderLayout());
entry.setBorder(BasicBorders.getInternalFrameBorder());
Box text = Box.createVerticalBox();
text.setBorder(BorderFactory.createEmptyBorder());
JTextPane desc = new JTextPane();
desc.setContentType("text/html");
desc.setEditable(false);
desc.setBackground(background);
desc.setText("<html>" + description.replace("<", "&lt;").replace("\n", "<br>") + "</html>");
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
actions.forEach(actionPanel::add);
if (icon != null) {
URL location = LoaderGui.class.getResource(icon);
if (location != null) {
entry.add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
}
}
text.add(desc);
JCheckBox showAdvanced = new JCheckBox("Show raw version range");
JTextPane advanced = new JTextPane();
advanced.setEditable(false);
advanced.setBackground(background);
advanced.setText(range.toString());
advanced.setVisible(false);
text.add(advanced);
showAdvanced.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
advanced.setVisible(showAdvanced.isSelected());
}
});
entry.add(text);
Box bottom = Box.createHorizontalBox();
bottom.add(showAdvanced, actionPanel);
entry.add(bottom, BorderLayout.SOUTH);
return entry;
}
}

View file

@ -26,6 +26,10 @@ public class AccessWidener {
get().loadFromData(data);
}
public static byte[] processClass(byte[] classBytes, String className) {
return get().process(classBytes, className);
}
private void loadFromData(Data data) {
classMap.putAll(data.classMap);
methods.putAll(data.methods);
@ -34,10 +38,6 @@ public class AccessWidener {
classNames.addAll(data.classNames);
}
public static byte[] processClass(byte[] classBytes, String className) {
return get().process(classBytes, className);
}
private byte[] process(byte[] classBytes, String className) {
if (!classNames.contains(className)) {
return classBytes;
@ -97,23 +97,6 @@ public class AccessWidener {
return writer.toByteArray();
}
public record Data(Map<String, Entry> classMap,
Map<String, Map<String, Entry>> methods,
Map<String, Map<String, Entry>> fields,
Map<String, Map<String, Entry>> mutations,
Set<String> classNames) {}
public record Entry(AccessType type, String targetType, String className, String name, String descriptor) {
public Entry(String[] line) {
this(AccessType.of(line[0]), line[1], line[2], line[3], line[4]);
}
public boolean isAccessGreaterThan(Entry other) {
return Access.of(type().access).index < Access.of(other.type.access).index;
}
}
@AllArgsConstructor
public enum AccessType {
ACCESSIBLE("accessible", Opcodes.ACC_PUBLIC),
@ -146,4 +129,22 @@ public class AccessWidener {
}
}
public record Data(Map<String, Entry> classMap,
Map<String, Map<String, Entry>> methods,
Map<String, Map<String, Entry>> fields,
Map<String, Map<String, Entry>> mutations,
Set<String> classNames) {
}
public record Entry(AccessType type, String targetType, String className, String name, String descriptor) {
public Entry(String[] line) {
this(AccessType.of(line[0]), line[1], line[2], line[3], line[4]);
}
public boolean isAccessGreaterThan(Entry other) {
return Access.of(type().access).index < Access.of(other.type.access).index;
}
}
}

View file

@ -16,12 +16,13 @@ import org.spongepowered.asm.mixin.MixinEnvironment;
public class MixinClassLoader extends URLClassLoader {
private static final ClassLoader SYSTEM = ClassLoader.getSystemClassLoader();
private final List<String> exclusions = new ArrayList<>();
static {
registerAsParallelCapable();
}
private final List<String> exclusions = new ArrayList<>();
public MixinClassLoader() {
super(new URL[0], null);
excludePackage("java");

View file

@ -139,9 +139,6 @@ public class ModDependencyResolver {
return result;
}
private record ProvidedMod(String modId, SemVer version, ModProperties source) {
}
@AllArgsConstructor
private enum DependencyType {
EQ("=", Object::equals),
@ -163,6 +160,8 @@ public class ModDependencyResolver {
}
}
private record ProvidedMod(String modId, SemVer version, ModProperties source) {
}
@Getter
public static class BreakingModException extends Exception {

View file

@ -51,7 +51,7 @@ public record SemVerImpl(int major, int minor, int patch, String prerelease, Str
if (obj instanceof SemVerImpl s) {
return compareTo(s) == 0;
}
return false;
return toString().equals(obj.toString());
}
@Override

View file

@ -48,6 +48,7 @@ public class Minecraft implements FrogPlugin {
return gamePath != null;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public void init(FrogLoader loader) throws Exception {
if (gamePath == null) {
@ -98,9 +99,11 @@ public class Minecraft implements FrogPlugin {
}).forEach(FrogLoaderImpl.getInstance().getClassloader()::addURL);
modProperties.forEach(props -> {
String name = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
if (name != null) {
Object o = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
if (o instanceof String name) {
Mixins.addConfiguration(name);
} else if (o instanceof Collection l) {
((Collection<String>) l).forEach(Mixins::addConfiguration);
}
});

View file

@ -5,7 +5,8 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.*;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;