Skip to content

Commit

Permalink
feat: server list sync (#17)
Browse files Browse the repository at this point in the history
* chore: remove duplicated code in function Path.createFileWithParentDirectoriesOrTerminate() in file FilePathUtils.kt

* chore: add Path.createFileOrTerminate() in FilePathUtils.kt

* fix(proguard): the BuildMinimizedJarTask will suppress the output if '-i' is not passed

* fix: fix proguard warning related to Okio

* chore(admin): use java.io.IOException for IOException in PrismLauncherDataSource

* chore(common): add Path.isFileEmpty() in SharedFilePathUtils.kt

* chore: remove println() in finalize() function

* docs(readme): update server list syncing status in README.md

* docs(readme): update the development status for syncing Mods, Resource Packs, Shader packs

* chore(proguard): use when block in BuildMinimizedJarTask.execute() to check for project.gradle.startParameter.logLevel
  • Loading branch information
EchoEllet authored Jul 5, 2024
1 parent 473cf81 commit c3ab40e
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 27 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ instance**.

The script will sync the following:

1. 🛠️ Mods (experimental)
2. 🎨 Resource packs (highly experimental)
3. ✨ Shader-packs (not implemented yet)
4. 🌐 Server List (in-game) (not implemented yet)
5. 🧩 Mods configurations (not implemented yet): These may be synced initially, since each player can have their own
1. 🛠️ Mods (Experimental): In the experimental phase and subject to changes.
2. 🎨 Resource Packs (Highly Experimental): In the highly experimental phase and subject to changes.
3. ✨ Shader Packs (Not Implemented Yet): This feature is not available yet. Future updates may implement it
4. 🌐 In-Game Server List (Alpha): It is subject to potential changes or removal in future updates.
5. 🧩 Mods configurations (Not Implemented Yet): These may be synced initially, since each player can have their own
configurations.
We have not yet found a solution, except by enforcing the same mod settings for all players.
6. ⌨️ Keybindings (not implemented yet): These may be synced initially since each player can have their own
6. ⌨️ Keybindings (Not Implemented Yet): These may be synced initially since each player can have their own
keybindings.
We have not yet found a solution for this, except by enforcing the same keybindings for all players

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import constants.DotMinecraftFileNames
import curseForgeDataSource
import launchers.Instance
import launchers.LauncherDataSource
import okio.IOException
import syncInfo.models.mod.Mod
import utils.SystemFileProvider
import utils.listFilteredPaths
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
Expand Down
14 changes: 8 additions & 6 deletions buildSrc/src/main/kotlin/BuildMinimizedJarTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,13 @@ open class BuildMinimizedJarTask : DefaultTask() {
// A workaround for executing ProGuard without getting the notes by disabling the logging
// when the `-i` or `--info` is not set

if (project.gradle.startParameter.logLevel != LogLevel.INFO) {
suppressOutputAndExecute {
proguardTask.actions.forEach { it.execute(proguardTask) }
when (project.gradle.startParameter.logLevel) {
LogLevel.INFO -> proguardTask.actions.forEach { it.execute(proguardTask) }
else -> {
suppressOutputAndExecute {
proguardTask.actions.forEach { it.execute(proguardTask) }
}
}
} else {
proguardTask.actions.forEach { it.execute(proguardTask) }
}

logResultMessage(isObfuscatedEnabled = isObfuscatedEnabled)
Expand All @@ -173,7 +174,8 @@ open class BuildMinimizedJarTask : DefaultTask() {
val formattedPercentageDifference = String.format("%.2f%%", kotlinMathAbs(percentageDifference))

logger.lifecycle(
"📦 The size of the Proguard ${if (isObfuscatedEnabled) "obfuscated" else "minimized"} JAR file (${minimizedJarFile.name}) is $minimizedFileSizeInMegabytes MB." +
"📦 The size of the Proguard ${if (isObfuscatedEnabled) "obfuscated" else "minimized"} JAR file " +
"(${minimizedJarFile.name}) is $minimizedFileSizeInMegabytes MB." +
" The size has been reduced \uD83D\uDCC9 by $formattedPercentageDifference. Location: ${minimizedJarFile.path}",
)
}
Expand Down
3 changes: 3 additions & 0 deletions common/src/main/kotlin/utils/SharedFilePathUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlinx.coroutines.withContext
import java.nio.file.FileSystemException
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.fileSize
import kotlin.io.path.name
import kotlin.streams.toList

Expand All @@ -31,3 +32,5 @@ suspend fun Path.listFilteredPaths(filter: (path: Path) -> Boolean): Result<List
} catch (e: Exception) {
Result.failure(e)
}

fun Path.isFileEmpty(): Boolean = fileSize() == 0L
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ kotlin = "2.0.0"
kotlinx-serialization = "1.7.0"
kolinx-coroutines = "1.9.0-RC"
okhttp = "4.12.0"
okio = "3.9.0"
flatlaf = "3.4.1"
ktoml = "0.5.2"
knbt = "0.11.5"

# Plugins
shadow-jar = "8.1.1"
Expand All @@ -24,9 +26,11 @@ java = "11"
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kolinx-coroutines" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
flatlaf-core = { module = "com.formdev:flatlaf", version.ref = "flatlaf" }
flatlaf-extras = { module = "com.formdev:flatlaf-extras", version.ref = "flatlaf" }
ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" }
knbt = { module = "net.benwoodworth.knbt:knbt", version.ref = "knbt" }

# For tests
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand Down
3 changes: 3 additions & 0 deletions sync-script/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.okhttp)
// Explicitly including Okio to use the latest version to solve some Proguard warnings
implementation(libs.okio)
implementation(libs.flatlaf.core)
implementation(libs.flatlaf.extras)
implementation(libs.knbt)

testImplementation(libs.kotlin.test)
}
Expand Down
4 changes: 2 additions & 2 deletions sync-script/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import syncInfo.models.SyncInfo
import syncInfo.models.instance
import syncService.ModsSyncService
import syncService.ResourcePacksSyncService
import syncService.ServersSyncService
import syncService.SyncService
import utils.ExecutionTimer
import utils.HttpService
Expand Down Expand Up @@ -300,6 +301,7 @@ suspend fun performSyncServices(scriptConfig: ScriptConfig) {
add(ModsSyncService())
if (scriptConfig.environment.isClient()) {
add(ResourcePacksSyncService())
add(ServersSyncService())
}
}

Expand All @@ -311,8 +313,6 @@ fun finalize(applicationExecutionTimer: ExecutionTimer) {

LoadingIndicatorDialog.instance?.isVisible = false

println()

// The temporary folder usually contains the downloaded files which will be moved once finished
// after finish syncing the contents successfully, we don't need it anymore.
handleTemporaryDirectory(isStart = false)
Expand Down
144 changes: 142 additions & 2 deletions sync-script/src/main/kotlin/syncService/ServersSyncService.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,151 @@
package syncService

import constants.SyncScriptDotMinecraftFiles
import net.benwoodworth.knbt.Nbt
import net.benwoodworth.knbt.NbtCompound
import net.benwoodworth.knbt.NbtCompression
import net.benwoodworth.knbt.NbtList
import net.benwoodworth.knbt.NbtTag
import net.benwoodworth.knbt.NbtVariant
import net.benwoodworth.knbt.add
import net.benwoodworth.knbt.buildNbtCompound
import net.benwoodworth.knbt.buildNbtList
import net.benwoodworth.knbt.decodeFromStream
import net.benwoodworth.knbt.encodeToStream
import net.benwoodworth.knbt.nbtCompound
import net.benwoodworth.knbt.put
import syncInfo.models.SyncInfo
import syncInfo.models.instance
import utils.ExecutionTimer
import utils.buildHtml
import utils.isFileEmpty
import utils.showErrorMessageAndTerminate
import java.io.EOFException
import kotlin.io.path.exists
import kotlin.io.path.inputStream
import kotlin.io.path.isRegularFile
import kotlin.io.path.name
import kotlin.io.path.outputStream
import kotlin.io.path.pathString
import kotlin.system.exitProcess

class ServersSyncService : SyncService {
private val serversFilePath = SyncScriptDotMinecraftFiles.ServersDat.path
private val serversDatFilePath = SyncScriptDotMinecraftFiles.ServersDat.path
private val serversSyncService = SyncInfo.instance.serverSyncInfo

companion object {
const val SERVER_NBT_MAIN_COMPOUND_KEY = ""
}

private val nbt =
Nbt {
variant = NbtVariant.Java
compression = NbtCompression.None
}

override suspend fun syncData() {
TODO("Not yet implemented")
val executionTimer = ExecutionTimer()
executionTimer.setStartTime()

println("\n\uD83D\uDD04 Syncing server list...")

val currentRootCompound =
loadServersDatFile().getOrElse {
showErrorMessageAndTerminate(
title = "📁 File Loading Error",
message =
buildHtml {
text("⚠ Unable to read the server list from the file '${serversDatFilePath.pathString}': ")
newLine()
text("$it")
newLines(2)
if (it is EOFException) {
text("This issue might occur if the file is corrupt or incomplete.")
newLine()
text("As a potential workaround, consider deleting the file '${serversDatFilePath.pathString}'.")
} else {
text("Deleting the file '${serversDatFilePath.pathString}' could resolve the issue.")
}
newLine()
boldText("Note: Deleting this file will reset the server list in the game.")
}.buildBodyAsText(),
)
return
}

val newServerListCompound: NbtList<NbtCompound> =
buildNbtList {
serversSyncService.servers.map {
val serverCompound =
buildNbtCompound {
put("ip", it.address)
put("name", it.name)
}
add(serverCompound)
}
}

val newRootCompound =
currentRootCompound.let {
val mutableRootMap = it.toMutableMap()

val mutableMainMap = it[SERVER_NBT_MAIN_COMPOUND_KEY]?.nbtCompound?.toMutableMap() ?: mutableMapOf()
mutableMainMap["servers"] = newServerListCompound

mutableRootMap[SERVER_NBT_MAIN_COMPOUND_KEY] = NbtCompound(mutableMainMap)

NbtCompound(mutableRootMap)
}

updateServersDatFile(newRootCompound = newRootCompound).getOrElse {
showErrorMessageAndTerminate(
title = "🚨 File Update Error",
message = "⚠️ Unable to update the '${serversDatFilePath.name}' file: $it",
)
return
}

println("\uD83D\uDD52 Finished syncing the server list in ${executionTimer.getRunningUntilNowDuration().inWholeMilliseconds}ms.")
}

private fun loadServersDatFile(): Result<NbtCompound> {
return try {
if (serversDatFilePath.exists()) {
if (!serversDatFilePath.isRegularFile()) {
showErrorMessageAndTerminate(
title = "❌ Invalid '${serversDatFilePath.name}' File",
message =
"\uD83D\uDEE0 '${serversDatFilePath.name}' must be a file \uD83D\uDCC2, a directory/folder was found instead.",
)
// This will never reach due to the previous statement stopping the application
exitProcess(1)
}
if (serversDatFilePath.isFileEmpty()) {
println("ℹ️ The file '${serversDatFilePath.name}' exists and is currently empty.")
return Result.success(createEmptyServerCompound())
}
return Result.success(
serversDatFilePath
.inputStream()
.use { inputStream ->
nbt.decodeFromStream<NbtTag>(inputStream)
} as NbtCompound,
)
}
Result.success(createEmptyServerCompound())
} catch (e: Exception) {
Result.failure(e)
}
}

private fun updateServersDatFile(newRootCompound: NbtCompound): Result<Unit> =
runCatching {
serversDatFilePath
.outputStream()
.use { outputStream ->
nbt.encodeToStream(newRootCompound, outputStream)
}
}

private fun createEmptyServerCompound(): NbtCompound = NbtCompound(mapOf(SERVER_NBT_MAIN_COMPOUND_KEY to NbtCompound(emptyMap())))
}
14 changes: 4 additions & 10 deletions sync-script/src/main/kotlin/utils/FilePathUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,10 @@ fun Path.createParentDirectoriesIfDoesNotExist() {

fun Path.createFileWithParentDirectoriesOrTerminate() {
createParentDirectoriesIfDoesNotExist()
if (!parent.exists()) {
try {
createParentDirectories()
} catch (e: Exception) {
showErrorMessageAndTerminate(
title = "File Creation Error ⚠\uFE0F",
message = "❌ Failed to create the directory '${this.name}': $e",
)
}
}
createFileOrTerminate()
}

fun Path.createFileOrTerminate() {
try {
createFile()
} catch (e: Exception) {
Expand Down
3 changes: 3 additions & 0 deletions sync-script/src/main/kotlin/utils/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import kotlin.system.exitProcess
* we might call [showErrorMessageAndTerminate] when having error while loading the [ScriptConfig]
* (for example), which is why we can't [ScriptConfig.getInstanceOrThrow] because we will get not initialized error
* while showing an error message
*
* TODO: Some usages pass HTML message to [message], which will cause unreadable message in CLI mode, we need to
* either provide a different message parameter for the CLI or convert the HTML to plain text
* */
fun showErrorMessageAndTerminate(
title: String,
Expand Down

0 comments on commit c3ab40e

Please sign in to comment.