Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ repositories {
}

dependencies {
testImplementation(libs.aether.api)
testImplementation(libs.snakeyaml)
testImplementation(libs.paper.api)
testImplementation(libs.junit4)
testImplementation(libs.sqlite)
testImplementation(libs.h2)
Expand Down
41 changes: 41 additions & 0 deletions docs/runtime-libraries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Runtime library loading

The Paper `SyntaxLoader` uses the same manifest structure as PunisherX:
`repositories` maps repository IDs to URLs, and `libraries` contains either
`group:artifact:version` strings or mappings with `coordinate` and `repository`.
Gradle still expands versions from the existing build configuration.

`central: "MAVEN_CENTRAL_DEFAULT_MIRROR"` is resolved by the loader through
Paper's `MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR`, including the server's
configured mirror. Do not use the obsolete Paper `/repository/maven-central/` URL.

Libraries without an explicit repository use Central, except SyntaxDevTeam
groups (`pl.syntaxdevteam` and its subgroups), which use the snapshots or releases
repository according to their version. An explicit `repository` always wins.
Each selected repository gets its own resolver with Central available for
transitive dependencies. Unknown repository IDs, invalid entries, missing manifests and
unexpanded placeholders stop bootstrap with a descriptive error.

Example:

```yaml
repositories:
central: "MAVEN_CENTRAL_DEFAULT_MIRROR"
syntaxdevteam-snapshots: "https://nexus.syntaxdevteam.pl/repository/maven-snapshots/"
syntaxdevteam-releases: "https://nexus.syntaxdevteam.pl/repository/maven-releases/"
custom: "https://example.com/maven/"
libraries:
- com.github.ben-manes.caffeine:caffeine:3.2.4
- coordinate: com.example:library:1.0.0
repository: custom
```

This change preserves library versions and the Paper/Folia plugin lifecycle.
It only changes dependency bootstrap and manifest validation.

## Verification — 2026-10-11

`test build` passed (240 tests, including seven loader regression tests;
no failures or errors). The packaged Paper JAR contains the expanded manifest
with repository assignments and no obsolete Central URL or unresolved versions.
A live Paper/Folia server start was not repeated.
111 changes: 83 additions & 28 deletions src/main/kotlin/pl/syntaxdevteam/plotsx/loader/SyntaxLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,101 @@ import org.eclipse.aether.artifact.DefaultArtifact
import org.eclipse.aether.graph.Dependency
import org.eclipse.aether.repository.RemoteRepository
import org.yaml.snakeyaml.Yaml
import java.io.IOException
import java.io.InputStream

/** Reads repository assignments from the same runtime manifest format as PunisherX. */
@Suppress("UnstableApiUsage", "unused")
class SyntaxLoader : PluginLoader {

override fun classloader(pluginClasspath: PluginClasspathBuilder) {
val resolver = MavenLibraryResolver()

resolveLibraries().map { DefaultArtifact(it) }
.forEach { artifact -> resolver.addDependency(Dependency(artifact, null)) }

resolver.addRepository(RemoteRepository.Builder("syntaxdevteam-snapshots", "default", "https://nexus.syntaxdevteam.pl/repository/maven-snapshots/").build())
resolver.addRepository(RemoteRepository.Builder("syntaxdevteam-releases", "default", "https://nexus.syntaxdevteam.pl/repository/maven-releases/").build())
resolver.addRepository(RemoteRepository.Builder("paper", "default", "https://repo.papermc.io/repository/maven-public/").build())
resolver.addRepository(RemoteRepository.Builder("central-mirror", "default", "https://repo.papermc.io/repository/maven-central/").build())
resolver.addRepository(RemoteRepository.Builder("jitpack", "default", "https://jitpack.io/").build())
resolver.addRepository(RemoteRepository.Builder("codemc-snapshots", "default", "https://repo.codemc.io/repository/maven-snapshots/").build())
pluginClasspath.addLibrary(resolver)
val config = try {
val input = SyntaxLoader::class.java.classLoader.getResourceAsStream("paper-libraries.yml")
?: error("paper-libraries.yml not found in plugin JAR")
input.use(::readLibraryConfig)
} catch (failure: Exception) {
throw IllegalStateException("Cannot configure PlotsX runtime libraries from paper-libraries.yml", failure)
}
val resolvers = linkedMapOf<String, MavenLibraryResolver>()
for (library in config.libraries) {
val repositoryId = selectRepository(library, config)
val resolver = resolvers.getOrPut(repositoryId) {
MavenLibraryResolver().apply {
repositoriesFor(repositoryId, config).forEach(::addRepository)
}
}
resolver.addDependency(Dependency(DefaultArtifact(library.coordinate), null))
}
resolvers.values.forEach(pluginClasspath::addLibrary)
}

private fun resolveLibraries(): List<String> {
return try {
readLibraryListFromYaml()
} catch (e: IOException) {
e.printStackTrace()
emptyList()
internal fun readLibraryConfig(input: InputStream): LibraryConfig {
val document = Yaml().load<Any?>(input) as? Map<*, *>
?: error("paper-libraries.yml must contain a mapping")
val repositoryData = document["repositories"] as? Map<*, *>
?: error("paper-libraries.yml must define repositories")
val repositories = linkedMapOf<String, RemoteRepository>()
for ((key, value) in repositoryData) {
val id = (key as? String)?.trim().orEmpty()
val configuredUrl = (value as? String)?.trim().orEmpty()
require(id.isNotEmpty() && configuredUrl.isNotEmpty()) { "Invalid repository entry: $key" }
val url = if (configuredUrl == "MAVEN_CENTRAL_DEFAULT_MIRROR") {
MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR
} else {
configuredUrl
}
require(url.startsWith("https://") || url.startsWith("http://")) { "Invalid repository URL for $id" }
repositories[id] = RemoteRepository.Builder(id, "default", url).build()
}
require(repositories.containsKey("central")) { "Missing central repository in paper-libraries.yml" }
val libraryData = document["libraries"] as? List<*>
?: error("paper-libraries.yml must define libraries")
require(libraryData.isNotEmpty()) { "No runtime libraries defined in paper-libraries.yml" }
val libraries = libraryData.mapIndexed { index, entry ->
val library = when (entry) {
is String -> LibrarySpec(entry.trim())
is Map<*, *> -> {
val coordinate = entry["coordinate"] as? String
?: error("Missing coordinate in library entry ${index + 1}")
val repository = entry["repository"]
require(repository == null || repository is String) { "Invalid repository in library entry ${index + 1}" }
LibrarySpec(coordinate.trim(), repository?.trim())
}
else -> error("Invalid library entry ${index + 1}")
}
require(library.coordinate.isNotEmpty() && !library.coordinate.contains("\${")) {
"Invalid or unexpanded library coordinate: ${library.coordinate}"
}
DefaultArtifact(library.coordinate)
library
}
return LibraryConfig(repositories, libraries).also { config ->
config.libraries.forEach { selectRepository(it, config) }
}
}

private fun readLibraryListFromYaml(): List<String> {
val yaml = Yaml()
val inputStream: InputStream? = SyntaxLoader::class.java.classLoader.getResourceAsStream("paper-libraries.yml")

if (inputStream == null) {
System.err.println("paper-libraries.yml not found in the classpath.")
return emptyList()
internal fun selectRepository(library: LibrarySpec, config: LibraryConfig): String {
val repositoryId = library.repository ?: when {
library.coordinate.substringBefore(':').let { it == "pl.syntaxdevteam" || it.startsWith("pl.syntaxdevteam.") } ->
if (library.coordinate.endsWith("-SNAPSHOT")) "syntaxdevteam-snapshots" else "syntaxdevteam-releases"
else -> "central"
}
require(config.repositories.containsKey(repositoryId)) {
"Unknown repository '$repositoryId' for ${library.coordinate}"
}
return repositoryId
}

val data: Map<String, List<String>> = yaml.load(inputStream)
return data["libraries"] ?: emptyList()
internal fun repositoriesFor(repositoryId: String, config: LibraryConfig): List<RemoteRepository> {
val selected = config.repositories.getValue(repositoryId)
// The selected repository also needs Central for transitive dependencies.
return if (repositoryId == "central") listOf(selected)
else listOf(selected, config.repositories.getValue("central"))
}

internal data class LibraryConfig(
val repositories: Map<String, RemoteRepository>,
val libraries: List<LibrarySpec>
)

internal data class LibrarySpec(val coordinate: String, val repository: String? = null)
}
8 changes: 8 additions & 0 deletions src/main/resources/paper-libraries.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
repositories:
syntaxdevteam-snapshots: "https://nexus.syntaxdevteam.pl/repository/maven-snapshots/"
syntaxdevteam-releases: "https://nexus.syntaxdevteam.pl/repository/maven-releases/"
central: "MAVEN_CENTRAL_DEFAULT_MIRROR"
paper: "https://repo.papermc.io/repository/maven-public/"
jitpack: "https://jitpack.io/"
codemc-snapshots: "https://repo.codemc.io/repository/maven-snapshots/"

libraries:
- pl.syntaxdevteam:syntaxcore:${syntaxcoreVersion}
- pl.syntaxdevteam:messageHandler-paper:${messagehandlerVersion}
Expand Down
116 changes: 116 additions & 0 deletions src/test/kotlin/pl/syntaxdevteam/plotsx/loader/SyntaxLoaderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package pl.syntaxdevteam.plotsx.loader

import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver
import org.junit.Test
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import java.io.ByteArrayInputStream

class SyntaxLoaderTest {
private val loader = SyntaxLoader()
private val repositories = """
repositories:
central: "MAVEN_CENTRAL_DEFAULT_MIRROR"
syntaxdevteam-snapshots: "https://example.com/snapshots/"
syntaxdevteam-releases: "https://example.com/releases/"
custom: "https://example.com/custom/"
""".trimIndent()

private fun read(libraries: String) = loader.readLibraryConfig(
ByteArrayInputStream((repositories + "\n" + libraries.trimIndent()).toByteArray())
)

@Test
fun routesCentralAndSyntaxDevTeamLibraries() {
val config = read("""
libraries:
- com.github.ben-manes.caffeine:caffeine:3.2.4
- net.kyori:adventure-nbt:5.2.0
- pl.syntaxdevteam:syntaxcore:1.4.1-R0.1-SNAPSHOT
- pl.syntaxdevteam.licencex:SyntaxLicenceX:0.1.2-SNAPSHOT
- pl.syntaxdevteam:syntaxcore:1.4.1
""")
assertEquals(
listOf("central", "central", "syntaxdevteam-snapshots", "syntaxdevteam-snapshots", "syntaxdevteam-releases"),
config.libraries.map { loader.selectRepository(it, config) }
)
}

@Test
fun explicitRepositoryWinsAndKeepsCentralForTransitives() {
val config = read("""
libraries:
- coordinate: pl.syntaxdevteam:example:1.0-SNAPSHOT
repository: custom
""")
val id = loader.selectRepository(config.libraries.single(), config)
assertEquals("custom", id)
assertEquals(listOf("custom", "central"), loader.repositoriesFor(id, config).map { it.id })
assertEquals(MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR, config.repositories.getValue("central").url)
assertEquals(listOf("central"), loader.repositoriesFor("central", config).map { it.id })
}

@Test
fun syntaxDevTeamResolverAlsoGetsCentral() {
val config = read("""
libraries:
- pl.syntaxdevteam:syntaxcore:1.4.1-R0.1-SNAPSHOT
""")
assertEquals(
listOf("syntaxdevteam-snapshots", "central"),
loader.repositoriesFor(loader.selectRepository(config.libraries.single(), config), config).map { it.id }
)
}

@Test
fun rejectsUnknownRepositoryBeforeBootstrap() {
reject("""
libraries:
- coordinate: com.example:library:1.0
repository: missing
""")
}

@Test
fun rejectsUnexpandedAndMalformedLibraryEntries() {
reject("libraries: ['com.example:library:" + '$' + "{version}']")
reject("libraries: [42]")
reject("libraries: [{repository: custom}]")
reject("libraries: [{coordinate: 'com.example:library:1.0', repository: 42}]")
reject("libraries: []")
}

@Test
fun rejectsMissingCentralRepository() {
try {
loader.readLibraryConfig(ByteArrayInputStream("repositories: {}\nlibraries: ['com.example:library:1.0']".toByteArray()))
fail("Missing Central must stop bootstrap")
} catch (failure: IllegalArgumentException) {
assertTrue(failure.message.orEmpty().contains("central"))
}
}

@Test
fun packagedManifestHasResolvedVersionsAndValidRepositories() {
val input = javaClass.classLoader.getResourceAsStream("paper-libraries.yml")
?: error("Processed runtime manifest is missing")
val config = input.use(loader::readLibraryConfig)
assertTrue(config.libraries.isNotEmpty())
config.libraries.forEach { library ->
assertTrue(!library.coordinate.contains("\${"))
assertTrue(loader.repositoriesFor(loader.selectRepository(library, config), config).isNotEmpty())
}
}

private fun reject(libraries: String) {
try {
read(libraries)
} catch (_: IllegalArgumentException) {
return
} catch (_: IllegalStateException) {
return
}
fail("Invalid runtime manifest must stop bootstrap: $libraries")
}
}
Loading