Skip to content

Repository files navigation

XpressLib

Shared config + lang plumbing for the XpressDev Minecraft mods, extracted from the copy-pasted config packages in CatchCombo and BBottleCaps.

It is a plain Kotlin/JVM library — no Minecraft, no Fabric Loader. gson, slf4j and Adventure are all compileOnly, because every Fabric mod already ships them; bundling them here would put a second copy on the classpath. Anything needing Minecraft types (Text, sending to a player) stays in the mod.

What it replaces

Copy-pasted class Replaced by
config/JsonFiles.kt JsonFiles
config/Lang.kt Lang
config/Message.kt (the DEFAULTS boilerplate) XpressMessage + messageDefaults<T>()
config/ConfigStore.kt (load / rewrite-if-changed / @Volatile fields) ConfigManager, ConfigHandle, LangHandle
The private JsonObject.int/double/bool/obj/array/primitive helpers in every ConfigCodec net.xpressdev.xpresslib.json.*
ColorUtil / MMUtils MiniMessage parsing, MessageService / CapMessages MiniMessages, Messenger

What stays in the mod: the config data classes and their JsonCodec (the keys and defaults are mod-specific), and the Component -> Text conversion, which needs a live server.

Using it from a mod

Via JitPack

Tag a release (git tag v1.1.1 && git push origin v1.1.1), then in the mod's build.gradle.kts:

repositories {
    maven("https://jitpack.io")
    mavenCentral()
}

dependencies {
    implementation("com.github.XpressDevelopment:XpressLib:v1.1.1")
    // Jar-in-Jar. Loom wraps a plain library jar in generated mod metadata for you.
    // isTransitive = false keeps kotlin-stdlib out — fabric-language-kotlin already provides it.
    include("com.github.XpressDevelopment:XpressLib:v1.1.1") { isTransitive = false }
}

The first resolve triggers the build on JitPack and takes a minute; check https://jitpack.io/#XpressDevelopment/XpressLib for the log if it fails.

jitpack.yml pins JDK 21 (JitPack's default is older). group and version in the build script only apply when they were not passed in, because JitPack builds with -Pgroup=com.github.<owner> -Pversion=<tag> and then looks for artifacts under exactly those coordinates. The publication's artifactId is rootProject.name, which matches the repo name — that is what com.github.XpressDevelopment:XpressLib resolves against. To reproduce a JitPack build locally:

./gradlew -Pgroup=com.github.XpressDevelopment -Pversion=v1.1.1 clean build publishToMavenLocal

The repo is public, so JitPack needs no authorization — it just clones and builds. Were it ever made private, that would need a paid JitPack plan for the org plus an auth token as the repository username.

Two things the build image forces:

  • jitpack.yml pins openjdk21; the default image JDK is older than the toolchain.
  • settings.gradle.kts must not use the foojay-resolver plugin. The image cannot reach plugins-artifacts.gradle.org, so resolving it fails the build during settings evaluation, before any of the build script runs. Nothing needs it — the JDK comes from jitpack.yml on CI and from the installed JDK locally.

Via mavenLocal

./gradlew publishToMavenLocal
repositories { mavenLocal() }

dependencies {
    implementation("net.xpressdev:XpressLib:1.1.1")
    include("net.xpressdev:XpressLib:1.1.1") { isTransitive = false }
}

API

Messages

enum class Message(override val key: String, override val default: String) : XpressMessage {
    COMBO_CLEARED("info.combo_reset", "<red>Your catch combo has reset!"),
    COMBO_UPDATE("info.catch_combo_update", "<red>{pokemon} <gold>Catch Combo: <red>{combo}");
}

That is the whole enum — the companion object { val DEFAULTS = ... } is gone; ConfigManager.lang takes Message.entries directly.

Config store

object ConfigStore {

    private val configs = ConfigManager(
        FabricLoader.getInstance().configDir.resolve("catchcombo"),
        CatchComboMod.LOGGER,
    )

    private val configFile = configs.config("config.json", ConfigCodec, ::CatchComboConfig)
    private val langFile = configs.lang(Message.entries)

    // Only touched when the owner opted into Mongo, so it is not part of configs.load().
    private val mongoFile = configs.config("mongo_config.json", MongoConfigCodec, ::MongoSettings, autoLoad = false)

    val config: CatchComboConfig get() = configFile.value
    val lang: Lang get() = langFile.value
    val mongo: MongoSettings get() = mongoFile.value

    fun load() {
        configs.load()
        if (config.storage.useMongo) mongoFile.load()
    }

    fun disableMongoPort() {
        mongoFile.update { it.copy(portExistingDataToMongo = false) }
    }
}

ConfigHandle.load() keeps the behaviour both mods already had: read the file, decode with the codec (falling back to defaults if it is missing or malformed), re-encode, and rewrite the file when it does not match the canonical form. That is what adds keys introduced by an update and migrates legacy keys in place. LangHandle.load() does the same for lang, with defaults first and stored values winning, so an owner's edits and extra keys survive.

Calling configs.load() again reloads everything — that is the /reload path.

Codecs

A codec is just decode + encode; the readers below are lenient by design, so a hand-edited file with one bad value logs a warning and falls back to that field's default rather than failing the load.

object ConfigCodec : JsonCodec<BottleCapsConfig> {

    override fun decode(json: JsonObject) = BottleCapsConfig(
        levelRequired = json.int("levelRequired", LOGGER) ?: BottleCapsConfig.DEFAULT_LEVEL_REQUIRED,
        blacklistedSpecies = json.stringSet("blacklistedSpecies", LOGGER),
        caps = BottleCapType.entries.associateWith { decodeCap(json, it) },
    )

    override fun encode(value: BottleCapsConfig) = JsonObject().apply {
        addProperty("levelRequired", value.levelRequired)
        add("blacklistedSpecies", jsonArrayOf(value.blacklistedSpecies))
        add("caps") {
            BottleCapType.entries.forEach { cap -> add(cap.name.lowercase()) { /* ... */ } }
        }
    }
}

Readers in net.xpressdev.xpresslib.json:

  • obj, array, string, int, long, double, bool, primitive — null when absent or unusable; pass a Logger to have unusable values reported.
  • stringIn, intIn, longIn, doubleIn, boolIn, stringSetIn — read from a nested section and fall back to the root, which is how CatchCombo still reads files written before the config grew sections: json.boolIn(combo, "chainReset", defaults.chainReset, LOGGER).
  • stringSet — a list of strings as an ordered set, trimmed and lowercased by default, non-text entries dropped with a warning.
  • doubleMap — an object of name -> number pairs (CatchCombo's otherAspects).
  • jsonArrayOf, jsonObjectOf, and add(key) { ... } for building the encoded side.

Lang

val text: String = ConfigStore.lang.format(Message.COMBO_UPDATE, "pokemon" to name, "combo" to count.toString())

{placeholder} substitution only; the string comes back raw. A key missing from the file falls back to the enum default and is warned about once. Most callers want Messenger below rather than this directly.

Text

MiniMessages.parse takes config text to an Adventure Component: MiniMessage, plus the legacy codes owners keep typing (&c, §c, &#RRGGBB), plus italic switched off unless asked for.

Messenger chains that onto a lang lookup, so a message goes from key to platform text in one call:

object MessageService {
    private val messages = Messenger(ConfigStore::lang, ColorUtil::toText)

    fun send(player: ServerPlayerEntity, message: Message, vararg placeholders: Pair<String, String>) {
        player.sendMessage(messages.render(message, *placeholders))
    }
}

The toNative lambda is the only platform-specific part — ColorUtil::toText in CatchCombo, { adventure!!.toNative(it) } in BBottleCaps. It is supplied rather than done in the library because Component -> Text needs a RegistryManager or FabricServerAudiences, i.e. a running server; that is also why each mod still captures the server in a SERVER_STARTED handler.

lang is passed as a supplier (ConfigStore::lang, not ConfigStore.lang) so a reload that swaps the loaded Lang is picked up without rebuilding the messenger.

Adventure is compiled against 4.14.0, the lower of the versions the mods resolve, since its API is a subset of 4.17.0's.

Building

./gradlew build            # compile + tests
./gradlew publishToMavenLocal

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages