Android: Improve installation when game is in non-root directory of ZIP

Launcher.kt extracts given ZIP file to the internal storage, preserving
its directory structure.

Before this change, if the game files were in a subdirectory, an
xsystem35.gr file was generated containing the path to the game files.
However, with this method, .xsys35rc and some data files (such as map
files of Kaeru Nyo) could not be loaded.

After this change, Launcher.kt will no longer generate the xsystem35.gr
file. Instead, it records the directory where the game is located in
game_directory.txt, and specify that directory in -gamedir to launch
xsystem35.

Fixes #51.
This commit is contained in:
kichikuou
2024-04-10 15:06:51 +09:00
parent 2b6267aeb9
commit 7c7768fba4
3 changed files with 38 additions and 56 deletions
@@ -30,9 +30,11 @@ import java.io.IOException
// Intent for this activity must have the following extra:
// - EXTRA_GAME_ROOT (string): A path to the game installation.
// - EXTRA_SAVE_DIRECTORY (string): A path to the save data directory.
class GameActivity : SDLActivity() {
companion object {
const val EXTRA_GAME_ROOT = "GAME_ROOT"
const val EXTRA_SAVE_DIRECTORY = "SAVE_DIRECTORY"
const val EXTRA_ARCHIVE_NAME = "ARCHIVE_NAME"
}
@@ -61,6 +63,7 @@ class GameActivity : SDLActivity() {
override fun getArguments(): Array<String> {
return arrayOf(
"-gamedir", intent.getStringExtra(EXTRA_GAME_ROOT)!!,
"-savedir", intent.getStringExtra(EXTRA_SAVE_DIRECTORY)!!,
"-playlist", Launcher.PLAYLIST_FILE)
}
@@ -25,9 +25,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.*
import java.lang.StringBuilder
import java.nio.charset.Charset
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@@ -41,11 +39,11 @@ interface LauncherObserver {
fun onInstallFailure(msgId: Int)
}
private const val SAVE_DIR = "save"
class Launcher private constructor(private val rootDir: File) {
companion object {
const val SAVE_DIR = "save"
const val TITLE_FILE = "title.txt"
const val GAMEDIR_FILE = "game_directory.txt"
const val PLAYLIST_FILE = "playlist2.txt"
const val OLD_PLAYLIST_FILE = "playlist.txt"
@@ -79,14 +77,14 @@ class Launcher private constructor(private val rootDir: File) {
isInstalling = true
GlobalScope.launch(Dispatchers.Main) {
try {
withContext(Dispatchers.IO) {
val gameDir = withContext(Dispatchers.IO) {
extractFiles(input, dir) { msg ->
GlobalScope.launch(Dispatchers.Main) {
observer?.onInstallProgress(msg)
}
}
}
observer?.onInstallSuccess(dir, archiveName)
observer?.onInstallSuccess(gameDir, archiveName)
} catch (e: InstallFailureException) {
observer?.onInstallFailure(e.msgId)
} catch (e: Exception) {
@@ -114,9 +112,11 @@ class Launcher private constructor(private val rootDir: File) {
continue
}
try {
val titleFile = File(path, TITLE_FILE)
val gameDirFile = File(path, GAMEDIR_FILE)
val gamePath = if (gameDirFile.exists()) File(path, gameDirFile.readText()) else path
val titleFile = File(gamePath, TITLE_FILE)
val title = titleFile.readText()
games.add(Entry(path, title, titleFile.lastModified()))
games.add(Entry(gamePath, title, titleFile.lastModified()))
migratePlaylist(path)
} catch (e: IOException) {
// Incomplete game installation. Delete it.
@@ -179,7 +179,7 @@ class Launcher private constructor(private val rootDir: File) {
}
}
private fun extractFiles(input: InputStream, outDir: File, progressCallback: (String) -> Unit) {
private fun extractFiles(input: InputStream, outDir: File, progressCallback: (String) -> Unit): File {
val configWriter = GameConfigWriter()
val hadDecodeError = forEachZipEntry(input) { zipEntry, zip ->
Log.i("extractFiles", zipEntry.name)
@@ -193,12 +193,13 @@ class Launcher private constructor(private val rootDir: File) {
}
configWriter.maybeAdd(zipEntry.name)
}
if (!configWriter.readyToWrite()) {
if (!configWriter.ready) {
if (hadDecodeError)
throw InstallFailureException(R.string.unsupported_zip)
throw InstallFailureException(R.string.cannot_find_ald)
}
configWriter.write(outDir)
return configWriter.gameDir?.let { File(outDir, it) } ?: outDir
}
// Xsystem35-sdl2 <=2.2.0 had a bug where playlist had an extra empty line at
@@ -217,66 +218,43 @@ class Launcher private constructor(private val rootDir: File) {
class InstallFailureException(val msgId: Int) : Exception()
// A helper class which generates xsystem35.gr and playlist.txt in the game root directory.
// A helper class which generates GAMEDIR_FILE and PLAYLIST_FILE.
private class GameConfigWriter {
private val grb = StringBuilder()
private var basename: String? = null
private val aldRegex = """(.*?)([a-z])([a-z])\.(ald|wai|bgi)""".toRegex(RegexOption.IGNORE_CASE)
private val aldType = mapOf(
"b" to "BGM",
"d" to "Data",
"g" to "Graphics",
"m" to "Midi",
"r" to "Resource",
"s" to "Scenario",
"w" to "Wave")
private val specialResources = mapOf(
"system39.ain" to "Ain",
"system39.ini" to "Init",
"sactefam.kld" to "SACT01"
)
var ready = false
private set
var gameDir: String? = null
private set
private val aldRegex = """.*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE)
private val audioRegex = """.*?(\d+)\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
private val audioFiles: Array<String?> = arrayOfNulls(100)
fun maybeAdd(path: String) {
val name = File(path).name
specialResources[name.lowercase(Locale.US)]?.let {
grb.appendLine("$it $path")
return
aldRegex.matchEntire(path)?.let {
gameDir = File(path).parent
ready = true
}
aldRegex.matchEntire(name)?.let {
val ext = it.groupValues[4].uppercase(Locale.US)
val type = if (ext == "ALD") {
aldType[it.groupValues[2].lowercase(Locale.US)]
} else {
ext
}
val id = it.groupValues[3].uppercase(Locale.US)
if (type != null) {
grb.appendLine("$type$id $path")
basename = it.groupValues[1]
}
}
audioRegex.matchEntire(name)?.let {
audioRegex.matchEntire(path)?.let {
val track = it.groupValues[1].toInt()
if (0 < track && track <= audioFiles.size)
audioFiles[track - 1] = path
}
}
fun readyToWrite() = basename != null
fun write(outDir: File) {
for (id in 'A' .. 'Z') {
grb.appendLine("Save$id ../save/${basename}s${id.lowercase(Locale.US)}.asd")
// Generate GAMEDIR_FILE
gameDir?.let {
File(outDir, GAMEDIR_FILE).writeText(it)
}
// Generate PLAYLIST_FILE
val absGameDir = gameDir?.let { File(outDir, it) } ?: outDir
val playlistFile = File(absGameDir, PLAYLIST_FILE)
if (!playlistFile.exists()) {
val prefixToRemove = gameDir?.let { "$it/" } ?: ""
val playlist = audioFiles.joinToString("\n") {
it?.removePrefix(prefixToRemove) ?: ""
}.trimEnd('\n')
playlistFile.writeText(playlist)
}
val gr = grb.toString()
Log.i("xsystem35.gr", gr)
File(outDir, "xsystem35.gr").writeText(gr)
val playlist = audioFiles.joinToString("\n") { it ?: "" }.trimEnd('\n')
File(outDir, PLAYLIST_FILE).writeText(playlist)
}
}
}
@@ -180,6 +180,7 @@ class LauncherActivity : Activity(), LauncherObserver {
val i = Intent()
i.setClass(applicationContext, GameActivity::class.java)
i.putExtra(GameActivity.EXTRA_GAME_ROOT, path.path)
i.putExtra(GameActivity.EXTRA_SAVE_DIRECTORY, File(filesDir, Launcher.SAVE_DIR).path)
i.putExtra(GameActivity.EXTRA_ARCHIVE_NAME, archiveName)
startActivity(i)
}