Skip to content

Commit 61e46b9

Browse files
committed
Support for multiple game versions
1 parent 5e1a271 commit 61e46b9

21 files changed

Lines changed: 454 additions & 332 deletions

File tree

base/src/main/kotlin/org/sinytra/probe/base/RestAPI.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import kotlinx.serialization.Serializable
55
import org.sinytra.probe.base.db.ProjectPlatform
66

77
@Serializable
8-
data class TestRequestBody(val platform: ProjectPlatform, val id: String)
8+
data class TestRequestBody(
9+
val platform: ProjectPlatform,
10+
val id: String,
11+
val gameVersion: String
12+
)
913

1014
interface ResponseBase {
1115
val project: TestProjectDTO
@@ -43,7 +47,7 @@ data class UnavailableResponseBody(
4347
data class TestEnvironmentDTO(
4448
val connectorVersion: String,
4549
val gameVersion: String,
46-
val neoForgeVersion: String
50+
val neoforgeVersion: String
4751
)
4852

4953
@Serializable

core/src/main/kotlin/org/sinytra/probe/core/platform/ModrinthPlatform.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.sinytra.probe.core.platform.ModrinthAPI.ModrinthVersionDependency
1010
import org.sinytra.probe.core.platform.ModrinthAPI.ModrinthVersion
1111
import org.sinytra.probe.core.platform.ModrinthAPI.ModrinthSearchResult
1212
import org.sinytra.probe.core.platform.ModrinthAPI.ModrinthProject
13+
import org.sinytra.probe.core.service.CacheService
1314
import org.slf4j.LoggerFactory
1415
import java.nio.file.Path
1516
import kotlin.io.path.*
@@ -27,7 +28,7 @@ data class ModrinthResolvedVersion(
2728

2829
class ModrinthPlatform(
2930
private val storagePath: Path,
30-
private val cache: PlatformCache
31+
private val cache: CacheService
3132
) : PlatformService {
3233
companion object {
3334
const val FAPI_ID: String = "P7dR8mSH"

core/src/main/kotlin/org/sinytra/probe/core/platform/PlatformCache.kt renamed to core/src/main/kotlin/org/sinytra/probe/core/service/CacheService.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
package org.sinytra.probe.core.platform
1+
package org.sinytra.probe.core.service
22

33
import io.lettuce.core.ExperimentalLettuceCoroutinesApi
44
import io.lettuce.core.api.StatefulRedisConnection
55
import io.lettuce.core.api.coroutines
66
import kotlinx.serialization.json.Json
77

88
@OptIn(ExperimentalLettuceCoroutinesApi::class)
9-
class PlatformCache(
10-
val redis: StatefulRedisConnection<String, String>
9+
class CacheService(
10+
redis: StatefulRedisConnection<String, String>
1111
) {
1212
val cmd = redis.coroutines()
1313

Lines changed: 102 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,46 @@
11
package org.sinytra.probe.core.service
22

3+
import kotlinx.coroutines.coroutineScope
4+
import kotlinx.io.files.FileNotFoundException
35
import org.slf4j.LoggerFactory
46
import java.io.FileOutputStream
57
import java.net.URI
68
import java.nio.channels.Channels
9+
import java.nio.file.FileSystems
710
import java.nio.file.Path
11+
import java.util.jar.Attributes
12+
import java.util.jar.Manifest
813
import javax.xml.parsers.DocumentBuilderFactory
9-
import kotlin.io.path.Path
10-
import kotlin.io.path.createParentDirectories
11-
import kotlin.io.path.div
12-
import kotlin.io.path.exists
14+
import kotlin.io.path.*
1315

14-
fun getMavenUrl(repo: String, group: String, name: String, version: String, classifier: String? = null): String {
15-
return "$repo/${group.replace('.', '/')}/$name/$version/$name-$version${classifier?.let { "-$it" }}.jar"
16-
}
16+
data class Library(
17+
val group: String, val name: String, val classifier: String?, val repository: String,
18+
val versionFilter: (gameVersion: String, version: String) -> Boolean
19+
)
1720

18-
data class TransformLib(val path: Path, val version: String)
21+
data class ResolvedLibrary(val path: Path, val version: String)
1922

20-
class DownloaderService(private val baseDir: Path, private val initialTransformerVersion: String?, private val useProvided: Boolean = true) {
23+
class DownloaderService(
24+
private val baseDir: Path,
25+
private val cache: CacheService,
26+
private val useCache: Boolean = true
27+
) {
2128
companion object {
2229
private val LOGGER = LoggerFactory.getLogger(DownloaderService::class.java)
30+
private const val NEO_MAVEN = "https://maven.neoforged.net/releases"
2331
private const val SINYTRA_MAVEN = "https://maven.sinytra.org"
2432

33+
private val TRANSFORMER_LIB = Library("org.sinytra.connector", "transformer", "all", SINYTRA_MAVEN)
34+
{ gameVersion, version ->
35+
version.contains("+") && version.split("+")[1] == gameVersion
36+
}
37+
private val NEO_UNIVERSAL_LIB = Library("net.neoforged", "neoforge", "universal", NEO_MAVEN)
38+
{ gameVersion, version ->
39+
val qualifier = gameVersion.split(".", limit = 2)[1]
40+
version.startsWith("$qualifier.")
41+
}
42+
private val NFRT_LIB = Library("net.neoforged", "neoform-runtime", "all", NEO_MAVEN) { _, _ -> true }
43+
2544
fun downloadFile(url: String, dest: Path) {
2645
if (!dest.exists()) {
2746
LOGGER.info("Downloading file $url to $dest")
@@ -35,54 +54,100 @@ class DownloaderService(private val baseDir: Path, private val initialTransforme
3554
}
3655
}
3756

38-
var activeTransformerVersion: String? = null
39-
private set
57+
suspend fun getTransformerLib(gameVersion: String): ResolvedLibrary {
58+
return getLibrary(TRANSFORMER_LIB, gameVersion)
59+
?: throw RuntimeException("Failed to resolve transformer library")
60+
}
61+
62+
suspend fun getNeoForgeUniversalLib(gameVersion: String): ResolvedLibrary {
63+
return getLibrary(NEO_UNIVERSAL_LIB, gameVersion)
64+
?: throw RuntimeException("Failed to resolve NeoForge Universal library")
65+
}
66+
67+
suspend fun getNFRTRuntime(gameVersion: String, version: String): ResolvedLibrary {
68+
return getLibrary(NFRT_LIB, gameVersion, version)
69+
?: throw RuntimeException("Failed to resolve NFRT library")
70+
}
71+
72+
suspend fun clearCache(gameVersion: String) {
73+
val libraries = listOf(TRANSFORMER_LIB, NEO_UNIVERSAL_LIB, NFRT_LIB)
74+
for (library in libraries) {
75+
val key = "probe:library:${library.name}:game:$gameVersion"
4076

41-
fun getTransformLib(): TransformLib {
42-
val provided = System.getProperty("org.sinytra.transformer.path")
43-
if (useProvided && provided != null) {
44-
val version = initialTransformerVersion ?: throw RuntimeException("Transformer version must be set")
45-
return TransformLib(Path(provided), version)
77+
cache.del(key)
4678
}
79+
}
4780

48-
if (activeTransformerVersion == null) {
49-
activeTransformerVersion = computeTransformerVersion()
81+
private suspend fun getLibrary(library: Library, gameVersion: String, libVersion: String? = null): ResolvedLibrary? {
82+
val property = gameVersion.replace(".", "_")
83+
val provided = System.getProperty("org.sinytra.probe.lib.${library.name}.$property.path")
84+
if (provided != null) {
85+
val path = Path(provided).takeIf { it.exists() } ?: throw FileNotFoundException(provided)
86+
val version = readImplementationVersion(path) ?: throw RuntimeException("Library '${library.name}' version must be set")
87+
return ResolvedLibrary(path, version)
5088
}
5189

52-
val outputFile = baseDir / "transformer-$activeTransformerVersion-all.jar"
90+
if (!useCache) return null
91+
92+
val key = "probe:library:${library.name}:game:$gameVersion"
93+
val version = libVersion
94+
?: System.getProperty("org.sinytra.probe.lib.${library.name}.$property.version")
95+
?: cache.get(key)
96+
?: coroutineScope {
97+
val remote = getRemoteLibraryVersion(library, gameVersion)
98+
cache.set(key, remote)
99+
remote
100+
}
101+
102+
val group = library.group.replace('.', '/')
103+
val classifier = library.classifier?.let { "-$it" } ?: ""
104+
val mavenPath = "${group}/${library.name}/$version/${library.name}-$version$classifier.jar"
105+
106+
val url = "${library.repository}/$mavenPath"
107+
val outputFile = baseDir / "maven" / mavenPath
53108
outputFile.createParentDirectories()
54109

55-
val transformerUrl = getMavenUrl(SINYTRA_MAVEN, "org.sinytra.connector", "transformer", activeTransformerVersion!!, "all")
56-
downloadFile(transformerUrl, outputFile)
110+
downloadFile(url, outputFile)
111+
57112
if (!outputFile.exists()) {
58-
throw IllegalStateException("Failed to download transformer lib")
113+
throw IllegalStateException("Failed to download library ${library.name}")
59114
}
60115

61-
return TransformLib(outputFile, activeTransformerVersion!!)
116+
return ResolvedLibrary(outputFile, version)
62117
}
63118

64-
private fun computeTransformerVersion(): String {
65-
if (initialTransformerVersion != null) {
66-
return initialTransformerVersion
67-
}
68-
119+
private fun getRemoteLibraryVersion(library: Library, gameVersion: String): String {
69120
try {
70121
val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
71-
val dest = "$SINYTRA_MAVEN/org/sinytra/connector/transformer/maven-metadata.xml"
122+
val path = library.group.replace('.', '/')
123+
124+
val dest = "${library.repository}/${path}/${library.name}/maven-metadata.xml"
72125
val doc = URI(dest).toURL().openStream().use(builder::parse)
73126
doc.documentElement.normalize()
74127

75-
val latest = doc.getElementsByTagName("latest")
76-
?.takeIf { it.length > 0 }
77-
?.item(0)?.textContent
128+
val versionElements = doc.getElementsByTagName("version")
129+
val versions = (0 until versionElements.length)
130+
.map { versionElements.item(it).textContent }
131+
.filter { library.versionFilter(gameVersion, it) }
78132

79-
if (latest != null) {
80-
return latest
81-
}
133+
versions.lastOrNull()?.let { return it }
82134
} catch (e: Exception) {
83-
LOGGER.error("Error fetching transformer version", e)
135+
LOGGER.error("Error fetching '${library.name}' version", e)
84136
}
85137

86-
throw RuntimeException("Could not fetch transformer version")
138+
throw RuntimeException("Could not fetch library version for '${library.name}'")
139+
}
140+
141+
private fun readImplementationVersion(path: Path): String? {
142+
return FileSystems.newFileSystem(path)
143+
.use { fs ->
144+
val mfPath = fs.getPath("META-INF", "MANIFEST.MF")
145+
.takeIf { it.exists() } ?: return null
146+
147+
val manifest = Manifest()
148+
mfPath.inputStream().use(manifest::read)
149+
150+
manifest.mainAttributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION)
151+
}
87152
}
88153
}
Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.sinytra.probe.core.service
22

3-
import org.sinytra.probe.core.service.DownloaderService.Companion.downloadFile
3+
import kotlinx.coroutines.runBlocking
4+
import org.slf4j.Logger
5+
import org.slf4j.LoggerFactory
46
import java.lang.ProcessBuilder.Redirect
57
import java.nio.file.Path
68
import java.util.concurrent.TimeUnit
@@ -12,36 +14,66 @@ class SetupService(
1214
private val baseDir: Path,
1315
private val useLocalCache: Boolean,
1416
private val nfrtVersion: String,
15-
private val neoForgeVersion: String,
16-
initialTransformerVersion: String?
17+
val gameVersions: List<String>,
18+
cache: CacheService
1719
) {
1820
companion object {
19-
private const val NEO_MAVEN = "https://maven.neoforged.net/releases"
21+
private val LOGGER: Logger = LoggerFactory.getLogger(SetupService::class.java)
2022
}
2123

22-
private var downloader = DownloaderService(baseDir, initialTransformerVersion)
24+
private var downloader = DownloaderService(baseDir, cache)
2325

24-
fun installDependencies(): GameFiles {
25-
val outputDir: Path = baseDir / "output"
26+
suspend fun installDependencies() {
27+
for (gameVersion in gameVersions) {
28+
LOGGER.info("Installing game files for $gameVersion")
29+
30+
installGameFiles(gameVersion)
31+
}
32+
}
33+
34+
fun getGameFiles(gameVersion: String): GameFiles {
35+
return runBlocking { installGameFiles(gameVersion) }
36+
}
37+
38+
fun getTransformLib(gameVersion: String): ResolvedLibrary {
39+
validateGameVersion(gameVersion)
40+
return runBlocking { downloader.getTransformerLib(gameVersion) }
41+
}
42+
43+
fun getNeoForgeVersion(gameVersion: String): String {
44+
validateGameVersion(gameVersion)
45+
return runBlocking { downloader.getNeoForgeUniversalLib(gameVersion).version }
46+
}
47+
48+
suspend fun updateLibraries() {
49+
LOGGER.info("Updating libraries")
50+
51+
gameVersions.forEach {
52+
downloader.clearCache(it)
53+
}
54+
55+
installDependencies()
56+
}
57+
58+
fun hasGameVersion(gameVersion: String): Boolean =
59+
gameVersions.contains(gameVersion)
60+
61+
private suspend fun installGameFiles(gameVersion: String): GameFiles {
62+
val outputDir: Path = baseDir / "neoforge" / gameVersion
2663
outputDir.createDirectories()
2764

2865
val cleanArtifact = outputDir / "clean.jar"
2966
val compiledArtifact = outputDir / "compiled.jar"
3067

31-
val neoUniversal = baseDir / "neoforge-$neoForgeVersion-universal.jar"
32-
val neoUniversalUrl = getMavenUrl(NEO_MAVEN, "net.neoforged", "neoforge", neoForgeVersion, "universal")
33-
downloadFile(neoUniversalUrl, neoUniversal)
34-
35-
val runtime = baseDir / "neoform-runtime-$nfrtVersion-all.jar"
36-
val nfrtUrl = getMavenUrl(NEO_MAVEN, "net.neoforged", "neoform-runtime", nfrtVersion, "all")
37-
downloadFile(nfrtUrl, runtime)
68+
val neoUniversal = downloader.getNeoForgeUniversalLib(gameVersion)
69+
val nfrt = downloader.getNFRTRuntime(gameVersion, nfrtVersion)
3870

3971
val workDir = baseDir / ".temp"
4072
workDir.createDirectories()
4173

42-
val neoMavenCoords = "net.neoforged:neoforge:$neoForgeVersion:userdev"
74+
val neoMavenCoords = "net.neoforged:neoforge:${neoUniversal.version}:userdev"
4375
val args = mutableListOf(
44-
"java", "-jar", runtime.absolutePathString(),
76+
"java", "-jar", nfrt.path.absolutePathString(),
4577
"run", "--neoforge", neoMavenCoords, "--dist", "joined",
4678
"--write-result=compiled:${compiledArtifact.fileName}",
4779
"--write-result=vanillaDeobfuscated:${cleanArtifact.fileName}",
@@ -65,27 +97,14 @@ class SetupService(
6597
.start()
6698
.waitFor(60, TimeUnit.MINUTES)
6799

68-
// Initialize transform lib
69-
getTransformLib()
100+
getTransformLib(gameVersion)
70101

71-
return GameFiles(cleanArtifact, listOf(neoUniversal, compiledArtifact))
102+
return GameFiles(cleanArtifact, listOf(neoUniversal.path, compiledArtifact))
72103
}
73104

74-
fun getTransformLib(): TransformLib = downloader.getTransformLib()
75-
76-
fun updateTransformLib(): TransformLib? {
77-
val newDownloader = DownloaderService(baseDir, null, false)
78-
val newTransformer = newDownloader.getTransformLib()
79-
80-
if (newTransformer.version != downloader.activeTransformerVersion) {
81-
downloader = newDownloader
82-
return newTransformer
105+
private fun validateGameVersion(gameVersion: String) {
106+
if (!gameVersions.contains(gameVersion)) {
107+
throw IllegalArgumentException("Game version $gameVersion is not supported")
83108
}
84-
85-
return null
86-
}
87-
88-
private fun getMavenUrl(repo: String, group: String, name: String, version: String, classifier: String? = null): String {
89-
return "$repo/${group.replace('.', '/')}/$name/$version/$name-$version${classifier?.let { "-$it" }}.jar"
90109
}
91110
}

core/src/main/kotlin/org/sinytra/probe/core/service/TransformationService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ data class TransformationResult(
2626
class TransformationService(
2727
private val storagePath: Path,
2828
private val platforms: GlobalPlatformService,
29-
private val gameFiles: GameFiles,
3029
private val setup: SetupService
3130
) {
3231
companion object {
@@ -39,6 +38,7 @@ class TransformationService(
3938

4039
val allFiles = (listOf(mainFile) + otherFiles).map { it.getFilePath(storagePath) }
4140

41+
val gameFiles = setup.getGameFiles(gameVersion)
4242
val classPath = gameFiles.loaderFiles.toMutableList() + resolveMandatedLibraries(gameVersion)
4343
val workDir = project.version.getFilePath(storagePath).parent / "output"
4444

@@ -83,7 +83,7 @@ class TransformationService(
8383

8484
@OptIn(ExperimentalSerializationApi::class)
8585
private fun runTransformer(workDir: Path, sources: List<Path>, cleanPath: Path, classPath: List<Path>, gameVersion: String): TransformLibOutput {
86-
val transformer = setup.getTransformLib()
86+
val transformer = setup.getTransformLib(gameVersion)
8787

8888
val baseArgs = listOf(
8989
"java",

0 commit comments

Comments
 (0)