About

LibJF is split into several modules, each of which provides separate functionality. This modularization is inspired by Fabric API. LibJF is only maintained for the latest version of minecraft and tries to provide a stable ABI during its lifespan but is open for breaking changes between versions.

Be aware that modules may depend on another and that those transitive dependencies must be JiJd separately. Modules may also require fabric-api to be present to work properly

Using LibJF

The recommended way to use LibJF is using the JfMods scripts, which include the LibJF maven repository and simplify building fabric mods. My own mods (including LibJF) use these.

Otherwise, you can add the repository as follows:

repositories {
    maven { url 'https://maven.frohnmeyer-wds.de/artifacts' }
}

and include LibJF modules like this:

dependencies {
    include modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v2:${project.libjf_version}")
    include modRuntimeOnly("io.gitlab.jfronny.libjf:libjf-config-ui-tiny:${project.libjf_version}")
    include("io.gitlab.jfronny.libjf:libjf-base:${project.libjf_version}")

    modLocalRuntime("io.gitlab.jfronny.libjf:libjf-config-reflect-v1:${project.libjf_version}")
    modLocalRuntime("io.gitlab.jfronny.libjf:libjf-devutil-v0:${project.libjf_version}")
}

For more information on specific modules, you can look at their own pages

Developing LibJF

To add a LibJF module, create a new directory for it and copy over a build.gradle file from elsewhere, Reference it in settings.gradle and develop it like any fabric mod. You should also create a testmod for it (look at the other modules for examples) JiJing and maven uploads are automatically managed by the JfMods scripts

libjf-base

libjf-base is a dependency of all other modules and provides common functionality between them. It has no dependencies. It includes:

  • a Gson strategy to ignore fields annotated with GsonHidden, ClientOnly and ServerOnly
  • LazySupplier, a supplier that caches the result of another supplier to which it delegates
  • ThrowingRunnable and ThrowingSupplier, counterparts of their default lambdas which allow exceptions
  • a "flags" system to allow dynamically enabling LibJF features through system properties and fabric.mod.json
  • a shared logger and Gson instance
  • the ResourcePath abstraction to describe locations of resources as simple strings
  • the CoProcess system used internally to control threads separate from the game thread
  • an HTTP abstraction from Inceptum

All of these are implemented in one reusable class (except the Gson strategy, whose annotation is separate), which should be simple to read

About

LibJF Config is a modular config library. Generally, using a combination of its modules is recommended. On the following pages, the modules of libjf-config are explained in more detail. If you just want to get started, a quick setup example for a client-side mod using the compiler plugin can be seen below:

build.gradle:

repositories {
    maven { url 'https://maven.frohnmeyer-wds.de/artifacts' }
}

dependencies {
    include modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v2:${project.libjf_version}")
    include modRuntimeOnly("io.gitlab.jfronny.libjf:libjf-config-ui-tiny:${project.libjf_version}")
    include("io.gitlab.jfronny.libjf:libjf-base:${project.libjf_version}")

    annotationProcessor("io.gitlab.jfronny.libjf:libjf-config-compiler-plugin-v2:${project.libjf_version}")
}

loom {
    mods {
        register(name, {
            sourceSet sourceSets.main
        })
    }
}

compileJava {
    options.compilerArgs << "-AmodId=" + archivesName // optionally fill in your mod id explicitly
}

fabric.mod.json:

{
  "entrypoints": {
    "libjf:config": ["some.package.JFC_YourConfigClass"] // The JFC_ prefix is for the config-compiler-plugin-v2-generated classes
  }
}

Config class:

@JfConfig
public class ConfigExample {
    @Entry public static boolean someEntry = true;
    
    static {
        JFC_ConfigExample.ensureInitialized();
    }
}

en_us.json:

{
  "modid.jfconfig.title": "Your Mod",
  "modid.jfconfig.someEntry": "Some Entry",
  "modid.jfconfig.someEntry.tooltip": "Some comment"
}

I also recommend adding ModMenu in order to more easily test your config in dev.

libjf-config-core-v2

The core module contains the abstractions and annotations used by other modules to interact with configs. It also contains the code for registering configs to mods, serialization and automatic reloads on change.

DSL

The DSL is used to create or register instances of ConfigInstance. You may obtain an instance via DSL.create()

ConfigHolder

You can use this class to obtain your registered config instance or those of other mods. To do so, use ConfigHolder.getInstance().get("modId") and ConfigHolder.getInstance().getRegistered()

Annotations

This module also provides the annotations used in other modules. Use them as follows:

@JfConfig // This class is a config. Also takes the optional parameter referencedConfigs to add a button to go to the config of another mod
class SomeConfig {
    @Entry // This field is an entry. Also takes the optional parameters min and max for clamping number values
    public static int someEntry = 15; // Any type can be used, but only int, long, float, double, boolean, their Boxes, Strings and enums are supported in ui-tiny
    
    @Preset // This method is a preset. A UI entry for it will be created. When it is pressed, this method will be called
    public static void somePreset() { // Must return void and take no arguments. The method name will be used as the preset name
        someEntry = 12; // You may modify the entries of the config here
    }
    
    @Verifier // This method is a verifier. It will be called whenever a config is changed or loaded.
    public static void someVerifier() {
        if (someEntry % 2 != 0) someEntry++; // You can enforce additional constraints on values
    }
    
    @Category // This is a category. Just like JfConfig, you may specify referencedConfigs
    public static class SomeCategory {
        // A category can contain anything supported by the main config class, including fields, presets, verifiers and subcategories
    }
}

Custom config registration

In order to register mods without the compiler plugin or runtime module, you can use a libjf:config entrypoint implementing JfCustomConfig. As a parameter, you will be provided with a defaulted DSL with which you must interact to do so.

Comments

For enums, the possible values will automatically be written to the config as comments. You can add additional comments for entries or categories by registering tooltips in your language file as follows:

{
  "<mod id>.jfconfig.<entry>.tooltip": "Some comment"
}

libjf-config-compiler-plugin-v2

This annotation processor provides finds classes annotated with @JfConfig and produces the necessary code and registration for them. Using this is the recommended way to use libjf-config. Please be aware that the restrictions of the reflection implementation in regard to class loading will also apply to configs using the plugin if it is loaded during runtime.

Please note that your fabric.mod.json must reference the generated class, not your original config. If you plan on accessing your config before onInitialize, it is recommended that you call .ensureInitialized() on your generated class from your config classes static initializer as seen in the example.

The code necessary for using this annotation processor is available here

libjf-config-commands

This serverside module provides commands for modifying configs using this library. If you are developing a serverside mod, you may wish to include it to enable easier configuration. The commands are available under /libjf config, using auto-complete is recommended.

libjf-config-ui-tiny

This module provides an automatically registered, TinyConfig-based UI for all mods using libjf-config. Embedding this is recommended when developing client-side mods. libjf-config-ui-tiny implements the config-core-provided ConfigScreenFactory, so you can ConfigScreenFactory.getInstance().create(config, parent).get() to obtain a screen for your config.

libjf-devutil

LibJF devutil is intended to be used as modLocalRuntime. It marks the running minecraft instance as a development instance (for example, this removes the need for eula.txt) and disables the UserApi (removing that Yggdrasil error message) It does not provide any useful functionality to end users

libjf-data-v0

libjf-data-v0 provides two additional tags for use in other mods or datapacks:

  • libjf:shulker_boxes_illegal prevents items from being placed in shulker boxes (intended for backpacks)
  • libjf:overpowered makes entities wearing four or more armor items with this tag invincible

libjf-data-manipulation-v0

libjf-data-manipulation-v0 provides code for modifying existing resources

RecipeUtil

RecipeUtil provides to methods to end users:

  • removeRecipe blocks minecraft from loading recipes using the specified tag
  • removeRecipeFor blocks minecraft from loading recipes producing the specified output. (Look at the log to find out which ones it blocks) removed for 1.19.4

UserResourceEvents

UserResourceEvents provides four events (CONTAINS, FIND_RESOURCE, OPEN, OPEN_ROOT) which get called every time the corresponding method is called on and ResourcePack. They allow modifying the resulting data. This is used in respackopts to manipulate resource loading. To temporarily disable these hooks, call the "disable" function, which invokes the lambda it is passed and disables the hooks while it is running

Examples

@Override
public void onInitialize() {
    // This should prevent resource packs from doing anything if my hooks are working and
    UserResourceEvents.OPEN.register((type, id, previous, pack) -> {
        if (pack instanceof DirectoryResourcePack) {
            LibJf.LOGGER.info(pack.getName() + " opened " + type.name() + "/" + id.toString());
        }
        return previous.get();
    });
    UserResourceEvents.CONTAINS.register((type, id, previous, pack) -> {
        if (pack instanceof DirectoryResourcePack) {
            return false;
        }
        return previous.get();
    });
    RecipeUtil.removeRecipeFor(Items.DIAMOND_SWORD);
}

libjf-translate-v1

libjf-translate-v1 provides a utility class for translating strings through user-configurable services.

To use this, first obtain a TranslateService instance. You can use TranslateService.getConfigured() to do so. Please be aware that due to the nature of java generics, using var instead of a specific type for instances is recommended. You can also directly access implementations, however, this is not recommended and is not subject to the API stability promise. The TranslateService interface exposes all relevant functionality.

libjf-unsafe-v0

libjf-unsafe-v0 provides an entrypoint which is called extremely early and ASM modification. It also ensures dependency libraries are on the classpath during both. Due to the fact that it accesses several internal components of fabric loader, it is probably incompatible with versions of fabric loader it was not explicitly developed for.

UltraEarlyInit

Implement UltraEarlyInit on your entrypoint and add it as a libjf:early entrypoint Please be aware that using any non-jdk classes during the ultra early init stage prevents them from being modified by Mixin/ASM.

ASM

Implement AsmConfig on a class and add it to your fabric.mod.json as a libjf:asm entrypoint. Use skipClasses to prevent a class from being modified by any ASM patches. getPatches should return a Set of patches that should be applied to classes (more on that later). skipClasses and getPatches will only be called once during the initialization process. Please be aware that using un-excluded classes during ASM modification can cause infinite loops. Please also note that during runtime, class names may be in intermediary, yarn or other mappings. You can use AsmTransformer.MAPPING_RESOLVER to resolve intermediary names to their runtime counterparts

Creating an ASM patch

The current ClassNode will be passed to the apply method of every registered Patch instance. You may modify it as you like, but some default things are provided to make this easier:

  • InterfaceImplTargetPatch will apply a Patch only on classes implementing or extending the interface or class specified as the first parameter
  • MethodModificationPatch allows applying MethodPatches only to specific methods on specific classes
  • MethodReplacementPatch is a MethodPatch which replaces the content of its target method with a predefined InsnList

Of course, you can (and should) implement your own patches in addition to these, Examples can be found in libjf-data-manipulation-v0 and GWWHIT

Debugging ASM

Debugging ASM is difficult, but you can use the asm.log/asm.export flags to get additional output. You can do so either by adding -Plibjf.asm.log -Plibjf.asm.export to your JVM args or the following to your fabric.mod.json:

{
  "custom": {
    "libjf": {
      "asm.log": true,
      "asm.export": true
    }
  }
}

I also recommend using IDEAs bytecode viewer on resulting classes to understand the bytecode

libjf-web-v1

libjf-web-v1 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-v1 depends on libjf-config-core-v2 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:

{
  "custom": {
    "libjf": {
      "web": true
    }
  }
}

Please be aware that your entrypoint may be called multiple times during one session. You can register content providers using the register* methods on the WebServer parameter of your entrypoint. Please do not store it in a field, you may instead use WebServer.getInstance() if you wish to access it elsewhere.