Merge branch 'config-dsl' into 'master'

Config DSL

See merge request jfmods/LibJF!3
This commit is contained in:
Johannes Frohnmeyer 2022-08-27 15:54:18 +00:00
commit af19515d0f
145 changed files with 3761 additions and 1002 deletions

View File

@ -1,6 +1,8 @@
apply from: "https://jfmods.gitlab.io/scripts/jfmod.gradle"
allprojects {
if (project.name in rootProject.nonModSubprojects) return
loom {
runs {
testmodClient {
@ -19,7 +21,9 @@ allprojects {
}
dependencies {
testmodRuntimeOnly("com.terraformersmc:modmenu:4.0.5")
testmodRuntimeOnly("net.fabricmc.fabric-api:fabric-api:$project.fabric_version")
modLocalRuntime("com.terraformersmc:modmenu:4.0.5")
modLocalRuntime(fabricApi.module("fabric-command-api-v2", "$project.fabric_version"))
compileOnly("io.gitlab.jfronny:commons:$rootProject.commons_version")
compileOnly("io.gitlab.jfronny:commons-gson:$rootProject.commons_version")
}
}

View File

@ -20,7 +20,7 @@ repositories {
and include LibJF modules like this:
```groovy
dependencies {
include modImplementation("io.gitlab.jfronny.libjf:libjf-config-v0:${project.jfapi_version}")
include modImplementation("io.gitlab.jfronny.libjf:libjf-config-v1:${project.jfapi_version}")
include("io.gitlab.jfronny.libjf:libjf-unsafe-v0:${project.jfapi_version}")
include("io.gitlab.jfronny.libjf:libjf-base:${project.jfapi_version}")
modRuntimeOnly("io.gitlab.jfronny.libjf:libjf-devutil-v0:${project.jfapi_version}")

View File

@ -1,7 +1,7 @@
# libjf-web-v0
libjf-web-v0 provides an HTTP web server you can use in your serverside (and technically also clientside) mods
to serve web content through a unified port.
libjf-web-v0 depends on libjf-config-v0 to provide its config, libjf-base, fabric-lifecycle-events-v1 and fabric-command-api-v1
libjf-web-v0 depends on libjf-config-v1 to provide its config, libjf-base, fabric-lifecycle-events-v1 and fabric-command-api-v1
### Getting started
Implement WebInit and register it as a libjf:web entrypoint. To enable the server, also add the following to your fabric.mod.json:

View File

@ -2,15 +2,18 @@
minecraft_version=1.19.2
yarn_mappings=build.8
loader_version=0.14.9
fabric_version=0.60.0+1.19.2
maven_group=io.gitlab.jfronny.libjf
archive_base_name=libjf
dev_only_module=libjf-devutil-v0
modrinth_id=WKwQAwke
modrinth_optional_dependencies=P7dR8mSH
dev_only_module=libjf-devutil-v0
non_mod_project=libjf-config-compiler-plugin
modrinth_id=libjf
modrinth_optional_dependencies=fabric-api
curseforge_id=482600
curseforge_optional_dependencies=fabric-api
fabric_version=0.60.0+1.19.2
commons_version=2022.7.4+11-13-3
bytebuddy_version=1.12.13

View File

@ -3,13 +3,13 @@ package io.gitlab.jfronny.libjf;
import io.gitlab.jfronny.commons.log.Logger;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.gson.GsonBuilder;
import io.gitlab.jfronny.libjf.coprocess.CoProcessManager;
import io.gitlab.jfronny.libjf.gson.GsonAdapter;;
import io.gitlab.jfronny.libjf.gson.HiddenAnnotationExclusionStrategy;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
public class LibJf {
private LibJf() {
}
public class LibJf implements ModInitializer {
public static final String MOD_ID = "libjf";
public static final Logger LOGGER = Logger.forName(MOD_ID);
@ -23,4 +23,9 @@ public class LibJf {
HiddenAnnotationExclusionStrategy.register();
GsonHolder.register();
}
@Override
public void onInitialize() {
Logger.resetFactory();
}
}

View File

@ -18,7 +18,10 @@
"fabric-lifecycle-events-v1": "*"
},
"entrypoints": {
"main": ["io.gitlab.jfronny.libjf.coprocess.CoProcessManager"]
"main": [
"io.gitlab.jfronny.libjf.LibJf",
"io.gitlab.jfronny.libjf.coprocess.CoProcessManager"
]
},
"custom": {
"modmenu": {

View File

@ -0,0 +1,7 @@
archivesBaseName = "libjf-config-reflect-v0"
dependencies {
api project(path: ":libjf-base", configuration: "dev")
api project(path: ":libjf-config-core-v1", configuration: "dev")
include modImplementation(fabricApi.module("fabric-command-api-v2", "${project.fabric_version}"))
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.impl;
package io.gitlab.jfronny.libjf.config.impl.commands;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.*;
@ -8,13 +8,12 @@ import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable;
import io.gitlab.jfronny.commons.throwable.ThrowingSupplier;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import io.gitlab.jfronny.libjf.config.api.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.*;
import net.minecraft.text.Text;
import java.util.function.Consumer;
import java.util.function.Function;
@ -30,23 +29,28 @@ public class JfConfigCommand implements ModInitializer {
LiteralArgumentBuilder<ServerCommandSource> c_config = literal("config")
.requires((serverCommandSource) -> serverCommandSource.hasPermissionLevel(4))
.executes(context -> {
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] Loaded configs for:"), false);
ConfigHolder.getInstance().getRegistered().forEach((s, config) -> {
context.getSource().sendFeedback(Text.literal("- " + s), false);
});
return Command.SINGLE_SUCCESS;
});
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] Loaded configs for:"), false);
ConfigHolder.getInstance().getRegistered().forEach((s, config) -> {
context.getSource().sendFeedback(Text.literal("- " + s), false);
});
return Command.SINGLE_SUCCESS;
});
LiteralArgumentBuilder<ServerCommandSource> c_reload = literal("reload").executes(context -> {
ConfigHolder.getInstance().getRegistered().forEach((mod, config) -> config.load());
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] Reloaded configs"), true);
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] Reloaded configs"), true);
return Command.SINGLE_SUCCESS;
});
LiteralArgumentBuilder<ServerCommandSource> c_reset = literal("reset").executes(context -> {
context.getSource().sendError(Text.literal("[libjf-config-v0] Please specify a config to reset"));
context.getSource().sendError(Text.literal("[libjf-config-v1] Please specify a config to reset"));
return Command.SINGLE_SUCCESS;
});
ConfigHolder.getInstance().getRegistered().forEach((id, config) -> {
registerEntries(config, id, c_config, c_reload, c_reset, cns -> {
c_reload.then(literal(id).executes(context -> {
config.load();
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] Reloaded config for " + id), true);
return Command.SINGLE_SUCCESS;
}));
registerEntries(config, id, c_config, c_reset, cns -> {
LiteralArgumentBuilder<ServerCommandSource> c_instance = literal(id);
cns.accept(c_instance);
return c_instance;
@ -56,37 +60,32 @@ public class JfConfigCommand implements ModInitializer {
});
}
private void registerEntries(ConfigInstance config, String subpath, LiteralArgumentBuilder<ServerCommandSource> c_config, LiteralArgumentBuilder<ServerCommandSource> c_reload, LiteralArgumentBuilder<ServerCommandSource> c_reset, Function<Consumer<LiteralArgumentBuilder<ServerCommandSource>>, LiteralArgumentBuilder<ServerCommandSource>> pathGen) {
private void registerEntries(ConfigCategory config, String subpath, LiteralArgumentBuilder<ServerCommandSource> c_config, LiteralArgumentBuilder<ServerCommandSource> c_reset, Function<Consumer<LiteralArgumentBuilder<ServerCommandSource>>, LiteralArgumentBuilder<ServerCommandSource>> pathGen) {
c_config.then(pathGen.apply(cns -> {
cns.executes(context -> {
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] " + subpath + " is a category"), false);
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] " + subpath + " is a category"), false);
return Command.SINGLE_SUCCESS;
});
for (EntryInfo<?> entry : config.getEntries()) {
registerEntry(config, subpath, cns, entry);
}
}));
c_reload.then(pathGen.apply(cns -> cns.executes(context -> {
config.load();
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] Reloaded config for " + subpath), true);
return Command.SINGLE_SUCCESS;
})));
c_reset.then(pathGen.apply(cns -> {
cns.executes(context -> {
config.getPresets().get(ConfigInstanceAbstract.CONFIG_PRESET_DEFAULT).run();
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] Reset config for " + subpath), true);
config.reset();
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] Reset config for " + subpath), true);
return Command.SINGLE_SUCCESS;
});
config.getPresets().forEach((id2, preset) -> {
cns.then(literal(id2).executes(context -> {
preset.run();
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] Loaded preset " + id2 + " for " + subpath), true);
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] Loaded preset " + id2 + " for " + subpath), true);
return Command.SINGLE_SUCCESS;
}));
});
}));
config.getCategories().forEach((id2, cfg) -> {
registerEntries(cfg, cfg.getCategoryPath(), c_config, c_reload, c_reset, cns -> {
registerEntries(cfg, cfg.getCategoryPath(), c_config, c_reset, cns -> {
return pathGen.apply(cns1 -> {
LiteralArgumentBuilder<ServerCommandSource> c_instance2 = literal(id2);
cns.accept(c_instance2);
@ -102,25 +101,25 @@ public class JfConfigCommand implements ModInitializer {
} else return Text.literal("Could not execute command");
});
private <T> void registerEntry(ConfigInstance config, String subpath, LiteralArgumentBuilder<ServerCommandSource> cns, EntryInfo<T> entry) {
private <T> void registerEntry(ConfigCategory config, String subpath, LiteralArgumentBuilder<ServerCommandSource> cns, EntryInfo<T> entry) {
LiteralArgumentBuilder<ServerCommandSource> c_entry = literal(entry.getName()).executes(context -> {
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] The value of " + subpath + "." + entry.getName() + " is " + tryRun(entry::getValue)), false);
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] The value of " + subpath + "." + entry.getName() + " is " + tryRun(entry::getValue)), false);
return Command.SINGLE_SUCCESS;
});
ArgumentType<?> type = getType(entry);
if (type != null) {
c_entry.then(argument("value", type).executes(context -> {
T value = context.getArgument("value", entry.getValueType());
@SuppressWarnings("unchecked") T value = context.getArgument("value", (Class<T>) entry.getValueType().asClass());
tryRun(() -> entry.setValue(value));
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] Set " + subpath + "." + entry.getName() + " to " + value), true);
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] Set " + subpath + "." + entry.getName() + " to " + value), true);
return Command.SINGLE_SUCCESS;
}));
}
else if (entry.getValueType().isEnum()) {
for (T enumConstant : entry.getValueType().getEnumConstants()) {
for (T enumConstant : ((Type.TEnum<T>)entry.getValueType()).options()) {
c_entry.then(literal(enumConstant.toString()).executes(context -> {
tryRun(() -> entry.setValue(enumConstant));
context.getSource().sendFeedback(Text.literal("[libjf-config-v0] Set " + subpath + "." + entry.getName() + " to " + enumConstant), true);
context.getSource().sendFeedback(Text.literal("[libjf-config-v1] Set " + subpath + "." + entry.getName() + " to " + enumConstant), true);
return Command.SINGLE_SUCCESS;
}));
}
@ -129,12 +128,13 @@ public class JfConfigCommand implements ModInitializer {
}
private <T> ArgumentType<?> getType(EntryInfo<T> info) {
Class<T> type = info.getValueType();
if (type == int.class || type == Integer.class) return IntegerArgumentType.integer((int) info.getMinValue(), (int) info.getMaxValue());
else if (type == float.class || type == Float.class) return FloatArgumentType.floatArg((float) info.getMinValue(), (float) info.getMaxValue());
else if (type == double.class || type == Double.class) return DoubleArgumentType.doubleArg(info.getMinValue(), info.getMaxValue());
else if (type == String.class) return StringArgumentType.greedyString();
else if (type == boolean.class || type == Boolean.class) return BoolArgumentType.bool();
Type<T> type = info.getValueType();
if (type.isInt()) return IntegerArgumentType.integer((int) info.getMinValue(), (int) info.getMaxValue());
else if (type.isLong()) return LongArgumentType.longArg((long) info.getMinValue(), (long) info.getMaxValue());
else if (type.isFloat()) return FloatArgumentType.floatArg((float) info.getMinValue(), (float) info.getMaxValue());
else if (type.isDouble()) return DoubleArgumentType.doubleArg(info.getMinValue(), info.getMaxValue());
else if (type.isString()) return StringArgumentType.greedyString();
else if (type.isBool()) return BoolArgumentType.bool();
else return null;
}

View File

@ -0,0 +1,29 @@
{
"schemaVersion": 1,
"id": "libjf-config-commands-v1",
"name": "LibJF Config Commands",
"version": "${version}",
"authors": ["JFronny"],
"contact": {
"website": "https://jfronny.gitlab.io",
"repo": "https://gitlab.com/jfmods/libjf"
},
"license": "MIT",
"environment": "server",
"entrypoints": {
"main": ["io.gitlab.jfronny.libjf.config.impl.commands.JfConfigCommand"]
},
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*",
"fabric-command-api-v2": "*",
"libjf-base": ">=${version}",
"libjf-config-core-v1": ">=${version}"
},
"custom": {
"modmenu": {
"badges": ["library"],
"parent": "libjf"
}
}
}

View File

@ -0,0 +1 @@
src/main/java/io/gitlab/jfronny/libjf/config/plugin/BuildMetadata.java

View File

@ -0,0 +1,68 @@
plugins {
id 'java-gradle-plugin'
id 'maven-publish'
}
group project.maven_group
version rootProject.ext.currentVer
repositories {
mavenCentral()
maven {
name = 'JF Commons'
url = 'https://gitlab.com/api/v4/projects/35745143/packages/maven'
}
}
dependencies {
compileOnly(gradleApi())
implementation("org.apache.ant:ant:1.10.12")
implementation("io.gitlab.jfronny:commons-gson:$rootProject.commons_version")
implementation("org.ow2.asm:asm:9.3")
implementation("org.ow2.asm:asm-commons:9.3")
implementation("org.ow2.asm:asm-util:9.3")
implementation(project(":libjf-config-core-v1")) {
transitive(false)
}
}
gradlePlugin {
plugins {
simplePlugin {
id = "io.gitlab.jfronny.libjf.libjf-config-compiler-plugin"
implementationClass = "io.gitlab.jfronny.libjf.config.plugin.ConfigPlugin"
}
}
}
publishing {
repositories {
mavenLocal()
if (project.hasProperty("maven")) {
maven {
url = project.getProperty("maven")
name = "dynamic"
credentials(HttpHeaderCredentials) {
name = "Job-Token"
value = System.getenv().CI_JOB_TOKEN
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
}
}
tasks.publish.dependsOn(tasks.build)
rootProject.tasks.deployDebug.dependsOn(tasks.publish)
java.nio.file.Files.writeString(java.nio.file.Path.of("$projectDir/src/main/java/io/gitlab/jfronny/libjf/config/plugin/BuildMetadata.java"), """
package io.gitlab.jfronny.libjf.config.plugin;
public class BuildMetadata {
public static final boolean isRelease = ${project.hasProperty("release")};
}
""")

View File

@ -0,0 +1,54 @@
package io.gitlab.jfronny.libjf.config.plugin;
import io.gitlab.jfronny.libjf.config.plugin.util.ZipCompressor;
import org.apache.tools.zip.ZipOutputStream;
import org.gradle.api.GradleException;
import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.tasks.WorkResult;
import org.gradle.api.tasks.WorkResults;
import org.objectweb.asm.Type;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
public class ConfigInjectCopyAction implements CopyAction {
private final File zipFile;
private final ZipCompressor compressor;
private final String encoding;
private final boolean preserveFileTimestamps;
private final String modId;
private final Set<Type> knownConfigClasses = new HashSet<>();
public ConfigInjectCopyAction(File zipFile,
ZipCompressor compressor,
String encoding,
boolean preserveFileTimestamps,
String modId) {
this.zipFile = zipFile;
this.compressor = compressor;
this.encoding = encoding;
this.preserveFileTimestamps = preserveFileTimestamps;
this.modId = modId;
}
@Override
public WorkResult execute(CopyActionProcessingStream stream) {
try (ZipOutputStream zipOutStr = compressor.createArchiveOutputStream(zipFile)) {
if (encoding != null) {
zipOutStr.setEncoding(encoding);
}
AtomicBoolean fmjFound = new AtomicBoolean(false);
stream.process(details -> fmjFound.compareAndSet(false,
new StreamAction(zipOutStr, zipFile, preserveFileTimestamps, modId, knownConfigClasses)
.processFile(details)
));
} catch (IOException e) {
throw new GradleException("Could not create ZIP " + zipFile, e);
}
return WorkResults.didWork(true);
}
}

View File

@ -0,0 +1,38 @@
package io.gitlab.jfronny.libjf.config.plugin;
import io.gitlab.jfronny.libjf.config.plugin.util.GradleVersionUtil;
import io.gitlab.jfronny.libjf.config.plugin.util.ZipCompressor;
import org.gradle.api.file.DuplicatesStrategy;
import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.bundling.Jar;
public abstract class ConfigInjectTask extends Jar {
@Input
public abstract Property<String> getModId();
private final GradleVersionUtil versionUtil;
public ConfigInjectTask() {
super();
setDuplicatesStrategy(DuplicatesStrategy.FAIL);
versionUtil = new GradleVersionUtil(getProject().getGradle().getGradleVersion());
}
@Override
protected CopyAction createCopyAction() {
return new ConfigInjectCopyAction(
getArchiveFile().get().getAsFile(),
getInternalCompressor(),
this.getMetadataCharset(),
isPreserveFileTimestamps(),
getModId().get()
);
}
@Internal
protected ZipCompressor getInternalCompressor() {
return versionUtil.getInternalCompressor(getEntryCompression(), this);
}
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.libjf.config.plugin;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class ConfigPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
}
}

View File

@ -0,0 +1,189 @@
package io.gitlab.jfronny.libjf.config.plugin;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.libjf.config.plugin.asm.ConfigInjectClassTransformer;
import io.gitlab.jfronny.libjf.config.plugin.fmj.FabricModJsonTransformer;
import io.gitlab.jfronny.libjf.config.plugin.util.*;
import org.apache.tools.zip.*;
import org.gradle.api.GradleException;
import org.gradle.api.file.FileCopyDetails;
import org.objectweb.asm.*;
import org.objectweb.asm.util.CheckClassAdapter;
import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class StreamAction {
private static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = (new GregorianCalendar(1980, 1, 1, 0, 0, 0)).getTimeInMillis();
private final ZipOutputStream zipOutStr;
private final File zipFile;
private final boolean preserveFileTimestamps;
private final String modId;
private final Set<Type> knownConfigClasses;
public StreamAction(ZipOutputStream zipOutStr, File zipFile, boolean preserveFileTimestamps, String modId, Set<Type> knownConfigClasses) {
this.zipOutStr = zipOutStr;
this.zipFile = zipFile;
this.preserveFileTimestamps = preserveFileTimestamps;
this.modId = modId;
this.knownConfigClasses = knownConfigClasses;
}
public boolean processFile(FileCopyDetails details) {
try {
if (details.isDirectory()) {
visitDirectory(details);
return false;
} else {
return visitFile(details);
}
} catch (Exception e) {
throw new GradleException("Could not add " + details + " to ZIP " + zipFile, e);
}
}
private void visitDirectory(FileCopyDetails details) throws IOException {
String path = details.getRelativePath().getPathString() + "/";
ZipEntry archiveEntry = new ZipEntry(path);
archiveEntry.setTime(getArchiveTimeFor(details.getLastModified()));
archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
zipOutStr.putNextEntry(archiveEntry);
zipOutStr.closeEntry();
}
private boolean visitFile(FileCopyDetails details) throws IOException {
if (details.getPath().endsWith(".jar")) {
return processArchive(details);
} else if (details.getPath().endsWith(".class")) {
processClass(details);
} else if (details.getPath().equals("fabric.mod.json")) {
processFMJ(details);
return true;
} else {
ZipEntry archiveEntry = new ZipEntry(details.getRelativePath().getPathString());
archiveEntry.setTime(getArchiveTimeFor(details.getLastModified()));
archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
zipOutStr.putNextEntry(archiveEntry);
details.copyTo(zipOutStr);
zipOutStr.closeEntry();
}
return false;
}
private boolean processArchive(FileCopyDetails details) throws IOException {
try (ZipFile archive = new ZipFile(details.getFile())) {
AtomicBoolean fmjFound = new AtomicBoolean(false);
EnumerationSpliterator.stream(archive.getEntries())
.map(RelativeArchivePath::new)
.map(ArchiveFileTreeElement::new)
.filter(it -> it.getRelativePath().isFile())
.forEach(archiveElement -> {
fmjFound.compareAndSet(false, visitArchiveFile(archiveElement, archive));
});
return fmjFound.get();
}
}
private void visitArchiveDirectory(RelativeArchivePath archiveDir) throws IOException {
zipOutStr.putNextEntry(archiveDir.entry);
zipOutStr.closeEntry();
}
private boolean visitArchiveFile(ArchiveFileTreeElement archiveFile, ZipFile archive) {
RelativeArchivePath archiveFilePath = archiveFile.getRelativePath();
try {
if (archiveFile.isClassFile()) {
processClass(archiveFilePath, archive);
} else if (archiveFilePath.getPathString().equals("fabric.mod.json")) {
processFMJ(archiveFilePath, archive);
return true;
} else {
copyArchiveEntry(archiveFilePath, archive);
}
return false;
} catch (IOException e) {
throw new GradleException("Could not read archive entry " + archiveFilePath.getPathString(), e);
}
}
private void copyArchiveEntry(RelativeArchivePath archiveFile, ZipFile archive) throws IOException {
ZipEntry entry = new ZipEntry(archiveFile.entry.getName());
entry.setTime(getArchiveTimeFor(archiveFile.entry.getTime()));
RelativeArchivePath path = new RelativeArchivePath(entry);
addParentDirectories(path);
zipOutStr.putNextEntry(path.entry);
try (InputStream is = archive.getInputStream(archiveFile.entry)) {
byte[] buffer = new byte[1024];
int n;
while (-1 != (n = is.read(buffer))) {
zipOutStr.write(buffer, 0, n);
}
}
zipOutStr.closeEntry();
}
private void addParentDirectories(RelativeArchivePath file) throws IOException {
if (file != null) {
addParentDirectories(file.getParent());
if (!file.isFile()) {
visitArchiveDirectory(file);
}
}
}
private void processClass(RelativeArchivePath file, ZipFile archive) throws IOException {
ZipEntry zipEntry = new ZipEntry(file.getPathString());
addParentDirectories(new RelativeArchivePath(zipEntry));
processClass(archive.getInputStream(file.entry), file.getPathString(), file.entry.getTime());
}
private void processClass(FileCopyDetails details) throws IOException {
try (InputStream is = new BufferedInputStream(new FileInputStream(details.getFile()))) {
processClass(is, details.getPath(), details.getLastModified());
}
}
private void processClass(InputStream classInputStream, String path, long lastModified) throws IOException {
final ClassReader reader = new ClassReader(classInputStream);
final ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
reader.accept(new CheckClassAdapter(
new ConfigInjectClassTransformer(modId, writer, knownConfigClasses)
), ClassReader.EXPAND_FRAMES);
ZipEntry archiveEntry = new ZipEntry(path);
archiveEntry.setTime(getArchiveTimeFor(lastModified));
zipOutStr.putNextEntry(archiveEntry);
zipOutStr.write(writer.toByteArray());
zipOutStr.closeEntry();
}
private void processFMJ(RelativeArchivePath file, ZipFile archive) throws IOException {
ZipEntry zipEntry = new ZipEntry(file.getPathString());
addParentDirectories(new RelativeArchivePath(zipEntry));
processFMJ(archive.getInputStream(file.entry), file.getPathString(), file.entry.getTime());
}
private void processFMJ(FileCopyDetails details) throws IOException {
try (InputStream is = new BufferedInputStream(new FileInputStream(details.getFile()))) {
processFMJ(is, details.getPath(), details.getLastModified());
}
}
private void processFMJ(InputStream fmj, String path, long lastModified) throws IOException {
ZipEntry archiveEntry = new ZipEntry(path);
archiveEntry.setTime(getArchiveTimeFor(lastModified));
zipOutStr.putNextEntry(archiveEntry);
// Leave this closeable open, as everything else will break the writer
try (JsonReader reader = new JsonReader(new InputStreamReader(fmj));
JsonWriter writer = new JsonWriter(new OutputStreamWriter(new DelegatingUncloseableOutputStream(zipOutStr)))) {
FabricModJsonTransformer.transform(reader, writer, knownConfigClasses);
}
zipOutStr.closeEntry();
}
private long getArchiveTimeFor(long timestamp) {
return preserveFileTimestamps ? timestamp : CONSTANT_TIME_FOR_ZIP_ENTRIES;
}
}

View File

@ -0,0 +1,40 @@
package io.gitlab.jfronny.libjf.config.plugin.asm;
import org.gradle.api.GradleException;
import org.objectweb.asm.AnnotationVisitor;
import java.util.List;
import static org.objectweb.asm.Opcodes.*;
public class AnnotationMetaGatheringVisitor extends AnnotationVisitor {
private final List<String> referencedConfigs;
public AnnotationMetaGatheringVisitor(AnnotationVisitor annotationVisitor, List<String> referencedConfigs) {
super(ASM9, annotationVisitor);
this.referencedConfigs = referencedConfigs;
}
@Override
public AnnotationVisitor visitArray(String name) {
return switch (name) {
case "referencedConfigs" -> new ArrayVisitor<>(super.visitArray(name), referencedConfigs);
default -> throw new GradleException("Unknown field in category or JfConfig annotation: " + name);
};
}
private static class ArrayVisitor<T> extends AnnotationVisitor {
private final List<T> target;
protected ArrayVisitor(AnnotationVisitor annotationVisitor, List<T> target) {
super(ASM9, annotationVisitor);
this.target = target;
}
@Override
public void visit(String name, Object value) {
super.visit(name, value);
target.add((T) value);
}
}
}

View File

@ -0,0 +1,212 @@
package io.gitlab.jfronny.libjf.config.plugin.asm;
import io.gitlab.jfronny.libjf.config.api.v1.Category;
import io.gitlab.jfronny.libjf.config.api.v1.JfConfig;
import io.gitlab.jfronny.libjf.config.plugin.BuildMetadata;
import io.gitlab.jfronny.libjf.config.plugin.util.ClInitInjectVisitor;
import io.gitlab.jfronny.libjf.config.plugin.util.GeneratorAdapter2;
import io.gitlab.jfronny.libjf.config.plugin.asm.value.DiscoveredValue;
import org.gradle.api.GradleException;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import java.util.*;
import static io.gitlab.jfronny.libjf.config.plugin.asm.value.KnownTypes.*;
import static org.objectweb.asm.Opcodes.*;
import static org.objectweb.asm.Type.*;
public class ConfigInjectClassTransformer extends ClassVisitor {
// Field names
public static final String PREFIX = "libjf$config$";
public static final String INIT_METHOD = PREFIX + "clinit";
public static final String REGISTER_METHOD = PREFIX + "register";
public static final String BUILDER_ROOT = PREFIX + "root";
public static final String CLINIT = "<clinit>";
// Transformation metadata
public final String modId;
public Type current;
private final Set<Type> knownConfigClasses;
// Transformer state
private final List<String> categories = new LinkedList<>();
private final List<String> referencedConfigs = new LinkedList<>();
private final List<String> presets = new LinkedList<>();
private final List<String> verifiers = new LinkedList<>();
private final List<DiscoveredValue> values = new LinkedList<>();
private boolean initFound = false;
private TransformerMode mode = TransformerMode.OTHER;
public ConfigInjectClassTransformer(String modId, ClassVisitor cw, Set<Type> knownConfigClasses) {
super(ASM9, cw);
this.modId = modId;
this.knownConfigClasses = knownConfigClasses;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.current = Type.getType('L' + name + ';');
mode = TransformerMode.OTHER;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (descriptor.equals(Type.getDescriptor(Category.class)) && mode == TransformerMode.OTHER) {
mode = TransformerMode.CONFIG_CATEGORY;
return new AnnotationMetaGatheringVisitor(super.visitAnnotation(descriptor, visible), referencedConfigs);
}
if (descriptor.equals(Type.getDescriptor(JfConfig.class)) && mode == TransformerMode.OTHER) {
mode = TransformerMode.CONFIG_ROOT;
knownConfigClasses.add(current);
return new AnnotationMetaGatheringVisitor(super.visitAnnotation(descriptor, visible), referencedConfigs);
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public void visitNestMember(String nestMember) {
if (mode != TransformerMode.OTHER) {
categories.add(nestMember);
}
super.visitNestMember(nestMember);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (mode == TransformerMode.CONFIG_ROOT) {
if ("<clinit>".equals(name)) {
initFound = true;
return new ClInitInjectVisitor(
super.visitMethod(access, name, descriptor, signature, exceptions),
current.getInternalName(),
INIT_METHOD,
"()V"
);
}
}
if (mode != TransformerMode.OTHER) {
if (name.startsWith(PREFIX)) {
throw new GradleException("This class declares methods generated by this plugin manually. Do not transform classes twice!");
}
}
if ((access & ACC_STATIC) == ACC_STATIC) {
// Possibly add verifier or preset
return new MethodMetaGatheringVisitor(
super.visitMethod(access, name, descriptor, signature, exceptions),
name, presets, verifiers);
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if ((access & ACC_STATIC) == ACC_STATIC) {
return new FieldMetaGatheringVisitor(super.visitField(access, name, descriptor, signature, value), name, values, Type.getType(descriptor));
}
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public void visitEnd() {
if (mode == TransformerMode.CONFIG_ROOT) {
if (!initFound) {
// Generate <clinit> if missing
GeneratorAdapter2 m = method(ACC_PRIVATE | ACC_STATIC, CLINIT, "()V", null, null);
m.invokeStatic(current, new Method(INIT_METHOD, "()V"));
m.returnValue();
m.endMethod();
}
// Generate main method
{
GeneratorAdapter2 m = method(ACC_PRIVATE | ACC_STATIC, INIT_METHOD, "()V", null, null);
m.comment("Migrate files from old paths");
m.invokeIStatic(CONFIG_HOLDER_TYPE, new Method("getInstance", CONFIG_HOLDER_TYPE, new Type[0]));
m.push(modId);
m.invokeInterface(CONFIG_HOLDER_TYPE, new Method("migrateFiles", "(Ljava/lang/String;)V"));
m.comment("Invoke DSL, continue in " + BUILDER_ROOT);
m.push(modId);
m.invokeIStatic(DSL_TYPE, new Method("create", DSL_DEFAULTED_TYPE, new Type[]{STRING_TYPE}));
m.λ("apply",
getMethodDescriptor(CONFIG_BUILDER_FUNCTION_TYPE),
getMethodType(CONFIG_BUILDER_TYPE, CONFIG_BUILDER_TYPE),
current.getInternalName(),
BUILDER_ROOT);
m.invokeInterface(DSL_DEFAULTED_TYPE, new Method("config", CONFIG_INSTANCE_TYPE, new Type[]{CONFIG_BUILDER_FUNCTION_TYPE}));
m.pop();
m.returnValue();
m.endMethod();
}
// Generate register method for impl
// This method does nothing but is needed to properly implement the interface
// This also ensures the static initializer is called
{
GeneratorAdapter2 m = method(ACC_PUBLIC | ACC_STATIC, REGISTER_METHOD, getMethodDescriptor(VOID_TYPE, DSL_DEFAULTED_TYPE), null, null);
m.comment("This placeholder method is referenced in the FMJ to ensure the static initializer is called");
m.returnValue();
m.endMethod();
}
}
if (mode != TransformerMode.OTHER) {
boolean root = mode == TransformerMode.CONFIG_ROOT;
Type builderType = root ? CONFIG_BUILDER_TYPE : CATEGORY_BUILDER_TYPE;
GeneratorAdapter2 m = method(ACC_PRIVATE | ACC_STATIC, BUILDER_ROOT, getMethodDescriptor(builderType, builderType), null, null);
m.loadArg(0);
for (String name : referencedConfigs) {
m.comment("Reference the config of \"" + name + "\"");
m.push(name);
dslInvoke(m, "referenceConfig", CATEGORY_BUILDER_TYPE, STRING_TYPE);
}
for (String name : categories) {
m.comment("Register the category defined in " + name);
m.push(camelCase(name.substring(current.getInternalName().length())));
m.λ("apply",
getMethodDescriptor(CATEGORY_BUILDER_FUNCTION_TYPE),
getMethodType(CATEGORY_BUILDER_TYPE, CATEGORY_BUILDER_TYPE),
name,
BUILDER_ROOT);
dslInvoke(m, "category", CATEGORY_BUILDER_TYPE, STRING_TYPE, CATEGORY_BUILDER_FUNCTION_TYPE);
}
for (String verifier : verifiers) {
m.comment("Register the verifier \"" + verifier + "\"");
m.runnable(current.getInternalName(), verifier);
dslInvoke(m, "addVerifier", CATEGORY_BUILDER_TYPE, RUNNABLE_TYPE);
}
for (String preset : presets) {
m.comment("Register the preset \"" + preset + "\"");
m.push(preset);
m.runnable(current.getInternalName(), preset);
dslInvoke(m, "addPreset", CATEGORY_BUILDER_TYPE, STRING_TYPE, RUNNABLE_TYPE);
}
for (DiscoveredValue value : values) {
m.comment("Register the value \"" + value + "\"");
value.generateRegistration(m, this);
}
m.returnValue();
m.endMethod();
for (DiscoveredValue value : values) {
value.generateλ(this);
}
}
super.visitEnd();
}
public void dslInvoke(GeneratorAdapter m, String name, Type returnType, Type... arguments) {
boolean root = mode == TransformerMode.CONFIG_ROOT;
Type builderType = root ? CONFIG_BUILDER_TYPE : CATEGORY_BUILDER_TYPE;
m.invokeInterface(builderType, new Method(name, returnType, arguments));
if (root) m.checkCast(CONFIG_BUILDER_TYPE);
}
private String camelCase(String s) {
return Character.toLowerCase(s.charAt(0)) + s.substring(1);
}
public GeneratorAdapter2 method(int access, String name, String descriptor, String signature, String[] exceptions) {
if (BuildMetadata.isRelease && !name.equals(CLINIT)) access |= ACC_SYNTHETIC;
return new GeneratorAdapter2(super.visitMethod(access, name, descriptor, signature, exceptions), access, name, descriptor);
}
}

View File

@ -0,0 +1,59 @@
package io.gitlab.jfronny.libjf.config.plugin.asm;
import io.gitlab.jfronny.libjf.config.api.v1.Entry;
import io.gitlab.jfronny.libjf.config.plugin.asm.value.DiscoveredValue;
import org.objectweb.asm.*;
import java.util.List;
import static org.objectweb.asm.Opcodes.*;
public class FieldMetaGatheringVisitor extends FieldVisitor {
private final String name;
private final List<DiscoveredValue> values;
private final Type type;
protected FieldMetaGatheringVisitor(FieldVisitor fieldVisitor, String name, List<DiscoveredValue> values, Type type) {
super(ASM9, fieldVisitor);
this.name = name;
this.values = values;
this.type = type;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (descriptor.equals(Type.getDescriptor(Entry.class))) {
return new MetaGatheringAnnotationVisitor(super.visitAnnotation(descriptor, visible));
}
return super.visitAnnotation(descriptor, visible);
}
public class MetaGatheringAnnotationVisitor extends AnnotationVisitor {
private double min = Double.NEGATIVE_INFINITY;
private double max = Double.POSITIVE_INFINITY;
protected MetaGatheringAnnotationVisitor(AnnotationVisitor annotationVisitor) {
super(ASM9, annotationVisitor);
}
@Override
public void visit(String name, Object value) {
super.visit(name, value);
switch (name) {
case "min" -> {
min = (Double)value;
}
case "max" -> {
max = (Double)value;
}
case "width" -> {}
default -> throw new IllegalArgumentException("Unexpected name in @Entry annotation: " + name);
}
}
@Override
public void visitEnd() {
super.visitEnd();
values.add(new DiscoveredValue(name, min, max, type));
}
}
}

View File

@ -0,0 +1,32 @@
package io.gitlab.jfronny.libjf.config.plugin.asm;
import io.gitlab.jfronny.libjf.config.api.v1.Preset;
import io.gitlab.jfronny.libjf.config.api.v1.Verifier;
import org.objectweb.asm.*;
import java.util.List;
import static org.objectweb.asm.Opcodes.*;
public class MethodMetaGatheringVisitor extends MethodVisitor {
private final String name;
private final List<String> presets;
private final List<String> verifiers;
protected MethodMetaGatheringVisitor(MethodVisitor methodVisitor, String name, List<String> presets, List<String> verifiers) {
super(ASM9, methodVisitor);
this.name = name;
this.presets = presets;
this.verifiers = verifiers;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (descriptor.equals(Type.getDescriptor(Preset.class))) {
presets.add(name);
}
if (descriptor.equals(Type.getDescriptor(Verifier.class))) {
verifiers.add(name);
}
return super.visitAnnotation(descriptor, visible);
}
}

View File

@ -0,0 +1,5 @@
package io.gitlab.jfronny.libjf.config.plugin.asm;
public enum TransformerMode {
CONFIG_ROOT, CONFIG_CATEGORY, OTHER
}

View File

@ -0,0 +1,96 @@
package io.gitlab.jfronny.libjf.config.plugin.asm.value;
import io.gitlab.jfronny.libjf.config.plugin.asm.ConfigInjectClassTransformer;
import io.gitlab.jfronny.libjf.config.plugin.util.GeneratorAdapter2;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import static io.gitlab.jfronny.libjf.config.plugin.asm.value.KnownTypes.*;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Type.*;
public class DiscoveredValue {
private static final String PREFIX = ConfigInjectClassTransformer.PREFIX;
public final String name;
public final double min;
public final double max;
public final Type aType;
public final KnownType type;
public DiscoveredValue(String name, double min, double max, Type type) {
this.name = name;
this.min = min;
this.max = max;
this.aType = type;
this.type = KnownType.of(type);
}
public void generateRegistration(GeneratorAdapter2 m, ConfigInjectClassTransformer t) {
if (type != KnownType.OBJECT) {
m.push(name);
m.getStatic(t.current, name, aType);
if (!aType.equals(type.unboxed)) {
m.comment("Unboxing value " + name + " from " + aType + " to " + type.unboxed);
m.unbox(type.unboxed); // Unbox (as parameter is unboxed) or leave as is (if target is unboxed)
}
}
switch (type) {
case INT, LONG, FLOAT, DOUBLE -> {
m.comment("Numeric type");
m.push(min);
m.push(max);
m.supplier(t.current.getInternalName(), gName(), gDesc());
m.consumer(t.current.getInternalName(), sName(), sDesc());
t.dslInvoke(m, "value", CATEGORY_BUILDER_TYPE, STRING_TYPE, type.unboxed, DOUBLE_TYPE, DOUBLE_TYPE, SUPPLIER_TYPE, CONSUMER_TYPE);
}
case BOOLEAN, STRING -> {
m.comment("Simple one-value type");
m.supplier(t.current.getInternalName(), gName(), gDesc());
m.consumer(t.current.getInternalName(), sName(), sDesc());
t.dslInvoke(m, "value", CATEGORY_BUILDER_TYPE, STRING_TYPE, type.unboxed, SUPPLIER_TYPE, CONSUMER_TYPE);
}
case OBJECT -> {
System.err.println("WARNING: Attempted to use unsupported type in config. The entry \"" + name + "\" will fall back to reflective runtime access!");
m.comment("Reflective access due to missing compatibility for the type \"" + aType + "\"");
m.push(t.current);
m.push(name);
m.invokeIStatic(ENTRY_INFO_TYPE, new Method("ofField", ENTRY_INFO_TYPE, new Type[]{CLASS_TYPE, STRING_TYPE}));
m.invokeInterface(CATEGORY_BUILDER_TYPE, new Method("value", CATEGORY_BUILDER_TYPE, new Type[]{ENTRY_INFO_TYPE}));
}
}
}
public void generateλ(ConfigInjectClassTransformer t) {
GeneratorAdapter2 m = t.method(ACC_PRIVATE | ACC_STATIC, gName(), gDesc().getInternalName(), null, null);
m.getStatic(t.current, name, aType);
m.comment("Boxing from " + aType + " (" + aType.getSort() + ")");
m.box(aType); // Box if target field uses unboxed value
m.returnValue();
m.endMethod();
m = t.method(ACC_PRIVATE | ACC_STATIC, sName(), sDesc().getInternalName(), null, null);
m.loadArg(0);
m.unbox(aType); // Unbox to the target fields type
m.putStatic(t.current, name, aType);
m.returnValue();
m.endMethod();
}
private String gName() {
return PREFIX + "get$" + name;
}
private Type gDesc() {
return getMethodType(type.boxed);
}
private String sName() {
return PREFIX + "set$" + name;
}
private Type sDesc() {
return getMethodType(VOID_TYPE, type.boxed);
}
}

View File

@ -0,0 +1,41 @@
package io.gitlab.jfronny.libjf.config.plugin.asm.value;
import org.objectweb.asm.Type;
import java.util.HashMap;
import java.util.Map;
import static io.gitlab.jfronny.libjf.config.plugin.asm.value.KnownTypes.*;
import static org.objectweb.asm.Type.*;
public enum KnownType {
INT(INT_BOX_TYPE, INT_TYPE),
LONG(LONG_BOX_TYPE, LONG_TYPE),
FLOAT(FLOAT_BOX_TYPE, FLOAT_TYPE),
DOUBLE(DOUBLE_BOX_TYPE, DOUBLE_TYPE),
STRING(STRING_TYPE, STRING_TYPE),
BOOLEAN(BOOLEAN_BOX_TYPE, BOOLEAN_TYPE),
OBJECT(OBJECT_TYPE, OBJECT_TYPE);
public final Type boxed;
public final Type unboxed;
private static final Map<Type, KnownType> t2t = new HashMap<>();
KnownType(Type boxed, Type unboxed) {
this.boxed = boxed;
this.unboxed = unboxed;
}
static {
for (KnownType value : values()) {
if (value != OBJECT) {
t2t.put(value.boxed, value);
t2t.put(value.unboxed, value);
}
}
}
public static KnownType of(Type type) {
return t2t.getOrDefault(type, OBJECT);
}
}

View File

@ -0,0 +1,38 @@
package io.gitlab.jfronny.libjf.config.plugin.asm.value;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.*;
import org.objectweb.asm.Type;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class KnownTypes {
// DSL types
public static final Type CONFIG_BUILDER_TYPE = Type.getType(ConfigBuilder.class);
public static final Type CONFIG_BUILDER_FUNCTION_TYPE = Type.getType(ConfigBuilder.ConfigBuilderFunction.class);
public static final Type CATEGORY_BUILDER_TYPE = Type.getType(CategoryBuilder.class);
public static final Type CATEGORY_BUILDER_FUNCTION_TYPE = Type.getType(CategoryBuilder.CategoryBuilderFunction.class);
public static final Type DSL_TYPE = Type.getType(DSL.class);
public static final Type DSL_DEFAULTED_TYPE = Type.getType(DSL.Defaulted.class);
public static final Type CONFIG_HOLDER_TYPE = Type.getType(ConfigHolder.class);
public static final Type CONFIG_INSTANCE_TYPE = Type.getType(ConfigInstance.class);
public static final Type ENTRY_INFO_TYPE = Type.getType(EntryInfo.class);
// Boxes
public static final Type INT_BOX_TYPE = Type.getType(Integer.class);
public static final Type LONG_BOX_TYPE = Type.getType(Long.class);
public static final Type FLOAT_BOX_TYPE = Type.getType(Float.class);
public static final Type DOUBLE_BOX_TYPE = Type.getType(Double.class);
public static final Type BOOLEAN_BOX_TYPE = Type.getType(Boolean.class);
public static final Type CHARACTER_BOX_TYPE = Type.getType(Character.class);
public static final Type SHORT_BOX_TYPE = Type.getType(Short.class);
public static final Type BYTE_BOX_TYPE = Type.getType(Byte.class);
// Additional
public static final Type STRING_TYPE = Type.getType(String.class);
public static final Type OBJECT_TYPE = Type.getType(Object.class);
public static final Type RUNNABLE_TYPE = Type.getType(Runnable.class);
public static final Type SUPPLIER_TYPE = Type.getType(Supplier.class);
public static final Type CONSUMER_TYPE = Type.getType(Consumer.class);
public static final Type NUMBER_TYPE = Type.getType(Number.class);
public static final Type CLASS_TYPE = Type.getType(Class.class);
}

View File

@ -0,0 +1,39 @@
package io.gitlab.jfronny.libjf.config.plugin.fmj;
import io.gitlab.jfronny.gson.*;
import io.gitlab.jfronny.gson.stream.JsonReader;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import io.gitlab.jfronny.libjf.config.plugin.asm.ConfigInjectClassTransformer;
import org.objectweb.asm.Type;
import java.util.*;
import java.util.function.Predicate;
public class FabricModJsonTransformer {
private static final Gson INPUT_GSON = new GsonBuilder().setLenient().create();
private static final Gson OUTPUT_GSON = new GsonBuilder().create();
private static final String ENTRYPOINTS = "entrypoints";
private static final String LIBJF_CONFIG = ConfigHolderImpl.MODULE_ID;
public static void transform(JsonReader reader, JsonWriter writer, Set<Type> configClasses) {
JsonObject fmj = INPUT_GSON.<JsonElement>fromJson(reader, JsonElement.class).getAsJsonObject();
if (!fmj.has(ENTRYPOINTS)) fmj.add(ENTRYPOINTS, new JsonObject());
JsonObject entrypoints = fmj.get(ENTRYPOINTS).getAsJsonObject();
if (!entrypoints.has(LIBJF_CONFIG)) entrypoints.add(LIBJF_CONFIG, new JsonArray());
JsonArray libjfConfig = entrypoints.getAsJsonArray(LIBJF_CONFIG).getAsJsonArray();
for (Type klazz : configClasses) {
// Remove references to class
Iterator<JsonElement> each = libjfConfig.iterator();
while(each.hasNext()) {
JsonElement element = each.next();
if (element.isJsonPrimitive() && element.getAsString().equals(klazz.getClassName())) {
each.remove();
}
}
// Add reference to init method
libjfConfig.add(klazz.getClassName() + "::" + ConfigInjectClassTransformer.REGISTER_METHOD);
}
OUTPUT_GSON.toJson(fmj, writer);
}
}

View File

@ -0,0 +1,78 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.file.RelativePath;
import org.gradle.api.internal.file.DefaultFileTreeElement;
import java.io.*;
public class ArchiveFileTreeElement implements FileTreeElement {
private final RelativeArchivePath archivePath;
public ArchiveFileTreeElement(RelativeArchivePath archivePath) {
this.archivePath = archivePath;
}
public boolean isClassFile() {
return archivePath.isClassFile();
}
@Override
public File getFile() {
return null;
}
@Override
public boolean isDirectory() {
return archivePath.entry.isDirectory();
}
@Override
public long getLastModified() {
return archivePath.entry.getLastModifiedDate().getTime();
}
@Override
public long getSize() {
return archivePath.entry.getSize();
}
@Override
public InputStream open() {
return null;
}
@Override
public void copyTo(OutputStream outputStream) {
}
@Override
public boolean copyTo(File file) {
return false;
}
@Override
public String getName() {
return archivePath.getPathString();
}
@Override
public String getPath() {
return archivePath.getLastName();
}
@Override
public RelativeArchivePath getRelativePath() {
return archivePath;
}
@Override
public int getMode() {
return archivePath.entry.getUnixMode();
}
public FileTreeElement asFileTreeElement() {
return new DefaultFileTreeElement(null, new RelativePath(!isDirectory(), archivePath.getSegments()), null, null);
}
}

View File

@ -0,0 +1,26 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
public class ClInitInjectVisitor extends MethodVisitor {
private final String owner;
private final String name;
private final String descriptor;
public ClInitInjectVisitor(MethodVisitor mw, String owner, String name, String descriptor) {
super(ASM9, mw);
this.owner = owner;
this.name = name;
this.descriptor = descriptor;
}
@Override
public void visitInsn(int opcode) {
if (opcode == RETURN) {
super.visitMethodInsn(INVOKESTATIC, owner, name, descriptor, false);
}
super.visitInsn(opcode);
}
}

View File

@ -0,0 +1,30 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import org.apache.tools.zip.Zip64Mode;
import org.apache.tools.zip.ZipOutputStream;
import org.gradle.api.UncheckedIOException;
import java.io.File;
public class DefaultZipCompressor implements ZipCompressor {
private final int entryCompressionMethod;
private final Zip64Mode zip64Mode;
public DefaultZipCompressor(boolean allowZip64Mode, int entryCompressionMethod) {
this.entryCompressionMethod = entryCompressionMethod;
this.zip64Mode = allowZip64Mode ? Zip64Mode.AsNeeded : Zip64Mode.Never;
}
@Override
public ZipOutputStream createArchiveOutputStream(File destination) {
try {
ZipOutputStream zipOutputStream = new ZipOutputStream(destination);
zipOutputStream.setUseZip64(zip64Mode);
zipOutputStream.setMethod(entryCompressionMethod);
return zipOutputStream;
} catch (Exception e) {
String message = String.format("Unable to create ZIP output stream for file %s.", destination);
throw new UncheckedIOException(message, e);
}
}
}

View File

@ -0,0 +1,36 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import java.io.IOException;
import java.io.OutputStream;
public class DelegatingUncloseableOutputStream extends OutputStream {
private final OutputStream delegate;
public DelegatingUncloseableOutputStream(OutputStream delegate) {
super();
this.delegate = delegate;
}
@Override
public void write(int i) throws IOException {
delegate.write(i);
}
@Override
public void write(byte[] b) throws IOException {
delegate.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
delegate.write(b, off, len);
}
@Override
public void flush() {
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,35 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class EnumerationSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
private final Enumeration<T> enumeration;
public EnumerationSpliterator(long est, int additionalCharacteristics, Enumeration<T> enumeration) {
super(est, additionalCharacteristics);
this.enumeration = enumeration;
}
@Override
public boolean tryAdvance(Consumer<? super T> action) {
if (enumeration.hasMoreElements()) {
action.accept(enumeration.nextElement());
return true;
}
return false;
}
@Override
public void forEachRemaining(Consumer<? super T> action) {
while (enumeration.hasMoreElements())
action.accept(enumeration.nextElement());
}
public static <T> Stream<T> stream(Enumeration<T> enumeration) {
EnumerationSpliterator<T> spliterator = new EnumerationSpliterator<>(Long.MAX_VALUE, Spliterator.ORDERED, enumeration);
return StreamSupport.stream(spliterator, false);
}
}

View File

@ -0,0 +1,123 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import io.gitlab.jfronny.libjf.config.plugin.BuildMetadata;
import io.gitlab.jfronny.libjf.config.plugin.asm.value.KnownTypes;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import java.lang.invoke.LambdaMetafactory;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
public class GeneratorAdapter2 extends GeneratorAdapter {
private static final Handle metafactory = new Handle(
H_INVOKESTATIC,
Type.getInternalName(LambdaMetafactory.class),
"metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
false
);
public GeneratorAdapter2(MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(ASM9, methodVisitor, access, name, descriptor);
}
public void λ(String lambdaMethodName, String lambdaType, Type lambdaDescriptor, Type implDescriptor, String implOwner, String implName) {
invokeDynamic(lambdaMethodName,
lambdaType,
metafactory,
lambdaDescriptor,
new Handle(H_INVOKESTATIC, implOwner, implName, implDescriptor.getInternalName(), false),
implDescriptor);
}
public void λ(String lambdaMethodName, String lambdaType, Type descriptor, String implOwner, String implName) {
λ(lambdaMethodName, lambdaType, descriptor, descriptor, implOwner, implName);
}
public void runnable(String implOwner, String implName) {
λ("run",
"()Ljava/lang/Runnable;",
Type.getType("()V"),
implOwner,
implName);
}
public void supplier(String implOwner, String implName, Type implDescriptor) {
λ("get",
"()Ljava/util/function/Supplier;",
Type.getType("()Ljava/lang/Object;"),
implDescriptor,
implOwner,
implName);
}
public void consumer(String implOwner, String implName, Type implDescriptor) {
λ("accept",
"()Ljava/util/function/Consumer;",
Type.getType("(Ljava/lang/Object;)V"),
implDescriptor,
implOwner,
implName);
}
public void box(Type type) {
Type boxedType;
switch (type.getSort()) {
case Type.VOID:
return;
case Type.CHAR:
boxedType = KnownTypes.CHARACTER_BOX_TYPE;
break;
case Type.BOOLEAN:
boxedType = KnownTypes.BOOLEAN_BOX_TYPE;
break;
case Type.DOUBLE:
boxedType = KnownTypes.DOUBLE_BOX_TYPE;
break;
case Type.FLOAT:
boxedType = KnownTypes.FLOAT_BOX_TYPE;
break;
case Type.LONG:
boxedType = KnownTypes.LONG_BOX_TYPE;
break;
case Type.INT:
boxedType = KnownTypes.INT_BOX_TYPE;
break;
case Type.SHORT:
boxedType = KnownTypes.SHORT_BOX_TYPE;
break;
case Type.BYTE:
boxedType = KnownTypes.BYTE_BOX_TYPE;
break;
default:
boxedType = null;
break;
}
if (boxedType == null) {
comment("Unkown boxed type, simply casting");
checkCast(type);
} else {
comment("Boxing " + type + " to " + boxedType);
invokeStatic(boxedType, new Method("valueOf", boxedType, new Type[]{type}));
}
}
public void invokeIStatic(Type owner, Method method) {
invokeInsn(Opcodes.INVOKESTATIC, owner, method, true);
}
public void comment(String comment) {
if (BuildMetadata.isRelease) return;
push("[Comment/Generator] " + comment);
pop();
}
// Taken from GeneratorAdapter
private void invokeInsn(final int opcode, final Type type, final Method method, final boolean isInterface) {
String owner = type.getSort() == Type.ARRAY ? type.getDescriptor() : type.getInternalName();
mv.visitMethodInsn(opcode, owner, method.getName(), method.getDescriptor(), isInterface);
}
}

View File

@ -0,0 +1,21 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import org.apache.tools.zip.ZipOutputStream;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.bundling.ZipEntryCompression;
import org.gradle.util.GradleVersion;
public class GradleVersionUtil {
private final GradleVersion version;
public GradleVersionUtil(String version) {
this.version = GradleVersion.version(version);
}
public ZipCompressor getInternalCompressor(ZipEntryCompression entryCompression, Jar jar) {
return switch (entryCompression) {
case DEFLATED -> new DefaultZipCompressor(jar.isZip64(), ZipOutputStream.DEFLATED);
case STORED -> new DefaultZipCompressor(jar.isZip64(), ZipOutputStream.STORED);
};
}
}

View File

@ -0,0 +1,32 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import org.apache.tools.zip.ZipEntry;
import org.gradle.api.file.RelativePath;
import java.util.Arrays;
import java.util.List;
public class RelativeArchivePath extends RelativePath {
public ZipEntry entry;
public RelativeArchivePath(ZipEntry entry) {
super(!entry.isDirectory(), entry.getName().split("/"));
this.entry = entry;
}
public boolean isClassFile() {
return getLastName().endsWith(".class");
}
@Override
public RelativeArchivePath getParent() {
List<String> segments = Arrays.asList(getSegments());
if (segments.size() == 1) {
return null;
} else {
//Parent is always a directory so add / to the end of the path
String path = String.join("/", segments.subList(0, segments.size() - 1)) + "/";
return new RelativeArchivePath(new ZipEntry(path));
}
}
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.libjf.config.plugin.util;
import org.apache.tools.zip.ZipOutputStream;
import org.gradle.api.internal.file.archive.compression.ArchiveOutputStreamFactory;
import java.io.File;
public interface ZipCompressor extends ArchiveOutputStreamFactory {
ZipOutputStream createArchiveOutputStream(File destination);
}

View File

@ -0,0 +1,5 @@
archivesBaseName = "libjf-config-core-v1"
dependencies {
api project(path: ":libjf-base", configuration: "dev")
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.api;
package io.gitlab.jfronny.libjf.config.api.v1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@ -8,4 +8,5 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Category {
String[] referencedConfigs() default {};
}

View File

@ -0,0 +1,33 @@
package io.gitlab.jfronny.libjf.config.api.v1;
import io.gitlab.jfronny.libjf.LibJf;
import java.util.List;
import java.util.Map;
public interface ConfigCategory {
String getId();
String getCategoryPath();
default String getTranslationPrefix() {
return getId() + ".jfconfig." + getCategoryPath();
}
List<EntryInfo<?>> getEntries();
Map<String, Runnable> getPresets();
List<ConfigInstance> getReferencedConfigs();
Map<String, ConfigCategory> getCategories();
ConfigInstance getRoot();
default void fix() {
for (EntryInfo<?> entry : getEntries()) {
entry.fix();
}
}
default void reset() {
for (EntryInfo<?> entry : getEntries()) {
try {
entry.reset();
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not reload default values", e);
}
}
}
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.libjf.config.api.v1;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import java.nio.file.Path;
import java.util.Map;
public interface ConfigHolder {
static ConfigHolder getInstance() {
return ConfigHolderImpl.INSTANCE;
}
void register(String modId, ConfigInstance config);
Map<String, ConfigInstance> getRegistered();
ConfigInstance get(String modId);
ConfigInstance get(Path configPath);
boolean isRegistered(String modId);
boolean isRegistered(Path configPath);
void migrateFiles(String modId);
}

View File

@ -0,0 +1,13 @@
package io.gitlab.jfronny.libjf.config.api.v1;
import java.nio.file.Path;
import java.util.*;
public interface ConfigInstance extends ConfigCategory {
static ConfigInstance get(String modId) {
return ConfigHolder.getInstance().get(modId);
}
void load();
void write();
Optional<Path> getFilePath();
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.api;
package io.gitlab.jfronny.libjf.config.api.v1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@ -0,0 +1,90 @@
package io.gitlab.jfronny.libjf.config.api.v1;
import io.gitlab.jfronny.gson.JsonElement;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import io.gitlab.jfronny.libjf.config.impl.dsl.DslEntryInfo;
import org.jetbrains.annotations.ApiStatus;
import java.io.IOException;
import java.lang.reflect.Field;
public interface EntryInfo<T> {
static EntryInfo<?> ofField(Field field) {
return DslEntryInfo.ofField(field);
}
@ApiStatus.Internal
static EntryInfo<?> ofField(Class<?> klazz, String name) {
try {
return ofField(klazz.getField(name));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
/**
* Get the name of this entry
* @return This entry's name
*/
String getName();
/**
* @return Get the default value of this entry
*/
T getDefault();
/**
* Gets the current value
* @return The current value
*/
T getValue() throws IllegalAccessException;
/**
* Set the current value to the parameter
* @param value The value to use
*/
void setValue(T value) throws IllegalAccessException;
/**
* Get the value type of this entry. Will use the class definition, not the current value
* @return The type of this entry
*/
Type<T> getValueType();
/**
* Ensure the current value is within expected bounds.
*/
void fix();
/**
* Set this entry's value to that of the element
* @param element The element to read from
*/
void loadFromJson(JsonElement element) throws IllegalAccessException;
/**
* Write the currently cached value to the writer
* @param writer The writer to write to
*/
void writeTo(JsonWriter writer, String translationPrefix) throws IOException, IllegalAccessException;
/**
* @return Get the width for this entry
*/
int getWidth();
/**
* @return Get the minimum value of this entry
*/
double getMinValue();
/**
* @return Get the maximum value for this entry
*/
double getMaxValue();
default void reset() throws IllegalAccessException {
setValue(getDefault());
}
}

View File

@ -0,0 +1,9 @@
package io.gitlab.jfronny.libjf.config.api.v1;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JfConfig {
String[] referencedConfigs() default {};
}

View File

@ -0,0 +1,7 @@
package io.gitlab.jfronny.libjf.config.api.v1;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
public interface JfCustomConfig {
void register(DSL.Defaulted dsl);
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.api;
package io.gitlab.jfronny.libjf.config.api.v1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.api;
package io.gitlab.jfronny.libjf.config.api.v1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@ -0,0 +1,39 @@
package io.gitlab.jfronny.libjf.config.api.v1.dsl;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import java.lang.reflect.Field;
import java.util.function.Consumer;
import java.util.function.Supplier;
public interface CategoryBuilder<Builder extends CategoryBuilder<Builder>> {
Builder setTranslationPrefix(String translationPrefix);
String getTranslationPrefix();
Builder addPreset(String id, Consumer<ConfigCategory> action);
Builder addPreset(String id, Runnable preset);
Builder addVerifier(Consumer<ConfigCategory> verifier);
Builder addVerifier(Runnable verifier);
Builder referenceConfig(String id);
Builder referenceConfig(ConfigInstance config);
Builder category(String id, CategoryBuilderFunction builder);
Builder value(String id, int def, double min, double max, Supplier<Integer> get, Consumer<Integer> set);
Builder value(String id, long def, double min, double max, Supplier<Long> get, Consumer<Long> set);
Builder value(String id, float def, double min, double max, Supplier<Float> get, Consumer<Float> set);
Builder value(String id, double def, double min, double max, Supplier<Double> get, Consumer<Double> set);
Builder value(String id, String def, Supplier<String> get, Consumer<String> set);
Builder value(String id, boolean def, Supplier<Boolean> get, Consumer<Boolean> set);
Builder value(String id, String def, String[] options, Supplier<String> get, Consumer<String> set);
<T extends Enum<T>> Builder value(String id, T def, Class<T> klazz, Supplier<T> get, Consumer<T> set);
<T> Builder value(String id, T def, double min, double max, Type<T> type, int width, Supplier<T> get, Consumer<T> set);
<T> Builder value(EntryInfo<T> entry);
String getId();
ConfigCategory build(Supplier<ConfigInstance> root);
@FunctionalInterface
interface CategoryBuilderFunction {
CategoryBuilder<?> apply(CategoryBuilder<?> builder);
}
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.libjf.config.api.v1.dsl;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import java.nio.file.Path;
import java.util.function.Consumer;
public interface ConfigBuilder<Builder extends ConfigBuilder<Builder>> extends CategoryBuilder<Builder> {
Builder setLoadMethod(Consumer<ConfigInstance> load);
Builder setWriteMethod(Consumer<ConfigInstance> write);
Builder setPath(Path path);
ConfigInstance build();
@FunctionalInterface
interface ConfigBuilderFunction {
ConfigBuilder<?> apply(ConfigBuilder<?> builder);
}
}

View File

@ -0,0 +1,25 @@
package io.gitlab.jfronny.libjf.config.api.v1.dsl;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import io.gitlab.jfronny.libjf.config.impl.dsl.DSLImpl;
public interface DSL {
static DSL create() {
return new DSLImpl();
}
static DSL.Defaulted create(String defaultId) {
return new DSLImpl.Defaulted(defaultId);
}
ConfigInstance config(String configId, ConfigBuilder.ConfigBuilderFunction builder);
ConfigInstance register(String configId, ConfigBuilder.ConfigBuilderFunction builder);
ConfigInstance register(ConfigHolder config, String configId, ConfigBuilder.ConfigBuilderFunction builder);
interface Defaulted extends DSL {
ConfigInstance config(ConfigBuilder.ConfigBuilderFunction builder);
ConfigInstance register(ConfigBuilder.ConfigBuilderFunction builder);
ConfigInstance register(ConfigHolder config, ConfigBuilder.ConfigBuilderFunction builder);
}
}

View File

@ -0,0 +1,200 @@
package io.gitlab.jfronny.libjf.config.api.v1.type;
import org.jetbrains.annotations.Nullable;
public sealed interface Type<T> {
static Type<?> ofClass(java.lang.reflect.Type klazz) {
if (klazz == int.class || klazz == Integer.class) return TInt.INSTANCE;
else if (klazz == long.class || klazz == Long.class) return TLong.INSTANCE;
else if (klazz == float.class || klazz == Float.class) return TFloat.INSTANCE;
else if (klazz == double.class || klazz == Double.class) return TDouble.INSTANCE;
else if (klazz == String.class) return TString.INSTANCE;
else if (klazz == boolean.class || klazz == Boolean.class) return TBool.INSTANCE;
else if (klazz instanceof Class<?> k && k.isEnum()) return new TEnum<>(k);
else return new TUnknown<>(klazz);
}
default boolean isInt() {
return false;
}
default boolean isLong() {
return false;
}
default boolean isFloat() {
return false;
}
default boolean isDouble() {
return false;
}
default boolean isString() {
return false;
}
default boolean isBool() {
return false;
}
default boolean isEnum() {
return false;
}
@Nullable java.lang.reflect.Type asClass();
String getName();
final class TInt implements Type<Integer> {
public static TInt INSTANCE = new TInt();
private TInt() {}
@Override
public boolean isInt() {
return true;
}
@Override
public Class<Integer> asClass() {
return Integer.class;
}
@Override
public String getName() {
return "Integer";
}
}
final class TLong implements Type<Long> {
public static TLong INSTANCE = new TLong();
private TLong() {}
@Override
public boolean isLong() {
return true;
}
@Override
public Class<Long> asClass() {
return Long.class;
}
@Override
public String getName() {
return "Long";
}
}
final class TFloat implements Type<Float> {
public static TFloat INSTANCE = new TFloat();
private TFloat() {}
@Override
public boolean isFloat() {
return true;
}
@Override
public Class<Float> asClass() {
return Float.class;
}
@Override
public String getName() {
return "Float";
}
}
final class TDouble implements Type<Double> {
public static TDouble INSTANCE = new TDouble();
private TDouble() {}
@Override
public boolean isDouble() {
return true;
}
@Override
public Class<Double> asClass() {
return Double.class;
}
@Override
public String getName() {
return "Double";
}
}
final class TString implements Type<String> {
public static TString INSTANCE = new TString();
private TString() {}
@Override
public boolean isString() {
return true;
}
@Override
public Class<String> asClass() {
return String.class;
}
@Override
public String getName() {
return "String";
}
}
final class TBool implements Type<Boolean> {
public static TBool INSTANCE = new TBool();
private TBool() {}
@Override
public boolean isBool() {
return true;
}
@Override
public Class<Boolean> asClass() {
return Boolean.class;
}
@Override
public String getName() {
return "Boolean";
}
}
final record TEnum<T>(@Nullable Class<T> klazz, String name, T[] options) implements Type<T> {
public TEnum(Class<T> klazz) {
this(klazz, klazz.getSimpleName(), klazz.getEnumConstants());
}
public static TEnum<String> create(String name, String[] options) {
return new TEnum<>(null, name, options);
}
@Override
public boolean isEnum() {
return true;
}
@Override
public Class<T> asClass() {
return klazz;
}
@Override
public String getName() {
return name;
}
public T optionForString(String string) {
for (T option : options) {
if (option.toString().equals(string)) return option;
}
return null;
}
}
final record TUnknown<T>(java.lang.reflect.Type klazz) implements Type<T> {
@Override
public @Nullable java.lang.reflect.Type asClass() {
return klazz;
}
@Override
public String getName() {
return klazz instanceof Class<?> k ? k.getSimpleName() : klazz.getTypeName();
}
}
}

View File

@ -0,0 +1,72 @@
package io.gitlab.jfronny.libjf.config.impl;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.Category;
import io.gitlab.jfronny.libjf.config.api.v1.JfConfig;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.CategoryBuilder;
import io.gitlab.jfronny.libjf.gson.FabricLoaderGsonGenerator;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.metadata.CustomValue;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedList;
import java.util.List;
import static io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl.MODULE_ID;
public class AuxiliaryMetadata {
public static AuxiliaryMetadata of(Category category) {
AuxiliaryMetadata meta = new AuxiliaryMetadata();
if (category != null) {
meta.referencedConfigs = List.of(category.referencedConfigs());
}
return meta.sanitize();
}
public static AuxiliaryMetadata of(JfConfig config) {
AuxiliaryMetadata meta = new AuxiliaryMetadata();
if (config != null) {
meta.referencedConfigs = List.of(config.referencedConfigs());
}
return meta.sanitize();
}
public static @Nullable AuxiliaryMetadata forMod(String modId) {
var metaRef = new Object() {
AuxiliaryMetadata meta = null;
};
FabricLoader.getInstance().getModContainer(modId).ifPresent(container -> {
CustomValue cv = container.getMetadata().getCustomValue(MODULE_ID);
if (cv == null) {
cv = container.getMetadata().getCustomValue("libjf");
if (cv != null) {
cv = cv.getAsObject().get("config");
}
}
if (cv != null) metaRef.meta = GsonHolder.getGson().fromJson(FabricLoaderGsonGenerator.toGson(cv), AuxiliaryMetadata.class);
});
return metaRef.meta;
}
public List<String> referencedConfigs;
public void applyTo(CategoryBuilder<?> builder) {
if (referencedConfigs != null) referencedConfigs.forEach(builder::referenceConfig);
}
public AuxiliaryMetadata sanitize() {
if (referencedConfigs == null) referencedConfigs = List.of();
else referencedConfigs = List.copyOf(referencedConfigs);
return this;
}
public AuxiliaryMetadata merge(AuxiliaryMetadata other) {
if (other == null) return this;
AuxiliaryMetadata meta = new AuxiliaryMetadata();
meta.referencedConfigs = new LinkedList<>();
meta.referencedConfigs.addAll(this.referencedConfigs);
meta.referencedConfigs.addAll(other.referencedConfigs);
return meta.sanitize();
}
}

View File

@ -0,0 +1,87 @@
package io.gitlab.jfronny.libjf.config.impl;
import com.google.common.collect.ImmutableMap;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import net.fabricmc.loader.api.FabricLoader;
import org.jetbrains.annotations.ApiStatus;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class ConfigHolderImpl implements ConfigHolder {
@ApiStatus.Internal
public static final ConfigHolderImpl INSTANCE = new ConfigHolderImpl();
private ConfigHolderImpl() {
}
public static final String MODULE_ID = "libjf:config";
private final Map<String, ConfigInstance> configs = new HashMap<>();
private final Map<Path, ConfigInstance> configsByPath = new HashMap<>();
@Override
public void register(String modId, ConfigInstance config) {
if (isRegistered(modId)) {
LibJf.LOGGER.warn("Overriding config class of " + modId + " to " + config);
}
LibJf.LOGGER.info("Registering config for " + modId);
configs.put(modId, config);
config.getFilePath().ifPresent(path -> configsByPath.put(path, config));
}
@Override
public Map<String, ConfigInstance> getRegistered() {
return ImmutableMap.copyOf(configs);
}
@Override
public ConfigInstance get(String configClass) {
return configs.get(configClass);
}
@Override
public ConfigInstance get(Path configPath) {
return configsByPath.get(configPath);
}
@Override
public boolean isRegistered(String modId) {
return configs.containsKey(modId);
}
@Override
public boolean isRegistered(Path configPath) {
return configsByPath.containsKey(configPath);
}
@Override
public void migrateFiles(String modId) {
Path cfg = FabricLoader.getInstance().getConfigDir();
Path target = cfg.resolve(modId + ".json5");
if (Files.exists(target)) return;
if (Files.exists(cfg.resolve(modId + ".json"))) {
try {
Files.move(cfg.resolve(modId + ".json"), target);
} catch (IOException ignored) {
}
}
FabricLoader.getInstance().getModContainer(modId).ifPresent(mod -> {
try {
for (String id : mod.getMetadata().getProvides()) {
if (Files.exists(cfg.resolve(id + ".json"))) {
Files.move(cfg.resolve(id + ".json"), target);
return;
}
if (Files.exists(cfg.resolve(id + ".json5"))) {
Files.move(cfg.resolve(id + ".json5"), target);
return;
}
}
} catch (IOException ignored) {
}
});
}
}

View File

@ -2,7 +2,7 @@ package io.gitlab.jfronny.libjf.config.impl;
import io.gitlab.jfronny.commons.throwable.*;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigHolder;
import io.gitlab.jfronny.libjf.coprocess.ThreadCoProcess;
import net.fabricmc.loader.api.FabricLoader;

View File

@ -0,0 +1,189 @@
package io.gitlab.jfronny.libjf.config.impl.dsl;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.CategoryBuilder;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class CategoryBuilderImpl<Builder extends CategoryBuilderImpl<Builder>> implements CategoryBuilder<Builder> {
public final List<CategoryBuilder<?>> categories = new LinkedList<>();
public final String id;
public final String categoryPath;
public String translationPrefix;
public final List<EntryInfo<?>> entries = new LinkedList<>();
public final Map<String, Consumer<ConfigCategory>> presets = new LinkedHashMap<>();
public final List<Supplier<ConfigInstance>> referencedConfigs = new LinkedList<>();
public final List<Consumer<ConfigCategory>> verifiers = new LinkedList<>();
private boolean built = false;
public CategoryBuilderImpl(String id, String categoryPath) {
this.id = id;
this.categoryPath = categoryPath;
this.translationPrefix = id + ".jfconfig." + categoryPath;
}
@Override
public Builder setTranslationPrefix(String translationPrefix) {
checkBuilt();
this.translationPrefix = translationPrefix;
return asBuilder();
}
@Override
public String getTranslationPrefix() {
checkBuilt();
return translationPrefix;
}
@Override
public Builder addPreset(String id, Consumer<ConfigCategory> action) {
checkBuilt();
presets.put(id, action);
return asBuilder();
}
@Override
public Builder addPreset(String id, Runnable preset) {
return addPreset(id, cfg -> preset.run());
}
@Override
public Builder addVerifier(Consumer<ConfigCategory> verifier) {
checkBuilt();
verifiers.add(verifier);
return asBuilder();
}
@Override
public Builder addVerifier(Runnable verifier) {
return addVerifier(cfg -> verifier.run());
}
@Override
public Builder referenceConfig(String id) {
checkBuilt();
referencedConfigs.add(() -> ConfigHolder.getInstance().get(id));
return asBuilder();
}
@Override
public Builder referenceConfig(ConfigInstance config) {
checkBuilt();
referencedConfigs.add(() -> config);
return asBuilder();
}
@Override
public Builder category(String id, CategoryBuilderFunction builder) {
checkBuilt();
categories.add(builder.apply(new CategoryBuilderImpl(id, categoryPath + id + ".").setTranslationPrefix(translationPrefix + id + ".")));
return asBuilder();
}
@Override
public Builder value(String id, int def, double min, double max, Supplier<Integer> get, Consumer<Integer> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, Type.TInt.INSTANCE, 100, min, max));
return asBuilder();
}
@Override
public Builder value(String id, long def, double min, double max, Supplier<Long> get, Consumer<Long> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, Type.TLong.INSTANCE, 100, min, max));
return asBuilder();
}
@Override
public Builder value(String id, float def, double min, double max, Supplier<Float> get, Consumer<Float> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, Type.TFloat.INSTANCE, 100, min, max));
return asBuilder();
}
@Override
public Builder value(String id, double def, double min, double max, Supplier<Double> get, Consumer<Double> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, Type.TDouble.INSTANCE, 100, min, max));
return asBuilder();
}
@Override
public Builder value(String id, String def, Supplier<String> get, Consumer<String> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, Type.TString.INSTANCE));
return asBuilder();
}
@Override
public Builder value(String id, boolean def, Supplier<Boolean> get, Consumer<Boolean> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, Type.TBool.INSTANCE));
return asBuilder();
}
@Override
public Builder value(String id, String def, String[] options, Supplier<String> get, Consumer<String> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, Type.TEnum.create(id, options)));
return asBuilder();
}
@Override
public <T extends Enum<T>> Builder value(String id, T def, Class<T> klazz, Supplier<T> get, Consumer<T> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, new Type.TEnum<>(klazz)));
return asBuilder();
}
@Override
public <T> Builder value(String id, T def, double min, double max, Type<T> type, int width, Supplier<T> get, Consumer<T> set) {
checkBuilt();
entries.add(new DslEntryInfo<>(id, def, get::get, set::accept, type, width, min, max));
return asBuilder();
}
@Override
public <T> Builder value(EntryInfo<T> entry) {
checkBuilt();
entries.add(Objects.requireNonNull(entry));
return asBuilder();
}
@Override
public String getId() {
return id;
}
protected Builder asBuilder() {
//noinspection unchecked
return (Builder) this;
}
protected void checkBuilt() {
if (built) throw new IllegalStateException("This builder was already used to build a category!");
}
protected void markBuilt() {
checkBuilt();
built = true;
}
@Override
public DslConfigCategory build(Supplier<ConfigInstance> root) {
markBuilt();
return new DslConfigCategory(id,
categoryPath,
translationPrefix,
entries,
presets,
() -> referencedConfigs.stream().map(Supplier::get).toList(),
categories.stream().collect(Collectors.toMap(CategoryBuilder::getId, b -> b.build(root))),
root,
verifiers);
}
}

View File

@ -0,0 +1,133 @@
package io.gitlab.jfronny.libjf.config.impl.dsl;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.gson.*;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.CategoryBuilder;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.ConfigBuilder;
import io.gitlab.jfronny.libjf.config.impl.JfConfigWatchService;
import io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigSafe;
import net.fabricmc.loader.api.FabricLoader;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ConfigBuilderImpl extends CategoryBuilderImpl<ConfigBuilderImpl> implements ConfigBuilder<ConfigBuilderImpl> {
public DslConfigInstance built;
public Consumer<ConfigInstance> load;
public Consumer<ConfigInstance> write;
public Path path;
public ConfigBuilderImpl(String id) {
super(id, "");
load = c -> {
c.getFilePath().ifPresent(path -> {
if (Files.exists(path)) {
try (BufferedReader br = Files.newBufferedReader(path)) {
JsonElement element = JsonParser.parseReader(br);
if (element.isJsonObject()) loadFrom(element.getAsJsonObject(), c);
else LibJf.LOGGER.error("Invalid config: Not a JSON object for " + id);
} catch (Exception e) {
LibJf.LOGGER.error("Could not read config for " + id, e);
}
}
c.write();
});
};
write = c -> {
c.getFilePath().ifPresent(path -> JfConfigWatchService.lock(path, () -> {
try (BufferedWriter bw = Files.newBufferedWriter(path);
JsonWriter jw = GsonHolder.getGson().newJsonWriter(bw)) {
writeTo(jw, c);
} catch (Exception e) {
LibJf.LOGGER.error("Could not write config", e);
}
}));
};
path = FabricLoader.getInstance().getConfigDir().resolve(id + ".json5");
}
private static void loadFrom(JsonObject source, ConfigCategory category) {
for (EntryInfo<?> entry : category.getEntries()) {
if (source.has(entry.getName())) {
try {
entry.loadFromJson(source.get(entry.getName()));
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not set config entry value of " + entry.getName(), e);
}
} else LibJf.LOGGER.error("Config does not contain entry for " + entry.getName());
}
for (Map.Entry<String, ConfigCategory> entry : category.getCategories().entrySet()) {
if (source.has(entry.getKey())) {
JsonElement el = source.get(entry.getKey());
if (el.isJsonObject()) loadFrom(el.getAsJsonObject(), entry.getValue());
else LibJf.LOGGER.error("Config category is not a JSON object, skipping");
} else LibJf.LOGGER.error("Config does not contain entry for subcategory " + entry.getKey());
}
}
private static void writeTo(JsonWriter writer, ConfigCategory category) throws IOException {
category.fix();
writer.beginObject();
String val;
for (EntryInfo<?> entry : category.getEntries()) {
try {
entry.writeTo(writer, category.getTranslationPrefix());
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not write entry", e);
}
}
for (Map.Entry<String, ConfigCategory> entry : category.getCategories().entrySet()) {
if ((val = JfConfigSafe.TRANSLATION_SUPPLIER.apply(category.getTranslationPrefix() + entry.getKey() + ".title")) != null)
writer.comment(val);
writer.name(entry.getKey());
writeTo(writer, entry.getValue());
}
writer.endObject();
}
@Override
public ConfigBuilderImpl setLoadMethod(Consumer<ConfigInstance> load) {
checkBuilt();
this.load = load;
return this;
}
public ConfigBuilderImpl setWriteMethod(Consumer<ConfigInstance> write) {
checkBuilt();
this.write = write;
return this;
}
public ConfigBuilderImpl setPath(Path path) {
checkBuilt();
this.path = path;
return this;
}
@Override
public DslConfigInstance build() {
markBuilt();
built = new DslConfigInstance(id,
translationPrefix,
entries,
presets,
() -> referencedConfigs.stream().map(Supplier::get).toList(),
categories.stream().collect(Collectors.toMap(CategoryBuilder::getId, b -> b.build(() -> built))),
() -> built,
verifiers,
load,
write,
path);
built.load();
return built;
}
}

View File

@ -0,0 +1,48 @@
package io.gitlab.jfronny.libjf.config.impl.dsl;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.ConfigBuilder;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
public class DSLImpl implements DSL {
@Override
public ConfigInstance config(String configId, ConfigBuilder.ConfigBuilderFunction builder) {
return builder.apply(new ConfigBuilderImpl(configId)).build();
}
@Override
public ConfigInstance register(String configId, ConfigBuilder.ConfigBuilderFunction builder) {
return register(ConfigHolder.getInstance(), configId, builder);
}
@Override
public ConfigInstance register(ConfigHolder config, String configId, ConfigBuilder.ConfigBuilderFunction builder) {
ConfigInstance instance = config(configId, builder);
config.register(configId, instance);
return instance;
}
public static class Defaulted extends DSLImpl implements DSL.Defaulted {
public final String defaultId;
public Defaulted(String defaultId) {
this.defaultId = defaultId;
}
@Override
public ConfigInstance config(ConfigBuilder.ConfigBuilderFunction builder) {
return config(defaultId, builder);
}
@Override
public ConfigInstance register(ConfigBuilder.ConfigBuilderFunction builder) {
return register(defaultId, builder);
}
@Override
public ConfigInstance register(ConfigHolder config, ConfigBuilder.ConfigBuilderFunction builder) {
return register(config, defaultId, builder);
}
}
}

View File

@ -0,0 +1,89 @@
package io.gitlab.jfronny.libjf.config.impl.dsl;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class DslConfigCategory implements ConfigCategory {
private final String id;
private final String categoryPath;
private final String translationPrefix;
private final List<EntryInfo<?>> entries;
private final Map<String, Consumer<ConfigCategory>> presets;
private final Supplier<List<ConfigInstance>> referencedConfigs;
private final Map<String, ConfigCategory> categories;
private final Supplier<ConfigInstance> root;
private final List<Consumer<ConfigCategory>> verifiers;
public DslConfigCategory(String id,
String categoryPath,
String translationPrefix,
List<EntryInfo<?>> entries,
Map<String, Consumer<ConfigCategory>> presets,
Supplier<List<ConfigInstance>> referencedConfigs,
Map<String, ConfigCategory> categories,
Supplier<ConfigInstance> root,
List<Consumer<ConfigCategory>> verifiers) {
this.id = id;
this.categoryPath = categoryPath;
this.translationPrefix = translationPrefix;
this.entries = entries;
this.presets = presets;
this.referencedConfigs = referencedConfigs;
this.categories = categories;
this.root = root;
this.verifiers = verifiers;
}
@Override
public String getId() {
return id;
}
@Override
public String getCategoryPath() {
return categoryPath;
}
@Override
public String getTranslationPrefix() {
return translationPrefix;
}
@Override
public List<EntryInfo<?>> getEntries() {
return entries;
}
@Override
public Map<String, Runnable> getPresets() {
return presets.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey,
t -> () -> t.getValue().accept(this)
));
}
@Override
public List<ConfigInstance> getReferencedConfigs() {
return referencedConfigs.get();
}
@Override
public Map<String, ConfigCategory> getCategories() {
return categories;
}
@Override
public ConfigInstance getRoot() {
return root.get();
}
@Override
public void fix() {
ConfigCategory.super.fix();
for (Consumer<ConfigCategory> verifier : verifiers) verifier.accept(this);
}
}

View File

@ -0,0 +1,47 @@
package io.gitlab.jfronny.libjf.config.impl.dsl;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class DslConfigInstance extends DslConfigCategory implements ConfigInstance {
private final Consumer<ConfigInstance> load;
private final Consumer<ConfigInstance> write;
private final Path path;
public DslConfigInstance(String id,
String translationPrefix,
List<EntryInfo<?>> entries,
Map<String, Consumer<ConfigCategory>> presets,
Supplier<List<ConfigInstance>> referencedConfigs,
Map<String, ConfigCategory> categories,
Supplier<ConfigInstance> root,
List<Consumer<ConfigCategory>> verifiers,
Consumer<ConfigInstance> load,
Consumer<ConfigInstance> write,
@Nullable Path path) {
super(id, "", translationPrefix, entries, presets, referencedConfigs, categories, root, verifiers);
this.load = load;
this.write = write;
this.path = path;
}
@Override
public void load() {
load.accept(this);
}
@Override
public void write() {
write.accept(this);
}
@Override
public Optional<Path> getFilePath() {
return Optional.ofNullable(path);
}
}

View File

@ -0,0 +1,200 @@
package io.gitlab.jfronny.libjf.config.impl.dsl;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.commons.throwable.ThrowingConsumer;
import io.gitlab.jfronny.commons.throwable.ThrowingSupplier;
import io.gitlab.jfronny.gson.JsonElement;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.Entry;
import io.gitlab.jfronny.libjf.config.api.v1.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigSafe;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;
public class DslEntryInfo<T> implements EntryInfo<T> {
private final String name;
private final T defaultValue;
private final ThrowingSupplier<T, IllegalAccessException> get;
private final ThrowingConsumer<T, IllegalAccessException> set;
private final Type<T> type;
private final int width;
private final double minValue;
private final double maxValue;
public DslEntryInfo(String name,
T defaultValue,
ThrowingSupplier<T, IllegalAccessException> get,
ThrowingConsumer<T, IllegalAccessException> set,
Type<T> type,
int width,
double minValue,
double maxValue) {
this.name = name;
this.defaultValue = defaultValue;
this.get = get;
this.set = set;
this.type = type;
this.width = width;
this.minValue = minValue;
this.maxValue = maxValue;
}
public DslEntryInfo(String name,
T def,
ThrowingSupplier<T, IllegalAccessException> get,
ThrowingConsumer<T, IllegalAccessException> set,
Type<T> type) {
this(name, def, get, set, type, 100, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
public static DslEntryInfo<Object> ofField(Field field) {
Entry entry = field.getAnnotation(Entry.class);
Object defaultValue = null;
try {
defaultValue = field.get(null);
} catch (IllegalAccessException ignored) {
}
//noinspection unchecked,rawtypes
return new DslEntryInfo<Object>(
field.getName(),
defaultValue,
() -> field.get(null),
v -> field.set(null, v),
(Type) Type.ofClass(field.getGenericType()),
entry == null ? 100 : entry.width(),
entry == null ? Double.NEGATIVE_INFINITY : entry.min(),
entry == null ? Double.POSITIVE_INFINITY : entry.max()
);
}
@Override
public String getName() {
return name;
}
@Override
public T getDefault() {
return defaultValue;
}
@Override
public T getValue() throws IllegalAccessException {
return get.get();
}
@Override
public void setValue(T value) throws IllegalAccessException {
set.accept(value);
}
@Override
public Type<T> getValueType() {
return type;
}
@Override
public void fix() {
Object value;
try {
value = getValue();
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not read value", e);
return;
}
final Object valueOriginal = value;
if (value instanceof final Integer v) {
if (v < minValue) value = (int)minValue;
if (v > maxValue) value = (int)maxValue;
} else if (value instanceof final Float v) {
if (v < minValue) value = (float)minValue;
if (v > maxValue) value = (float)maxValue;
} else if (value instanceof final Double v) {
if (v < minValue) value = minValue;
if (v > maxValue) value = maxValue;
}
if (valueOriginal != value) {
try {
setUnchecked(value);
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not write value", e);
}
}
}
@Override
public void loadFromJson(JsonElement element) throws IllegalAccessException {
if (type.isBool()) {
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean()) {
setUnchecked(element.getAsBoolean());
}
} else if (type.isString()) {
if (element.isJsonPrimitive()) {
setUnchecked(element.getAsString());
}
} else if (type.isInt()) {
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
setUnchecked(element.getAsNumber().intValue());
}
} else if (type.isLong()) {
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
setUnchecked(element.getAsNumber().longValue());
}
} else if (type.isDouble()) {
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
setUnchecked(element.getAsNumber().doubleValue());
}
} else if (type.isFloat()) {
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
setUnchecked(element.getAsNumber().floatValue());
}
} else if (type.isEnum()) {
Type.TEnum<T> e = (Type.TEnum<T>) type;
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
setUnchecked(e.optionForString(element.getAsString()));
}
} else {
setValue(GsonHolder.getGson().fromJson(element, type.asClass()));
}
}
@SuppressWarnings("unchecked")
private void setUnchecked(Object object) throws IllegalAccessException {
if (object == null) return;
setValue((T) object);
}
@Override
public void writeTo(JsonWriter writer, String translationPrefix) throws IOException, IllegalAccessException {
T value = getValue();
String val;
if ((val = JfConfigSafe.TRANSLATION_SUPPLIER.apply(translationPrefix + getName() + ".tooltip")) != null) {
writer.comment(val);
}
if (type.isEnum()) {
writer.comment("Valid: [" + Arrays.stream(((Type.TEnum<T>)type).options()).map(Objects::toString).collect(Collectors.joining(", ")) + "]");
}
writer.name(name);
GsonHolder.getGson().toJson(value, Objects.requireNonNullElse(type.asClass(), String.class), writer);
}
@Override
public int getWidth() {
return width;
}
@Override
public double getMinValue() {
return minValue;
}
@Override
public double getMaxValue() {
return maxValue;
}
}

View File

@ -0,0 +1,31 @@
package io.gitlab.jfronny.libjf.config.impl.entrypoint;
import io.gitlab.jfronny.libjf.config.api.v1.JfCustomConfig;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.entrypoint.EntrypointContainer;
import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint;
import net.minecraft.util.Language;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
public class JfConfigSafe implements PreLaunchEntrypoint {
public static Function<String, String> TRANSLATION_SUPPLIER = s -> null;
public static final Set<String> REGISTERED_MODS = new HashSet<>();
@Override
public void onPreLaunch() {
for (EntrypointContainer<Object> custom : FabricLoader.getInstance().getEntrypointContainers(ConfigHolderImpl.MODULE_ID, Object.class)) {
if (!REGISTERED_MODS.contains(custom.getProvider().getMetadata().getId()) && custom.getEntrypoint() instanceof JfCustomConfig cfg) {
REGISTERED_MODS.add(custom.getProvider().getMetadata().getId());
cfg.register(DSL.create(custom.getProvider().getMetadata().getId()));
}
}
TRANSLATION_SUPPLIER = s -> {
String translated = Language.getInstance().get(s);
return translated.equals(s) ? null : translated;
};
}
}

View File

@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"id": "libjf-config-v0",
"id": "libjf-config-core-v1",
"name": "LibJF Config",
"version": "${version}",
"authors": ["JFronny"],
@ -11,20 +11,13 @@
"license": "MIT",
"environment": "*",
"entrypoints": {
"modmenu": ["io.gitlab.jfronny.libjf.config.impl.client.ModMenu"],
"client": ["io.gitlab.jfronny.libjf.config.impl.client.JfConfigClient"],
"libjf:preEarly": ["io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigUnsafe"],
"preLaunch": ["io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigSafe"],
"main": ["io.gitlab.jfronny.libjf.config.impl.JfConfigCommand"],
"libjf:coprocess": ["io.gitlab.jfronny.libjf.config.impl.JfConfigWatchService"]
},
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*",
"fabric-resource-loader-v0": "*",
"fabric-command-api-v2": "*",
"libjf-base": ">=${version}",
"libjf-unsafe-v0": ">=${version}"
"libjf-base": ">=${version}"
},
"custom": {
"modmenu": {

View File

@ -0,0 +1,63 @@
package io.gitlab.jfronny.libjf.config.test;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.*;
import java.util.LinkedList;
import java.util.List;
@JfConfig(referencedConfigs = {"Yes", "No", "Yes"})
public class TestConfigCustom implements JfCustomConfig {
// User-created
@Entry
public static Integer someValue = 10;
@Entry public static String someOther = "Yes";
@Category
public static class SomeCategory {
@Entry public static boolean someBool = true;
@Entry public static List<String> someObject = new LinkedList<>();
@Verifier
public static void exampleVerifier() {
}
@Preset
public static void examplePreset() {
}
// Generated
private static CategoryBuilder<?> libjf$config$root(CategoryBuilder<?> builder) {
return builder
.value("someBool", someBool, () -> SomeCategory.someBool, v -> SomeCategory.someBool = v)
.value(EntryInfo.ofField(SomeCategory.class, "someObject"))
.addVerifier(SomeCategory::exampleVerifier)
.addPreset("examplePreset", SomeCategory::examplePreset);
}
}
// Generated
@Override
public void register(DSL.Defaulted dsl) {
// Here to ensure the static initializer is called
}
static {
libjf$config$clinit();
}
private static void libjf$config$clinit() {
ConfigHolder.getInstance().migrateFiles("libjf-config-v1-testmod");
DSL.create("libjf-config-v1-testmod").config(TestConfigCustom::libjf$config$root);
}
private static ConfigBuilder<?> libjf$config$root(ConfigBuilder<?> builder) {
return builder
.referenceConfig("libjf-config-reflect-v0-testmod")
.value("someValue", someValue, -50, 50, () -> someValue, v -> someValue = v)
.value("someOther", someOther, () -> someOther, v -> someOther = v)
.category("someCategory", SomeCategory::libjf$config$root);
}
}

View File

@ -0,0 +1,18 @@
{
"schemaVersion": 1,
"id": "libjf-config-v1-testmod",
"version": "1.0",
"environment": "*",
"entrypoints": {
"libjf:config_custom": [
"io.gitlab.jfronny.libjf.config.test.TestConfigCustom$LibJF_Companion"
]
},
"custom": {
"libjf": {
"config": {
"referencedConfigs": ["libjf-web-v0"]
}
}
}
}

View File

@ -0,0 +1,10 @@
archivesBaseName = "libjf-config-legacy-shim"
dependencies {
api project(path: ":libjf-base", configuration: "dev")
api project(path: ":libjf-config-core-v1", configuration: "dev")
api project(path: ":libjf-config-commands-v1", configuration: "dev")
api project(path: ":libjf-config-reflect-v1", configuration: "dev")
api project(path: ":libjf-config-ui-tiny-v1", configuration: "dev")
api project(path: ":libjf-unsafe-v0", configuration: "dev")
}

View File

@ -4,6 +4,7 @@ import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.ClickableWidget;
@Deprecated
public interface WidgetFactory {
Widget build(int screenWidth, TextRenderer textRenderer, ButtonWidget done);

View File

@ -0,0 +1,9 @@
package io.gitlab.jfronny.libjf.config.api;
import java.lang.annotation.*;
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Category {
}

View File

@ -1,13 +1,14 @@
package io.gitlab.jfronny.libjf.config.api;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import io.gitlab.jfronny.libjf.config.impl.legacy.ConfigHolderImpl;
import java.nio.file.Path;
import java.util.Map;
@Deprecated
public interface ConfigHolder {
static ConfigHolder getInstance() {
return ConfigHolderImpl.INSTANCE;
return new ConfigHolderImpl(io.gitlab.jfronny.libjf.config.api.v1.ConfigHolder.getInstance());
}
void register(String modId, Class<?> config);
Map<String, ConfigInstance> getRegistered();

View File

@ -3,6 +3,7 @@ package io.gitlab.jfronny.libjf.config.api;
import java.util.List;
import java.util.Map;
@Deprecated
public interface ConfigInstance {
static ConfigInstance get(Class<?> configClass) {
return ConfigHolder.getInstance().get(configClass);
@ -28,4 +29,5 @@ public interface ConfigInstance {
entry.fix();
}
}
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.libjf.config.api;
import java.lang.annotation.*;
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Entry {
int width() default 100;
double min() default Double.NEGATIVE_INFINITY;
double max() default Double.POSITIVE_INFINITY;
}

View File

@ -5,6 +5,7 @@ import io.gitlab.jfronny.gson.stream.JsonWriter;
import java.io.IOException;
@Deprecated
public interface EntryInfo<T> {
/**
* @return Get the default value of this entry

View File

@ -1,4 +1,5 @@
package io.gitlab.jfronny.libjf.config.api;
@Deprecated
public interface JfConfig {
}

View File

@ -0,0 +1,9 @@
package io.gitlab.jfronny.libjf.config.api;
import java.lang.annotation.*;
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Preset {
}

View File

@ -0,0 +1,9 @@
package io.gitlab.jfronny.libjf.config.api;
import java.lang.annotation.*;
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Verifier {
}

View File

@ -0,0 +1,119 @@
package io.gitlab.jfronny.libjf.config.impl.legacy;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.*;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigCategory;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.CategoryBuilder;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import io.gitlab.jfronny.libjf.config.impl.AuxiliaryMetadata;
import io.gitlab.jfronny.libjf.config.impl.dsl.DslEntryInfo;
import io.gitlab.jfronny.libjf.config.impl.reflect.ReflectiveConfigBuilderImpl;
import net.fabricmc.loader.api.ModContainer;
import java.lang.reflect.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@Deprecated
public record ConfigHolderImpl(io.gitlab.jfronny.libjf.config.api.v1.ConfigHolder base) implements ConfigHolder {
private static final Map<Class<?>, io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance> klazzToInstance = new HashMap<>();
@Override
public void register(String modId, Class<?> config) {
AtomicReference<ModContainer> mc = new AtomicReference<>();
base.migrateFiles(modId);
klazzToInstance.put(config, DSL.create(modId).register(builder -> {
Optional.ofNullable(AuxiliaryMetadata.forMod(modId)).ifPresent(meta -> meta.applyTo(builder));
return applyCategory(builder, config);
}));
}
private <T extends CategoryBuilder<?>> T applyCategory(T builder, Class<?> klazz) {
for (Field field : klazz.getFields()) {
if (field.isAnnotationPresent(Entry.class)) {
Entry entry = field.getAnnotation(Entry.class);
Object defaultValue = null;
try {
defaultValue = field.get(null);
} catch (IllegalAccessException ignored) {
}
//noinspection unchecked,rawtypes
builder.value(new DslEntryInfo<Object>(
field.getName(),
defaultValue,
() -> field.get(null),
v -> field.set(null, v),
(Type) Type.ofClass(field.getGenericType()),
entry.width(),
entry.min(),
entry.max()
));
}
}
builder.addPreset(ReflectiveConfigBuilderImpl.CONFIG_PRESET_DEFAULT, ConfigCategory::reset);
for (Method method : klazz.getMethods()) {
if (method.isAnnotationPresent(Preset.class)) {
builder.addPreset(builder.getTranslationPrefix() + method.getName(), c -> {
try {
method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException e) {
LibJf.LOGGER.error("Could not apply preset", e);
}
});
} else if (method.isAnnotationPresent(Verifier.class)) {
builder.addVerifier(c -> {
try {
method.invoke(null);
} catch (IllegalAccessException | InvocationTargetException e) {
LibJf.LOGGER.error("Could not run verifier", e);
}
});
}
}
for (Class<?> category : klazz.getClasses()) {
if (category.isAnnotationPresent(Category.class)) {
String name = category.getSimpleName();
name = Character.toLowerCase(name.charAt(0)) + name.substring(1); // camelCase
builder.category(name, categoryBuilder -> applyCategory(categoryBuilder, category));
}
}
return builder;
}
@Override
public Map<String, ConfigInstance> getRegistered() {
return base.getRegistered().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, s -> ConfigInstanceImpl.of(s.getValue())));
}
@Override
public ConfigInstance get(Class<?> configClass) {
return ConfigInstanceImpl.of(klazzToInstance.get(configClass));
}
@Override
public ConfigInstance get(String modId) {
return ConfigInstanceImpl.of(base.get(modId));
}
@Override
public ConfigInstance get(Path configPath) {
return ConfigInstanceImpl.of(base.get(configPath));
}
@Override
public boolean isRegistered(Class<?> configClass) {
return klazzToInstance.containsKey(configClass);
}
@Override
public boolean isRegistered(String modId) {
return base.isRegistered(modId);
}
@Override
public boolean isRegistered(Path configPath) {
return base.isRegistered(configPath);
}
}

View File

@ -0,0 +1,72 @@
package io.gitlab.jfronny.libjf.config.impl.legacy;
import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import io.gitlab.jfronny.libjf.config.api.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigCategory;
import java.util.*;
import java.util.stream.Collectors;
@Deprecated
public record ConfigInstanceImpl(io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance root, ConfigCategory category) implements ConfigInstance {
public ConfigInstanceImpl(io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance instance) {
this(Objects.requireNonNull(instance), instance);
}
public static ConfigInstanceImpl of(io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance instance) {
return instance == null ? null : new ConfigInstanceImpl(instance);
}
@Override
public void load() {
root.load();
}
@Override
public void write() {
root.write();
}
@Override
public String getId() {
return category.getId();
}
@Override
public String getCategoryPath() {
return category.getCategoryPath();
}
@Override
public String getTranslationPrefix() {
return category.getTranslationPrefix();
}
@Override
public List<EntryInfo<?>> getEntries() {
return category.getEntries().stream().<EntryInfo<?>>map(EntryInfoImpl::new).toList();
}
@Override
public Map<String, Runnable> getPresets() {
return category.getPresets();
}
@Override
public List<ConfigInstance> getReferencedConfigs() {
return category.getReferencedConfigs().stream().<ConfigInstance>map(r -> new ConfigInstanceImpl(root, r)).toList();
}
@Override
public Map<String, ConfigInstance> getCategories() {
return category.getCategories().entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey,
s -> new ConfigInstanceImpl(root, s.getValue())
));
}
@Override
public void fix() {
category.fix();
}
}

View File

@ -0,0 +1,65 @@
package io.gitlab.jfronny.libjf.config.impl.legacy;
import io.gitlab.jfronny.gson.JsonElement;
import io.gitlab.jfronny.gson.stream.JsonWriter;
import io.gitlab.jfronny.libjf.config.api.EntryInfo;
import java.io.IOException;
@Deprecated
public record EntryInfoImpl<T>(io.gitlab.jfronny.libjf.config.api.v1.EntryInfo<T> base) implements EntryInfo {
@Override
public Object getDefault() {
return base.getDefault();
}
@Override
public Object getValue() throws IllegalAccessException {
return base.getValue();
}
@Override
public void setValue(Object value) throws IllegalAccessException {
base.setValue((T) value);
}
@Override
public Class<?> getValueType() {
return base.getValueType().asClass() instanceof Class<?> k ? k : null;
}
@Override
public void fix() {
base.fix();
}
@Override
public String getName() {
return base.getName();
}
@Override
public void loadFromJson(JsonElement element) throws IllegalAccessException {
base.loadFromJson(element);
}
@Override
public void writeTo(JsonWriter writer, String translationPrefix) throws IOException, IllegalAccessException {
base.writeTo(writer, translationPrefix);
}
@Override
public int getWidth() {
return base.getWidth();
}
@Override
public double getMinValue() {
return base.getMinValue();
}
@Override
public double getMaxValue() {
return base.getMaxValue();
}
}

View File

@ -0,0 +1,20 @@
package io.gitlab.jfronny.libjf.config.impl.legacy;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.JfConfig;
import io.gitlab.jfronny.libjf.config.api.v1.JfCustomConfig;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
import io.gitlab.jfronny.libjf.unsafe.DynamicEntry;
@Deprecated
public class LegacyRegistrationHook implements JfCustomConfig {
@Override
public void register(DSL.Defaulted dsl) {
DynamicEntry.execute("libjf:config", Object.class, s -> {
if (s.instance().getClass().isAnnotationPresent(io.gitlab.jfronny.libjf.config.api.v1.JfConfig.class)) return;
if (s.instance() instanceof JfCustomConfig) return;
if (!(s.instance() instanceof JfConfig)) return;
ConfigHolder.getInstance().register(s.modId(), s.instance().getClass());
});
}
}

View File

@ -0,0 +1,32 @@
{
"schemaVersion": 1,
"id": "libjf-config-legacy-shim",
"name": "LibJF Config Legacy Shim",
"version": "${version}",
"provides": ["libjf-config-v0"],
"authors": ["JFronny"],
"contact": {
"website": "https://jfronny.gitlab.io",
"repo": "https://gitlab.com/jfmods/libjf"
},
"license": "MIT",
"environment": "*",
"entrypoints": {
"libjf:config": [
"io.gitlab.jfronny.libjf.config.impl.legacy.LegacyRegistrationHook"
]
},
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*",
"libjf-config-core-v1": ">=${version}",
"libjf-config-reflect-v1": ">=${version}",
"libjf-unsafe-v0": ">=${version}"
},
"custom": {
"modmenu": {
"badges": ["library"],
"parent": "libjf"
}
}
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.test;
package io.gitlab.jfronny.libjf.config.test.legacy;
import io.gitlab.jfronny.commons.serialize.gson.api.Ignore;
import io.gitlab.jfronny.libjf.config.api.*;

View File

@ -0,0 +1,13 @@
{
"libjf-config-legacy-shim-testmod.jfconfig.title": "JfConfig example",
"libjf-config-legacy-shim-testmod.jfconfig.disablePacks": "Disable resource packs",
"libjf-config-legacy-shim-testmod.jfconfig.intTest": "Int Test",
"libjf-config-legacy-shim-testmod.jfconfig.decimalTest": "Decimal Test",
"libjf-config-legacy-shim-testmod.jfconfig.dieStr": "String Test",
"libjf-config-legacy-shim-testmod.jfconfig.gsonOnlyStr.tooltip": "George",
"libjf-config-legacy-shim-testmod.jfconfig.enumTest": "Enum Test",
"libjf-config-legacy-shim-testmod.jfconfig.enumTest.tooltip": "Enum Test Tooltip",
"libjf-config-legacy-shim-testmod.jfconfig.enum.Test.Test": "Test",
"libjf-config-legacy-shim-testmod.jfconfig.enum.Test.ER": "ER",
"libjf-config-legacy-shim-testmod.jfconfig.moskau": "Moskau"
}

View File

@ -1,13 +1,16 @@
{
"schemaVersion": 1,
"id": "libjf-config-v0-testmod",
"id": "libjf-config-legacy-shim-testmod",
"version": "1.0",
"environment": "*",
"entrypoints": {
"libjf:config": [
"io.gitlab.jfronny.libjf.config.test.TestConfig"
"io.gitlab.jfronny.libjf.config.test.legacy.TestConfig"
]
},
"depends": {
"libjf-config-v0": "*"
},
"custom": {
"libjf": {
"config": {
@ -15,4 +18,4 @@
}
}
}
}
}

View File

@ -0,0 +1,7 @@
archivesBaseName = "libjf-config-reflect-v1"
dependencies {
api project(path: ":libjf-base", configuration: "dev")
api project(path: ":libjf-unsafe-v0", configuration: "dev")
api project(path: ":libjf-config-core-v1", configuration: "dev")
}

View File

@ -0,0 +1,10 @@
package io.gitlab.jfronny.libjf.config.api.v1.reflect;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.ConfigBuilder;
import io.gitlab.jfronny.libjf.config.impl.reflect.ReflectiveConfigBuilderImpl;
public interface ReflectiveConfigBuilder extends ConfigBuilder.ConfigBuilderFunction {
static ReflectiveConfigBuilder of(String id, Class<?> klazz) {
return new ReflectiveConfigBuilderImpl(id, klazz);
}
}

View File

@ -0,0 +1,69 @@
package io.gitlab.jfronny.libjf.config.impl.reflect;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.CategoryBuilder;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.ConfigBuilder;
import io.gitlab.jfronny.libjf.config.api.v1.reflect.ReflectiveConfigBuilder;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import io.gitlab.jfronny.libjf.config.impl.AuxiliaryMetadata;
import io.gitlab.jfronny.libjf.config.impl.dsl.DslEntryInfo;
import java.lang.reflect.*;
import java.util.Objects;
public class ReflectiveConfigBuilderImpl implements ReflectiveConfigBuilder {
public static final String CONFIG_PRESET_DEFAULT = "libjf-config-v1.default";
private final AuxiliaryMetadata rootMeta;
private final Class<?> rootClass;
public ReflectiveConfigBuilderImpl(String id, Class<?> klazz) {
this.rootClass = Objects.requireNonNull(klazz);
this.rootMeta = AuxiliaryMetadata.of(klazz.getAnnotation(JfConfig.class))
.merge(AuxiliaryMetadata.forMod(id));
}
@Override
public ConfigBuilder<?> apply(ConfigBuilder<?> builder) {
return applyCategory(builder, rootClass, rootMeta);
}
private static <T extends CategoryBuilder<?>> T applyCategory(T builder, Class<?> configClass, AuxiliaryMetadata meta) {
meta.applyTo(builder);
for (Field field : configClass.getFields()) {
if (field.isAnnotationPresent(Entry.class)) {
builder.value(DslEntryInfo.ofField(field));
}
}
builder.addPreset(CONFIG_PRESET_DEFAULT, ConfigCategory::reset);
for (Method method : configClass.getMethods()) {
if (method.isAnnotationPresent(Preset.class)) {
builder.addPreset(builder.getTranslationPrefix() + method.getName(), c -> {
try {
method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException e) {
LibJf.LOGGER.error("Could not apply preset", e);
}
});
} else if (method.isAnnotationPresent(Verifier.class)) {
builder.addVerifier(c -> {
try {
method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException e) {
LibJf.LOGGER.error("Could not run verifier", e);
}
});
}
}
for (Class<?> categoryClass : configClass.getClasses()) {
if (categoryClass.isAnnotationPresent(Category.class)) {
String name = categoryClass.getSimpleName();
name = Character.toLowerCase(name.charAt(0)) + name.substring(1); // camelCase
builder.category(name, builder1 -> applyCategory(builder1, categoryClass, AuxiliaryMetadata.of(categoryClass.getAnnotation(Category.class))));
}
}
return builder;
}
}

View File

@ -0,0 +1,38 @@
package io.gitlab.jfronny.libjf.config.impl.reflect.entrypoint;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
import io.gitlab.jfronny.libjf.config.api.v1.reflect.ReflectiveConfigBuilder;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigSafe;
import io.gitlab.jfronny.libjf.config.impl.reflect.ReflectiveConfigBuilderImpl;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.entrypoint.EntrypointContainer;
import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint;
public class JfConfigReflectSafe implements PreLaunchEntrypoint {
@Override
public void onPreLaunch() {
for (EntrypointContainer<Object> config : FabricLoader.getInstance().getEntrypointContainers(ConfigHolderImpl.MODULE_ID, Object.class)) {
registerIfMissing(config.getProvider().getMetadata().getId(), config.getEntrypoint());
}
}
public static void registerIfMissing(String modId, Object config) {
if (!JfConfigSafe.REGISTERED_MODS.contains(modId)) {
JfConfigSafe.REGISTERED_MODS.add(modId);
ConfigHolder.getInstance().migrateFiles(modId);
if (config instanceof JfCustomConfig cfg) {
cfg.register(DSL.create(modId));
} else {
Class<?> klazz = config.getClass();
if (klazz.isAnnotationPresent(JfConfig.class)) {
DSL.create(modId).register(ReflectiveConfigBuilder.of(modId, klazz));
} else {
LibJf.LOGGER.error("Attempted to register improper config for mod " + modId + " (missing @JfConfig annotation or JfCustomConfig interface)");
}
}
}
}
}

View File

@ -0,0 +1,16 @@
package io.gitlab.jfronny.libjf.config.impl.reflect.entrypoint;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl;
import io.gitlab.jfronny.libjf.unsafe.DynamicEntry;
import io.gitlab.jfronny.libjf.unsafe.UltraEarlyInit;
public class JfConfigUnsafe implements UltraEarlyInit {
@Override
public void init() {
DynamicEntry.execute(ConfigHolderImpl.MODULE_ID, Object.class,
s -> JfConfigReflectSafe.registerIfMissing(s.modId(), s.instance())
);
LibJf.LOGGER.info("Finished LibJF config entrypoint");
}
}

View File

@ -0,0 +1,34 @@
{
"schemaVersion": 1,
"id": "libjf-config-reflect-v1",
"name": "LibJF Config Reflect",
"version": "${version}",
"authors": ["JFronny"],
"contact": {
"website": "https://jfronny.gitlab.io",
"repo": "https://gitlab.com/jfmods/libjf"
},
"license": "MIT",
"environment": "*",
"entrypoints": {
"libjf:preEarly": [
"io.gitlab.jfronny.libjf.config.impl.reflect.entrypoint.JfConfigUnsafe"
],
"preLaunch": [
"io.gitlab.jfronny.libjf.config.impl.reflect.entrypoint.JfConfigReflectSafe"
]
},
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*",
"libjf-base": ">=${version}",
"libjf-unsafe-v0": ">=${version}",
"libjf-config-core-v1": ">=${version}"
},
"custom": {
"modmenu": {
"badges": ["library"],
"parent": "libjf"
}
}
}

View File

@ -0,0 +1,58 @@
package io.gitlab.jfronny.libjf.config.test.reflect;
import io.gitlab.jfronny.commons.serialize.gson.api.Ignore;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import java.util.ArrayList;
import java.util.List;
@JfConfig(referencedConfigs = {"libjf-web-v0"})
public class TestConfig {
@Entry
public static boolean disablePacks = false;
@Entry public static Boolean disablePacks2 = false;
@Entry public static int intTest = 20;
@Entry(min = -6) public static float floatTest = -5;
@Entry(max = 21) public static double doubleTest = 20;
@Entry public static String dieStr = "lolz";
@Entry @Ignore
public static String guiOnlyStr = "lolz";
public static String gsonOnlyStr = "lolz";
@Entry public static Test enumTest = Test.Test;
@Entry public static List<String> stringList;
@Preset
public static void moskau() {
disablePacks = true;
disablePacks2 = true;
intTest = -5;
floatTest = -6;
doubleTest = 4;
dieStr = "Moskau";
}
@Verifier
public static void setIntTestIfDisable() {
if (disablePacks) intTest = 0;
}
@Verifier
public static void stringListVerifier() {
if (stringList == null) stringList = new ArrayList<>(List.of("Obama"));
}
public enum Test {
Test, ER
}
@Category
public static class Subcategory {
@Entry public static boolean boolInSub = false;
@Entry public static int intIbSub = 15;
@Category
public static class Inception {
@Entry public static Test yesEnum = Test.ER;
}
}
}

View File

@ -0,0 +1,13 @@
{
"libjf-config-reflect-v0-testmod.jfconfig.title": "JfConfig example",
"libjf-config-reflect-v0-testmod.jfconfig.disablePacks": "Disable resource packs",
"libjf-config-reflect-v0-testmod.jfconfig.intTest": "Int Test",
"libjf-config-reflect-v0-testmod.jfconfig.decimalTest": "Decimal Test",
"libjf-config-reflect-v0-testmod.jfconfig.dieStr": "String Test",
"libjf-config-reflect-v0-testmod.jfconfig.gsonOnlyStr.tooltip": "George",
"libjf-config-reflect-v0-testmod.jfconfig.enumTest": "Enum Test",
"libjf-config-reflect-v0-testmod.jfconfig.enumTest.tooltip": "Enum Test Tooltip",
"libjf-config-reflect-v0-testmod.jfconfig.enum.Test.Test": "Test",
"libjf-config-reflect-v0-testmod.jfconfig.enum.Test.ER": "ER",
"libjf-config-reflect-v0-testmod.jfconfig.moskau": "Moskau"
}

View File

@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"id": "libjf-config-reflect-v0-testmod",
"version": "1.0",
"environment": "*",
"entrypoints": {
"libjf:config": [
"io.gitlab.jfronny.libjf.config.test.reflect.TestConfig"
]
}
}

View File

@ -0,0 +1,8 @@
archivesBaseName = "libjf-config-ui-tiny-v1"
dependencies {
api project(path: ":libjf-base", configuration: "dev")
api project(path: ":libjf-config-core-v1", configuration: "dev")
include fabricApi.module("fabric-resource-loader-v0", "${project.fabric_version}")
modCompileOnly("com.terraformersmc:modmenu:4.0.5")
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.libjf.config.api.v1.ui.tiny;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.TinyConfigScreen;
import net.minecraft.client.gui.screen.Screen;
public interface ConfigScreen {
static Screen create(ConfigInstance config, Screen parent) {
return new TinyConfigScreen(config, parent);
}
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.libjf.config.api.v1.ui.tiny;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.ClickableWidget;
public interface WidgetFactory {
Widget build(int screenWidth, TextRenderer textRenderer, ButtonWidget done);
record Widget(Runnable updateControls, ClickableWidget control) {
}
}

View File

@ -1,16 +1,15 @@
package io.gitlab.jfronny.libjf.config.impl.client;
package io.gitlab.jfronny.libjf.config.impl.ui.tiny;
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
import com.terraformersmc.modmenu.api.ModMenuApi;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.impl.client.gui.TinyConfigScreen;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import java.util.HashMap;
import java.util.Map;
public class ModMenu implements ModMenuApi {
public class ModMenuAdapter implements ModMenuApi {
@Override
public Map<String, ConfigScreenFactory<?>> getProvidedConfigScreenFactories() {
Map<String, ConfigScreenFactory<?>> factories = new HashMap<>();

View File

@ -1,10 +1,13 @@
package io.gitlab.jfronny.libjf.config.impl.client.gui;
package io.gitlab.jfronny.libjf.config.impl.ui.tiny;
import io.gitlab.jfronny.commons.throwable.Try;
import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import io.gitlab.jfronny.libjf.config.api.WidgetFactory;
import io.gitlab.jfronny.libjf.config.impl.client.gui.presets.PresetsScreen;
import io.gitlab.jfronny.libjf.unsafe.SafeLog;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.ui.tiny.WidgetFactory;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigCategory;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.entry.EntryInfoWidgetBuilder;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.entry.EntryListWidget;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.presets.PresetsScreen;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
@ -19,7 +22,7 @@ import java.util.*;
@Environment(EnvType.CLIENT)
public class TinyConfigScreen extends Screen {
public TinyConfigScreen(ConfigInstance config, Screen parent) {
public TinyConfigScreen(ConfigCategory config, Screen parent) {
super(Text.translatable(config.getTranslationPrefix() + "title"));
this.parent = parent;
this.config = config;
@ -28,36 +31,40 @@ public class TinyConfigScreen extends Screen {
}
private final String translationPrefix;
private final Screen parent;
private final ConfigInstance config;
private final ConfigCategory config;
private final List<WidgetState<?>> widgets;
private MidnightConfigListWidget list;
private EntryListWidget list;
@Override
protected void init() {
super.init();
config.fix();
for (WidgetState<?> widget : widgets) {
widget.updateCache();
}
this.addDrawableChild(new ButtonWidget(4, 6, 80, 20, Text.translatable("libjf-config-v0.presets"), button -> {
MinecraftClient.getInstance().setScreen(new PresetsScreen(this, config));
}));
if (!config.getPresets().isEmpty()) {
this.addDrawableChild(new ButtonWidget(4, 6, 80, 20, Text.translatable("libjf-config-v1.presets"), button -> {
Objects.requireNonNull(client).setScreen(new PresetsScreen(this, config));
}));
}
this.addDrawableChild(new ButtonWidget(this.width / 2 - 154, this.height - 28, 150, 20, ScreenTexts.CANCEL, button -> {
config.load();
Objects.requireNonNull(client).setScreen(parent);
}));
ButtonWidget done = this.addDrawableChild(new ButtonWidget(this.width / 2 + 4, this.height - 28, 150, 20, ScreenTexts.DONE, (button) -> {
for (WidgetState<?> state : widgets) {
Try.orElse(state::writeToEntry, e -> SafeLog.error("Could not write config data to class", e));
Try.orElse(state::writeToEntry, e -> LibJf.LOGGER.error("Could not write config data to class", e));
}
config.write();
config.getRoot().write();
Objects.requireNonNull(client).setScreen(parent);
}));
this.list = new MidnightConfigListWidget(this.client, this.width, this.height, 32, this.height - 32, 25);
this.list = new EntryListWidget(this.client, this.width, this.height, 32, this.height - 32, 25);
this.addSelectableChild(this.list);
for (Map.Entry<String, ConfigInstance> entry : config.getCategories().entrySet()) {
for (Map.Entry<String, ConfigCategory> entry : config.getCategories().entrySet()) {
this.list.addReference(width / 2,
Text.translatable(entry.getValue().getTranslationPrefix() + "title"),
() -> new TinyConfigScreen(entry.getValue(), this));
@ -65,7 +72,7 @@ public class TinyConfigScreen extends Screen {
for (WidgetState<?> info : widgets) {
MutableText name = Text.translatable(translationPrefix + info.entry.getName());
WidgetFactory.Widget control = info.factory.build(width, textRenderer, done);
ButtonWidget resetButton = new ButtonWidget(width - 155, 0, 40, 20, Text.translatable("libjf-config-v0.reset"), (button -> {
ButtonWidget resetButton = new ButtonWidget(width - 155, 0, 40, 20, Text.translatable("libjf-config-v1.reset"), (button -> {
info.reset();
control.updateControls().run();
}));
@ -78,7 +85,7 @@ public class TinyConfigScreen extends Screen {
for (ConfigInstance ci : config.getReferencedConfigs()) {
if (ci != null) {
this.list.addReference(width / 2,
Text.translatable("libjf-config-v0.see-also", Text.translatable(ci.getTranslationPrefix() + "title")),
Text.translatable("libjf-config-v1.see-also", Text.translatable(ci.getTranslationPrefix() + "title")),
() -> new TinyConfigScreen(ci, this));
}
}
@ -116,6 +123,6 @@ public class TinyConfigScreen extends Screen {
@Override
public void close() {
MinecraftClient.getInstance().setScreen(parent);
Objects.requireNonNull(client).setScreen(parent);
}
}

View File

@ -1,9 +1,9 @@
package io.gitlab.jfronny.libjf.config.impl.client.gui;
package io.gitlab.jfronny.libjf.config.impl.ui.tiny;
import io.gitlab.jfronny.commons.tuple.Tuple;
import io.gitlab.jfronny.libjf.config.api.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.WidgetFactory;
import io.gitlab.jfronny.libjf.unsafe.SafeLog;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.v1.ui.tiny.WidgetFactory;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.text.Text;
@ -23,10 +23,14 @@ public class WidgetState<T> {
this.entry = entry;
this.knownStates = knownStates;
this.factory = factory;
updateCache();
}
public void updateCache() {
try {
updateCache(entry.getValue());
} catch (IllegalAccessException e) {
SafeLog.error("Could not create initial widget state cache", e);
LibJf.LOGGER.error("Could not set widget state cache to current value", e);
}
}

View File

@ -1,11 +1,13 @@
package io.gitlab.jfronny.libjf.config.impl.client.gui;
package io.gitlab.jfronny.libjf.config.impl.ui.tiny.entry;
import io.gitlab.jfronny.commons.throwable.Try;
import io.gitlab.jfronny.commons.tuple.Tuple;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import io.gitlab.jfronny.libjf.config.api.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.WidgetFactory;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigCategory;
import io.gitlab.jfronny.libjf.config.api.v1.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import io.gitlab.jfronny.libjf.config.api.v1.ui.tiny.WidgetFactory;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.WidgetState;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.widget.ButtonWidget;
@ -14,7 +16,6 @@ import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Pattern;
@ -24,38 +25,36 @@ public class EntryInfoWidgetBuilder {
private static final Pattern INTEGER_ONLY = Pattern.compile("(-?\\d*)");
private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)");
public static List<WidgetState<?>> buildWidgets(ConfigInstance config) {
WidgetState<?> state;
public static List<WidgetState<?>> buildWidgets(ConfigCategory config) {
List<WidgetState<?>> knownStates = new ArrayList<>();
for (EntryInfo<?> info : config.getEntries()) {
if ((state = initEntry(config, info, knownStates)) != null) {
knownStates.add(state);
}
knownStates.add(initEntry(config, info, knownStates));
}
return knownStates;
}
private static <T> WidgetState<T> initEntry(ConfigInstance config, EntryInfo<T> info, List<WidgetState<?>> knownStates) {
Class<T> type = info.getValueType();
private static <T> WidgetState<T> initEntry(ConfigCategory config, EntryInfo<T> info, List<WidgetState<?>> knownStates) {
Type<T> type = info.getValueType();
WidgetState<T> state = new WidgetState<>();
WidgetFactory factory;
if (type == int.class || type == Integer.class) factory = textField(info, state, INTEGER_ONLY, Integer::parseInt, true, info.getMinValue(), info.getMaxValue());
else if (type == float.class || type == Float.class) factory = textField(info, state, DECIMAL_ONLY, Float::parseFloat, false, info.getMinValue(), info.getMaxValue());
else if (type == double.class || type == Double.class) factory = textField(info, state, DECIMAL_ONLY, Double::parseDouble, false, info.getMinValue(), info.getMaxValue());
else if (type == String.class) factory = textField(info, state, null, String::length, true, Math.min(info.getMinValue(),0), Math.max(info.getMaxValue(),1));
else if (type == boolean.class || type == Boolean.class) {
if (type.isInt()) factory = textField(info, state, INTEGER_ONLY, Integer::parseInt, true, info.getMinValue(), info.getMaxValue());
else if (type.isLong()) factory = textField(info, state, INTEGER_ONLY, Long::parseLong, true, info.getMinValue(), info.getMaxValue());
else if (type.isFloat()) factory = textField(info, state, DECIMAL_ONLY, Float::parseFloat, false, info.getMinValue(), info.getMaxValue());
else if (type.isDouble()) factory = textField(info, state, DECIMAL_ONLY, Double::parseDouble, false, info.getMinValue(), info.getMaxValue());
else if (type.isString()) factory = textField(info, state, null, String::length, true, Math.min(info.getMinValue(),0), Math.max(info.getMaxValue(),1));
else if (type.isBool()) {
factory = toggle(info, state,
value -> !(Boolean) value,
value -> Text.literal((Boolean) value ? "True" : "False").formatted((Boolean) value ? Formatting.GREEN : Formatting.RED));
} else if (type.isEnum()) {
List<T> values = Arrays.asList(info.getValueType().getEnumConstants());
T[] values = ((Type.TEnum<T>)type).options();
factory = toggle(info, state, value -> {
int index = values.indexOf(value) + 1;
return values.get(index >= values.size() ? 0 : index);
}, value -> Text.translatable(config.getTranslationPrefix() + "enum." + type.getSimpleName() + "." + state.cachedValue));
int index = indexOf(values, value) + 1;
return values[index >= values.length ? 0 : index];
}, value -> Text.translatable(config.getTranslationPrefix() + "enum." + type.getName() + "." + state.cachedValue));
} else {
LibJf.LOGGER.error("Invalid entry type in " + info.getName() + ": " + type.getName());
LibJf.LOGGER.error("Unsupported entry type in " + info.getName() + ": " + type.getName() + " - not displaying config control");
factory = ((screenWidth, textRenderer, done) -> new WidgetFactory.Widget(() -> {}, new ButtonWidget(-10, 0, 0, 0, Text.of(""), null)));
}
@ -63,6 +62,13 @@ public class EntryInfoWidgetBuilder {
return state;
}
private static int indexOf(Object[] array, Object value) {
for (int i = 0; i < array.length; i++) {
if (array[i] == value) return i;
}
return -1;
}
private static <T> WidgetFactory toggle(EntryInfo<T> info, WidgetState<T> state, Function<Object, Object> increment, Function<T, Text> valueTextifier) {
return (screenWidth, textRenderer, done) -> {
ButtonWidget button = new ButtonWidget(screenWidth - 110, 0, info.getWidth(), 20, valueTextifier.apply(state.cachedValue), btn -> {

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.impl.client.gui;
package io.gitlab.jfronny.libjf.config.impl.ui.tiny.entry;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
@ -19,10 +19,10 @@ import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
@Environment(EnvType.CLIENT)
public class MidnightConfigListWidget extends ElementListWidget<MidnightConfigListWidget.ConfigListEntryAbstract> {
public class EntryListWidget extends ElementListWidget<EntryListWidget.ConfigEntry> {
TextRenderer textRenderer;
public MidnightConfigListWidget(MinecraftClient minecraftClient, int i, int j, int k, int l, int m) {
public EntryListWidget(MinecraftClient minecraftClient, int i, int j, int k, int l, int m) {
super(minecraftClient, i, j, k, l, m);
this.centerListVertically = false;
textRenderer = minecraftClient.textRenderer;
@ -38,7 +38,7 @@ public class MidnightConfigListWidget extends ElementListWidget<MidnightConfigLi
}
public void addReference(int centerX, Text text, Supplier<Screen> targetScreen) {
this.addEntry(new ConfigListReferenceEntry(centerX, text, targetScreen));
this.addEntry(new ConfigReferenceEntry(centerX, text, targetScreen));
}
@Override
@ -47,7 +47,7 @@ public class MidnightConfigListWidget extends ElementListWidget<MidnightConfigLi
}
public Optional<Text> getHoveredEntryTitle(double mouseY) {
for (ConfigListEntryAbstract abstractEntry : this.children()) {
for (ConfigEntry abstractEntry : this.children()) {
if (abstractEntry instanceof ConfigScreenEntry entry
&& entry.button.visible
&& mouseY >= entry.button.y && mouseY < entry.button.y + itemHeight) {
@ -58,14 +58,14 @@ public class MidnightConfigListWidget extends ElementListWidget<MidnightConfigLi
}
@Environment(EnvType.CLIENT)
public static abstract class ConfigListEntryAbstract extends Entry<ConfigListEntryAbstract> {
public static abstract class ConfigEntry extends Entry<ConfigEntry> {
}
@Environment(EnvType.CLIENT)
public static class ConfigListReferenceEntry extends ConfigListEntryAbstract {
public static class ConfigReferenceEntry extends ConfigEntry {
private final ClickableWidget button;
public ConfigListReferenceEntry(int centerX, Text text, Supplier<Screen> targetScreen) {
public ConfigReferenceEntry(int centerX, Text text, Supplier<Screen> targetScreen) {
this.button = new ButtonWidget(centerX - 154, 0, 308, 20, text, btn -> MinecraftClient.getInstance().setScreen(targetScreen.get()));
}
@ -87,7 +87,7 @@ public class MidnightConfigListWidget extends ElementListWidget<MidnightConfigLi
}
@Environment(EnvType.CLIENT)
public static class ConfigScreenEntry extends ConfigListEntryAbstract {
public static class ConfigScreenEntry extends ConfigEntry {
private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
public final ClickableWidget button;
private final BooleanSupplier resetVisible;

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.libjf.config.impl.client.gui.presets;
package io.gitlab.jfronny.libjf.config.impl.ui.tiny.presets;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.Element;

View File

@ -1,25 +1,25 @@
package io.gitlab.jfronny.libjf.config.impl.client.gui.presets;
package io.gitlab.jfronny.libjf.config.impl.ui.tiny.presets;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.ConfigInstance;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigCategory;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.*;
import net.minecraft.text.Text;
import java.util.Map;
import java.util.Objects;
@Environment(EnvType.CLIENT)
public class PresetsScreen extends Screen {
private final Screen parent;
private final ConfigInstance config;
private final ConfigCategory config;
private PresetListWidget list;
public PresetsScreen(Screen parent, ConfigInstance config) {
super(Text.translatable("libjf-config-v0.presets"));
public PresetsScreen(Screen parent, ConfigCategory config) {
super(Text.translatable("libjf-config-v1.presets"));
this.parent = parent;
this.config = config;
}
@ -35,7 +35,7 @@ public class PresetsScreen extends Screen {
LibJf.LOGGER.info("Preset selected: " + entry.getKey());
entry.getValue().run();
config.fix();
MinecraftClient.getInstance().setScreen(parent);
Objects.requireNonNull(client).setScreen(parent);
}));
}
this.addSelectableChild(this.list);
@ -43,7 +43,7 @@ public class PresetsScreen extends Screen {
@Override
public void close() {
MinecraftClient.getInstance().setScreen(parent);
Objects.requireNonNull(client).setScreen(parent);
}
@Override

View File

@ -0,0 +1,6 @@
{
"libjf-config-v1.presets": "Presets",
"libjf-config-v1.default": "Default",
"libjf-config-v1.see-also": "See also: %s",
"libjf-config-v1.reset": "Reset"
}

View File

@ -0,0 +1,29 @@
{
"schemaVersion": 1,
"id": "libjf-config-ui-tiny-v1",
"name": "LibJF Config UI: Tiny",
"version": "${version}",
"authors": ["JFronny"],
"contact": {
"website": "https://jfronny.gitlab.io",
"repo": "https://gitlab.com/jfmods/libjf"
},
"license": "MIT",
"environment": "client",
"entrypoints": {
"modmenu": ["io.gitlab.jfronny.libjf.config.impl.ui.tiny.ModMenuAdapter"]
},
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*",
"fabric-resource-loader-v0": "*",
"libjf-base": ">=${version}",
"libjf-config-core-v1": ">=${version}"
},
"custom": {
"modmenu": {
"badges": ["library"],
"parent": "libjf"
}
}
}

Some files were not shown because too many files have changed in this diff Show More