Skip to content

Commit

Permalink
Merge pull request #2 from HarleyGilpin/main
Browse files Browse the repository at this point in the history
feat: cache auto installation prompt when first starting up file-server
  • Loading branch information
HarleyGilpin authored Jul 12, 2024
2 parents 4c12312 + 5a68b49 commit 0fd31d1
Show file tree
Hide file tree
Showing 7 changed files with 494 additions and 268 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This repository hosts the file server files, which is what the client requires w
<b>2011Scape Related<b>
- [Game Repository](https://github.com/2011Scape/game)
- [File Server Repository](https://github.com/2011Scape/file-server)
- [Client Repository](https://github.com/2011Scape/rs-client)
- [Client Repository](https://github.com/2011Scape/runetek5-client)
- [Installation Guide](https://github.com/2011Scape/installation-guide)

<b>Extras</b>
Expand Down
17 changes: 12 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("com.displee:rs-cache-library:6.7")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
implementation("io.ktor:ktor-server-core:1.5.0")
implementation("io.ktor:ktor-server-core:2.3.0")
implementation("io.ktor:ktor-network:1.5.0")

implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("ch.qos.logback:logback-classic:1.4.12")
implementation("com.michael-bull.kotlin-inline-logger:kotlin-inline-logger-jvm:1.0.2")

testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
Expand All @@ -37,10 +37,14 @@ dependencies {

tasks {
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "11"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "11"
}
withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
}
}

Expand All @@ -55,7 +59,10 @@ tasks.withType<Jar> {

dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
configurations.runtimeClasspath
.get()
.filter { it.name.endsWith("jar") }
.map { zipTree(it) }
})
}

Expand Down
2 changes: 0 additions & 2 deletions cache/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
*
!.gitignore
40 changes: 32 additions & 8 deletions src/main/kotlin/world/gregs/rs2/file/FileServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import kotlin.math.min

class FileServer(
private val provider: DataProvider,
private val versionTable: ByteArray
private val versionTable: ByteArray,
) {
private val logger = InlineLogger()

/**
* Fulfills a request by sending the requested files data to the requester
*/
suspend fun fulfill(read: ByteReadChannel, write: ByteWriteChannel, prefetch: Boolean) {
suspend fun fulfill(
read: ByteReadChannel,
write: ByteWriteChannel,
prefetch: Boolean,
) {
val value = read.readUMedium()
val index = value shr 16
val archive = value and 0xffff
Expand All @@ -24,7 +28,10 @@ class FileServer(
/**
* @return data for an [index]'s [archive] file or [versionTable] when index and archive are both 255
*/
fun data(index: Int, archive: Int): ByteArray? {
fun data(
index: Int,
archive: Int,
): ByteArray? {
if (index == 255 && archive == 255) {
return versionTable
}
Expand All @@ -34,7 +41,13 @@ class FileServer(
/**
* Writes response header followed by the contents of [data] to [write]
*/
suspend fun serve(write: ByteWriteChannel, index: Int, archive: Int, data: ByteArray, prefetch: Boolean) {
suspend fun serve(
write: ByteWriteChannel,
index: Int,
archive: Int,
data: ByteArray,
prefetch: Boolean,
) {
val compression = data[0].toInt()
val size = getInt(data[1], data[2], data[3], data[4]) + if (compression != 0) 8 else 4
logger.trace { "Serving file $index $archive - $size." }
Expand All @@ -47,7 +60,14 @@ class FileServer(
/**
* Writes [source] [offset] [size] to [write] and starting at [headerSize] inserting a [SEPARATOR] every [split] bytes
*/
suspend fun serve(write: ByteWriteChannel, headerSize: Int, source: ByteArray, offset: Int, size: Int, split: Int) {
suspend fun serve(
write: ByteWriteChannel,
headerSize: Int,
source: ByteArray,
offset: Int,
size: Int,
split: Int,
) {
var length = min(size, split - headerSize)
write.writeFully(source, offset, length)
var written = length
Expand All @@ -61,12 +81,16 @@ class FileServer(
}

companion object {

private fun getInt(b1: Byte, b2: Byte, b3: Byte, b4: Byte) = b1.toInt() shl 24 or (b2.toInt() and 0xff shl 16) or (b3.toInt() and 0xff shl 8) or (b4.toInt() and 0xff)
private fun getInt(
b1: Byte,
b2: Byte,
b3: Byte,
b4: Byte,
) = b1.toInt() shl 24 or (b2.toInt() and 0xff shl 16) or (b3.toInt() and 0xff shl 8) or (b4.toInt() and 0xff)

private const val SEPARATOR = 255
private const val HEADER = 4
private const val SPLIT = 512
private const val OFFSET = 1
}
}
}
186 changes: 185 additions & 1 deletion src/main/kotlin/world/gregs/rs2/file/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ package world.gregs.rs2.file

import com.displee.cache.CacheLibrary
import com.github.michaelbull.logging.InlineLogger
import java.awt.GraphicsEnvironment
import java.io.File
import java.math.BigInteger
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.util.zip.ZipInputStream
import javax.swing.JOptionPane
import kotlin.concurrent.thread

object Main {
Expand Down Expand Up @@ -39,6 +48,25 @@ object Main {
}
logger.info { "Settings loaded." }

// Verify the cache files are present before proceeding
val cacheDirectory = Paths.get(cachePath)
if (!verifyCacheFiles(cacheDirectory)) {
val userResponse =
if (isDesktopAvailable()) {
promptUserWithDialog()
} else {
println("No desktop environment detected. Defaulting to yes for cache download.")
"yes"
}

if (userResponse.equals("yes", ignoreCase = true)) {
downloadAndInstallCache(cacheDirectory)
} else {
println("Cache download declined. Exiting.")
return
}
}

val cache = CacheLibrary(cachePath)
val versionTable = cache.generateNewUkeys(exponent, modulus)
logger.debug { "Version table generated: ${versionTable.contentToString()}" }
Expand All @@ -56,4 +84,160 @@ object Main {
runtime.addShutdownHook(thread(start = false) { network.stop() })
network.start(port, threads)
}
}

private fun isDesktopAvailable(): Boolean = !GraphicsEnvironment.isHeadless()

private fun promptUserWithDialog(): String {
val options = arrayOf("Yes", "No")
val response =
JOptionPane.showOptionDialog(
null,
"The cache path directory is missing required files. Do you want to download and install the cache?",
"Cache Download",
JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options,
options[0],
)
return if (response == JOptionPane.YES_OPTION) "yes" else "no"
}

private fun downloadAndInstallCache(cachePath: Path) {
val cacheMirrors =
listOf(
URL("https://2011scape.com/downloads/cache.zip"),
URL("https://archive.openrs2.org/caches/runescape/278/disk.zip"),
)
val zipFile = cachePath.resolve("cache.zip")

for (cacheUrl in cacheMirrors) {
try {
Files.createDirectories(cachePath) // Ensure the cache directory exists
println("Downloading cache from $cacheUrl.")

// Download with progress
downloadFileWithProgress(cacheUrl, zipFile)

println("Download complete. Unzipping cache...")
unzip(zipFile, cachePath.parent) // Extract to parent directory
Files.delete(zipFile)
println("Unzip complete.")
return // Exit the loop once download and extraction are successful
} catch (e: Exception) {
println("Failed to download from $cacheUrl. Trying next URL if available.")
e.printStackTrace()
}
}

println("All cache download attempts failed.")
}

private fun downloadFileWithProgress(
url: URL,
destination: Path,
) {
url.openStream().use { input ->
Files.newOutputStream(destination, StandardOpenOption.CREATE).use { output ->
val buffer = ByteArray(1024)
var bytesRead: Int
var totalBytesRead = 0L
val fileSize = url.openConnection().contentLengthLong

while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
totalBytesRead += bytesRead
printProgress(totalBytesRead, fileSize)
}
println() // Move to the next line after download completes
}
}
}

private fun printProgress(
bytesRead: Long,
totalBytes: Long,
) {
val progress = (bytesRead * 100) / totalBytes
print("\rDownloading: $progress%")
}

private fun unzip(
zipFilePath: Path,
destDir: Path,
) {
ZipInputStream(Files.newInputStream(zipFilePath)).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val entryName = entry.name
val normalizedPath = Paths.get(entryName).normalize()
val filePath = destDir.resolve(normalizedPath)

if (!entry.isDirectory) {
Files.createDirectories(filePath.parent)
Files.copy(zip, filePath, StandardCopyOption.REPLACE_EXISTING)
println("Extracted file: $filePath")
} else {
Files.createDirectories(filePath)
println("Created directory: $filePath")
}

zip.closeEntry()
entry = zip.nextEntry
}
}
}

private fun verifyCacheFiles(filestore: Path): Boolean {
val requiredFiles =
listOf(
"main_file_cache.dat2",
"main_file_cache.idx0",
"main_file_cache.idx1",
"main_file_cache.idx10",
"main_file_cache.idx11",
"main_file_cache.idx12",
"main_file_cache.idx13",
"main_file_cache.idx14",
"main_file_cache.idx15",
"main_file_cache.idx16",
"main_file_cache.idx17",
"main_file_cache.idx18",
"main_file_cache.idx19",
"main_file_cache.idx2",
"main_file_cache.idx20",
"main_file_cache.idx21",
"main_file_cache.idx22",
"main_file_cache.idx23",
"main_file_cache.idx24",
"main_file_cache.idx25",
"main_file_cache.idx255",
"main_file_cache.idx26",
"main_file_cache.idx27",
"main_file_cache.idx28",
"main_file_cache.idx29",
"main_file_cache.idx3",
"main_file_cache.idx30",
"main_file_cache.idx31",
"main_file_cache.idx32",
"main_file_cache.idx33",
"main_file_cache.idx34",
"main_file_cache.idx35",
"main_file_cache.idx36",
"main_file_cache.idx4",
"main_file_cache.idx5",
"main_file_cache.idx6",
"main_file_cache.idx7",
"main_file_cache.idx8",
"main_file_cache.idx9",
)

for (file in requiredFiles) {
val filePath = filestore.resolve(file)
if (Files.notExists(filePath)) {
return false
}
}
return true
}
}
Loading

0 comments on commit 0fd31d1

Please sign in to comment.