Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
88c9fe1
build: release 1.24.0 (build-logic 0.1.6)
THEROER Jul 9, 2026
ccc35ca
build: release 1.25.0 — per-Java publish coordinate + build-logic cle…
THEROER Jul 10, 2026
4fee515
feat(messaging): cross-server messaging module (release 1.26.0)
THEROER Jul 10, 2026
dd3c908
refactor(bootstrap): extract shared language wiring into core Languag…
THEROER Jul 10, 2026
d1d0fa6
refactor(bootstrap): migrate Fabric/NeoForge/Velocity/Bungee to Langu…
THEROER Jul 10, 2026
a0b0aa1
test(bootstrap): cover LanguageBootstrap presets and apply/close wiring
THEROER Jul 10, 2026
d9b5799
feat(bootstrap): add metadata-derived forPlugin overloads for proxies
THEROER Jul 10, 2026
6511dcb
refactor(logger): self-type LogBuilderCore to drop covariant override…
THEROER Jul 10, 2026
e4edbc5
feat(velocity): add typed Logger facade for parity with other platforms
THEROER Jul 10, 2026
1dcd62d
feat(bungee): add typed Logger facade for parity with other platforms
THEROER Jul 10, 2026
ef9c236
fix(bundles): adapt proxy bundles to Logger-typed bootstrap.logger()
THEROER Jul 10, 2026
50051e8
docs(readme): update quickstart for unified bootstrap API
THEROER Jul 10, 2026
9200f5e
feat(logger): expose setEscapePlaceholders on the Logger facade
THEROER Jul 10, 2026
e4701ff
docs(logger): richer class JavaDoc and drop doclint:missing suppressions
THEROER Jul 10, 2026
45706f2
fix(logger): clean up chat output — brand-only prefix, level shown by…
THEROER Jul 10, 2026
5c0e3a5
feat(fabric): add Redis messaging parity for mods
THEROER Jul 10, 2026
ade74f1
feat(neoforge): add Redis messaging parity for mods
THEROER Jul 10, 2026
2ad3cc5
fix(neoforge-bundle): ship messaging module and Jackson in the standa…
THEROER Jul 10, 2026
8f3b638
fix(release): smokeTest polls the java-suffixed coordinate, not a bar…
THEROER Jul 10, 2026
91a79f5
feat(release): add verifyReleaseConsistency local task
THEROER Jul 10, 2026
f0a9dd8
chore(build-logic): bump plugins to 0.1.8
THEROER Jul 10, 2026
4632091
refactor(build-logic): extract per-target Exec fan-out helper
THEROER Jul 10, 2026
2e6d656
feat(build-logic): release spec model + release{} DSL + settings wiring
THEROER Jul 10, 2026
3d8d499
feat(build-logic): local Maven release fan-out (releaseMavenAll)
THEROER Jul 10, 2026
3c79d34
feat(build-logic): local git tag/push task (releaseTag)
THEROER Jul 10, 2026
ff6028d
feat(build-logic): local Modrinth release (releaseModrinth)
THEROER Jul 10, 2026
30eb3d7
feat(build-logic): javadoc upload task (releaseJavadoc)
THEROER Jul 10, 2026
5978830
feat(build-logic): configurable release orchestrator
THEROER Jul 10, 2026
880bce2
feat(build-logic): resolve Modrinth token from property or env
THEROER Jul 10, 2026
c8ff6bf
chore(release): bump version to 1.27.0
THEROER Jul 10, 2026
f12366f
fix(build-logic): put fan-out child Gradle home under .gradle, not build
THEROER Jul 10, 2026
c821b4c
fix(build-logic): make release preflight idempotent for resume
THEROER Jul 10, 2026
38ae64f
feat(build-logic): idempotent publish via -Pskip_existing (resumable …
THEROER Jul 10, 2026
67dc93c
fix(build-logic): forward publish creds to fan-out children via env
THEROER Jul 10, 2026
6e0177d
fix(build-logic): Modrinth bundle build uses per-target platforms
THEROER Jul 10, 2026
31b32ba
fix(build-logic): tolerate 409 on javadoc upload (resumable)
THEROER Jul 10, 2026
8aaac10
fix(fabric-bundle): inline messaging module on all branches
THEROER Jul 10, 2026
dae0bb9
feat(build-logic): publish common modules bare, +java<N> only for bun…
THEROER Jul 10, 2026
39be032
feat(build-logic): gate releases to the release branch (default main)
THEROER Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,24 +103,32 @@ dependencies {

## Quickstart

Bootstrap helpers are the recommended platform entry points:
Bootstrap helpers are the recommended platform entry points. The name and data
directory are derived from the plugin/mod metadata, so the entry points are
consistent across platforms:

- Bukkit/Paper: `BukkitBootstrap.forPlugin(this)`
- BungeeCord: `BungeeBootstrap.forPlugin(this, "MyPlugin")`
- BungeeCord: `BungeeBootstrap.forPlugin(this)`
- Velocity: `VelocityBootstrap.forPlugin(proxy, this)`
- Fabric: `FabricBootstrap.forMod("mymod", () -> server)`
- Velocity: `VelocityBootstrap.forPlugin(proxy, this, "MyPlugin", dataDir)`
- NeoForge: manual wiring with `NeoForgePlatformProvider` +
`CommandRegistry.create(...)`
- NeoForge: `NeoForgeBootstrap.forMod("mymod", () -> server)`

Every builder exposes `.withRecommendedDefaults()` (the default, every language
and messages feature on) and `.minimal()` (opt out of the automatic language and
messages wiring when you manage localization yourself), so you configure intent
with one call instead of a wall of boolean toggles.

If your project has a shared `common` module, let the platform layer create the
runtime and pass `MagicRuntime` into the shared services instead of calling the
bootstrap helpers from common code.

`buildRuntime()` returns a managed `MagicRuntime` container with the platform,
config manager, logger, language manager, and optional command registry.
Enable diagnostics with `.enableDiagnostics()` and fetch the service from
`runtime.requireComponent(DiagnosticsService.class)` or the bootstrap runtime
result accessor.
config manager, logger, language manager, and optional command registry. On
every platform, `bootstrap.logger()` returns a typed `Logger` facade with
`info/warn/error/...` methods, player-typed overloads, and a fluent
`Logger#log()` builder. Enable diagnostics with `.enableDiagnostics()` and fetch
the service from `runtime.requireComponent(DiagnosticsService.class)` or the
bootstrap runtime result accessor.

See the full setup guide in the docs:
https://magicutils.theroer.dev/getting-started/quickstart/
Expand Down
26 changes: 26 additions & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
import java.net.HttpURLConnection
import java.net.URI

plugins {
kotlin("jvm") version "2.2.0"
Expand Down Expand Up @@ -44,6 +46,30 @@ publishing {
}
}

// -Pskip_existing makes publishing idempotent: skip a PublishToMavenRepository
// task whose POM is already in the (immutable) repo, so a resumed release does
// not 409 on the plugins that a prior run already uploaded. HEAD is cheap and
// runs at execution time (skip decided per task), not at configuration.
if (project.hasProperty("skip_existing") && project.hasProperty("publish_repo")) {
val repoBase = (project.property("publish_repo") as String).trimEnd('/')
tasks.withType<PublishToMavenRepository>().configureEach {
onlyIf {
val pub = publication as org.gradle.api.publish.maven.MavenPublication
val path = "${pub.groupId.replace('.', '/')}/${pub.artifactId}/${pub.version}/" +
"${pub.artifactId}-${pub.version}.pom"
val url = URI("$repoBase/$path").toURL()
val exists = runCatching {
(url.openConnection() as HttpURLConnection).run {
requestMethod = "HEAD"; connectTimeout = 10000; readTimeout = 10000
val code = responseCode; disconnect(); code == 200
}
}.getOrDefault(false)
if (exists) logger.lifecycle("skip_existing: ${pub.artifactId}:${pub.version} already published, skipping.")
!exists
}
}
}



dependencies {
Expand Down
2 changes: 1 addition & 1 deletion build-logic/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ neoForgeModdevVersion=2.0.139
# Coordinates for publishing the build-logic plugins as a reusable tool.
# Independent of the MagicUtils library version (root gradle.properties).
pluginsGroup=dev.ua.theroer.magicutils.build
pluginsVersion=0.1.5
pluginsVersion=0.1.8
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class MagicUtilsConsumerBukkitPlugin : Plugin<Project> {
project.pluginManager.apply("magicutils.target")

val target = project.extensions.getByType(MagicUtilsTargetExtension::class.java)
val consumer = project.magicUtilsConsumerExtension()

project.extensions.configure(JavaPluginExtension::class.java) { java ->
java.toolchain.languageVersion.set(JavaLanguageVersion.of(target.java.get()))
Expand All @@ -43,53 +42,11 @@ class MagicUtilsConsumerBukkitPlugin : Plugin<Project> {

project.exposeMagicUtilsTargetFacts(target)

// MagicUtils modules the consumer declared. Bukkit has no jar-in-jar, so
// embedMode maps onto the dependency configuration + the shadow jar:
// SHADED → api/implementation, so shadow relocates the modules into the
// fat jar (a self-contained plugin). This is the Bukkit default
// (AUTO resolves here), but see the SHADED note in EmbedMode:
// two shaded consumers on one server load two isolated copies —
// use EXTERNAL there.
// EXTERNAL → compileOnly, and dev/ua/theroer/magicutils is stripped from
// the shadow jar, so the built jar is thin and expects
// MagicUtils installed as a separate server plugin.
// JAR_IN_JAR → rejected by resolveEmbedMode (fail-fast; no JiJ on Bukkit).
// Resolved at afterEvaluate: the consumer sets embedMode in its DSL block,
// which runs after this plugin applies. Bukkit has no Loom early-observe of
// configurations, so a late `add` is safe (unlike the Fabric consumer).
project.afterEvaluate {
val embed = resolveEmbedMode(consumer.embedMode.get(), ConsumerLoader.BUKKIT)
val shaded = embed == EmbedMode.SHADED
val apiConfig = if (shaded) "api" else "compileOnly"
val implConfig = if (shaded) "implementation" else "compileOnly"
val base = consumer.magicutilsVersion.get()
consumer.apiModules.get().forEach { module ->
project.dependencies.add(apiConfig, magicUtilsModuleCoordinate(module, base, target))
}
consumer.implementationModules.get().forEach { module ->
project.dependencies.add(implConfig, magicUtilsModuleCoordinate(module, base, target))
}

// EXTERNAL: strip MagicUtils and its bundled libraries from the shadow
// jar, so this jar carries none of them and the standalone MagicUtils
// plugin provides the single copy at runtime. These reach the fat jar
// transitively via the common module (whose MagicUtils deps are `api`),
// so moving this module's deps to compileOnly alone does not remove
// them; the shadow exclude does. jackson is the config modules' only
// external dependency and the bundle plugin owns its own relocated
// copy — shipping a second here produced a ClassCastException under
// Paper's isolated classloaders, so it is excluded too.
if (!shaded) {
project.tasks.named("shadowJar", com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class.java)
.configure { shadow ->
// The `**/` prefix also catches multi-release copies under
// META-INF/versions/<n>/, which a root-anchored pattern
// (e.g. `com/fasterxml/...`) would leave behind.
shadow.exclude("**/dev/ua/theroer/magicutils/**")
shadow.exclude("**/com/fasterxml/jackson/**")
}
}
}
// MagicUtils modules per embedMode + the EXTERNAL shadow-strip. Bukkit and
// Velocity share this exactly (flat-classpath JVM plugins, no jar-in-jar),
// so it lives in one helper. SHADED is the Bukkit default (AUTO); EXTERNAL
// makes a thin jar that expects the standalone MagicUtils bundle beside it.
project.configureJvmConsumerEmbed(target, ConsumerLoader.BUKKIT)

// Local dev server (runPaper/runFolia), only when the consumer opted in
// with `magicutilsConsumer { devServer { ... } }`. The Folia runner is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ internal fun magicUtilsModuleCoordinate(
module: String,
version: String,
target: MagicUtilsTargetExtension,
): String = "dev.ua.theroer:$module:${target.publishedVersion(version)}"
): String = "dev.ua.theroer:$module:${target.publishedVersion(module, version)}"

/**
* Exposes the resolved target's facts as extra properties so consumer build
Expand All @@ -408,8 +408,9 @@ internal fun magicUtilsModuleCoordinate(
* - `magicutilsIsDeobfuscated` — Boolean
* - `magicutilsClassifier` — e.g. `mc26`
* - `magicutilsPublishedVersion` — the base `magicutils_version` with the
* target's `+<minecraft>` suffix (e.g. `1.22.0+26.2`), for the rare coordinate
* a script must build by hand (a classifier-less bundle jar).
* bundle's `+java<N>` suffix (e.g. `1.27.1+java25`), for the rare coordinate a
* script must build by hand (a classifier-less bundle jar). Bundles keep the
* suffix; plain modules are published bare, so this fact is bundle-shaped.
*/
internal fun Project.exposeMagicUtilsTargetFacts(target: MagicUtilsTargetExtension) {
val base = magicUtilsConsumerExtension().magicutilsVersion.get()
Expand All @@ -418,7 +419,9 @@ internal fun Project.exposeMagicUtilsTargetFacts(target: MagicUtilsTargetExtensi
extensions.extraProperties.set("magicutilsJavaVersion", target.java.get())
extensions.extraProperties.set("magicutilsIsDeobfuscated", target.isDeobfuscated)
extensions.extraProperties.set("magicutilsClassifier", target.mcClassifier)
extensions.extraProperties.set("magicutilsPublishedVersion", target.publishedVersion(base))
// This fact is consumed only for bundle jars, which retain the +java<N>
// coordinate; pass a bundle-shaped name so the discriminator keeps the suffix.
extensions.extraProperties.set("magicutilsPublishedVersion", target.publishedVersion("magicutils-bundle", base))
// Optional per-platform facts (not every targets.properties defines them).
target.neoforge.orNull?.let { extensions.extraProperties.set("magicutilsNeoforgeVersion", it) }
}
Expand Down Expand Up @@ -452,3 +455,51 @@ internal fun Project.addConsumerMagicUtilsModules(
register(apiConfiguration, consumer.apiModules)
register(implementationConfiguration, consumer.implementationModules)
}

/**
* Wires the consumer's MagicUtils modules onto a plain-JVM loader (Bukkit,
* Velocity) honouring [EmbedMode], and — for EXTERNAL — strips MagicUtils and its
* bundled jackson from the shadow jar so the standalone bundle owns the single
* runtime copy. Bukkit and Velocity are identical here (both are flat-classpath
* JVM plugins with no jar-in-jar), so this is the one place that logic lives;
* each consumer plugin just passes its [ConsumerLoader]. BungeeCord is always
* shaded (no EXTERNAL split) and uses [addConsumerMagicUtilsModules] instead.
*
* Runs in `afterEvaluate` because the consumer sets `embedMode` in its DSL block,
* which executes after the plugin applies; a plain JVM loader has no Loom
* early-observe of configurations, so a late `add` is safe.
*/
internal fun Project.configureJvmConsumerEmbed(
target: MagicUtilsTargetExtension,
loader: ConsumerLoader,
) {
val consumer = magicUtilsConsumerExtension()
afterEvaluate {
val shaded = resolveEmbedMode(consumer.embedMode.get(), loader) == EmbedMode.SHADED
val apiConfig = if (shaded) "api" else "compileOnly"
val implConfig = if (shaded) "implementation" else "compileOnly"
val base = consumer.magicutilsVersion.get()
consumer.apiModules.get().forEach { module ->
dependencies.add(apiConfig, magicUtilsModuleCoordinate(module, base, target))
}
consumer.implementationModules.get().forEach { module ->
dependencies.add(implConfig, magicUtilsModuleCoordinate(module, base, target))
}

// EXTERNAL: the modules reach the fat jar transitively via the common
// module (whose MagicUtils deps are `api`), so moving this module's own
// deps to compileOnly doesn't remove them — the shadow exclude does. The
// `**/` prefix also catches multi-release copies under
// META-INF/versions/<n>/. jackson is the config modules' only external
// dependency and the bundle ships its own relocated copy; a second one
// here clashes under the isolated plugin/proxy classloaders, so it goes
// too.
if (!shaded) {
tasks.named("shadowJar", com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class.java)
.configure { shadow ->
shadow.exclude("**/dev/ua/theroer/magicutils/**")
shadow.exclude("**/com/fasterxml/jackson/**")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class MagicUtilsConsumerFabricPlugin : Plugin<Project> {
// for compile/dev-runtime; on deobfuscated 26.x there is no remap, so the
// classifier-less jar is itself the fat, self-contained artifact used for
// both publish and dev runtime.
val version = target.publishedVersion(consumer.magicutilsVersion.get())
val version = target.publishedVersion("magicutils-fabric-bundle", consumer.magicutilsVersion.get())
val module = "dev.ua.theroer:magicutils-fabric-bundle:$version"
val shippedDep = module
val fatDep = if (target.isDeobfuscated) shippedDep else "$module:dev"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class MagicUtilsConsumerNeoForgePlugin : Plugin<Project> {
val embed = resolveEmbedMode(consumer.embedMode.get(), ConsumerLoader.NEOFORGE)
if (embed == EmbedMode.JAR_IN_JAR) {
val bundleCoordinate = project.provider {
val version = target.publishedVersion(consumer.magicutilsVersion.get())
val version = target.publishedVersion("magicutils-neoforge-bundle", consumer.magicutilsVersion.get())
project.dependencies.create("dev.ua.theroer:magicutils-neoforge-bundle:$version")
}
// moddev registers `jarJar` at apply time, but hook it via
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,44 +55,11 @@ class MagicUtilsConsumerVelocityPlugin : Plugin<Project> {

project.exposeMagicUtilsTargetFacts(target)

// MagicUtils modules the consumer declared, wired per embedMode. Velocity
// has no jar-in-jar, so this maps onto the dependency configuration + the
// shadow jar exactly like the Bukkit consumer. Resolved at afterEvaluate
// because the consumer sets embedMode in its DSL block, which runs after
// this plugin applies; Velocity has no Loom early-observe of
// configurations, so a late `add` is safe (unlike the Fabric consumer).
project.afterEvaluate {
val embed = resolveEmbedMode(consumer.embedMode.get(), ConsumerLoader.VELOCITY)
val shaded = embed == EmbedMode.SHADED
val apiConfig = if (shaded) "api" else "compileOnly"
val implConfig = if (shaded) "implementation" else "compileOnly"
val base = consumer.magicutilsVersion.get()
consumer.apiModules.get().forEach { module ->
project.dependencies.add(apiConfig, magicUtilsModuleCoordinate(module, base, target))
}
consumer.implementationModules.get().forEach { module ->
project.dependencies.add(implConfig, magicUtilsModuleCoordinate(module, base, target))
}

// EXTERNAL: strip MagicUtils and its bundled libraries from the shadow
// jar, so this jar carries none of them and the standalone
// velocity-bundle provides the single copy at runtime. They reach the
// fat jar transitively via the common module (whose MagicUtils deps are
// `api`), so moving this module's deps to compileOnly alone does not
// remove them; the shadow exclude does. jackson is the config modules'
// only external dependency and the bundle owns its own copy — shipping
// a second here clashes under the proxy classloader, so exclude it too.
if (!shaded) {
project.tasks.named("shadowJar", com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class.java)
.configure { shadow ->
// The `**/` prefix also catches multi-release copies under
// META-INF/versions/<n>/, which a root-anchored pattern
// would leave behind.
shadow.exclude("**/dev/ua/theroer/magicutils/**")
shadow.exclude("**/com/fasterxml/jackson/**")
}
}
}
// MagicUtils modules per embedMode + the EXTERNAL shadow-strip — shared
// with the Bukkit consumer (see configureJvmConsumerEmbed). Velocity's
// default is SHADED (self-contained proxy plugin); EXTERNAL yields a thin
// jar that expects the standalone velocity-bundle beside it on the proxy.
project.configureJvmConsumerEmbed(target, ConsumerLoader.VELOCITY)

project.afterEvaluate {
val spec = consumer.devServerSpec.orNull ?: return@afterEvaluate
Expand Down
Loading
Loading