mirror of
https://github.com/kichikuou/xsystem35-sdl2.git
synced 2026-09-22 22:48:08 +03:00
Android: Support CCD and MDS CD images
Extend the Android CD image reader to accept .bin/.img + .ccd and .mdf/.mds pairs. CCD metadata is converted into the existing track model, and MDS descriptors provide data and audio track offsets for extraction.
This commit is contained in:
@@ -103,6 +103,8 @@ internal class CdImageReader private constructor(
|
||||
const val ISO_SECTOR_SIZE = 2048
|
||||
private const val RAW_AUDIO_SECTOR_SIZE = 2352
|
||||
private const val WAVE_HEADER_SIZE = 44
|
||||
private const val MDS_MODE_AUDIO = 0xa9
|
||||
private const val MDS_MODE_MODE1 = 0xaa
|
||||
|
||||
fun open(
|
||||
contentResolver: ContentResolver,
|
||||
@@ -115,10 +117,23 @@ internal class CdImageReader private constructor(
|
||||
if (selected.metadata == null) {
|
||||
return CdImageReader(image, listOf(null, TrackInfo.iso(image.size)))
|
||||
}
|
||||
val cueText = contentResolver.openInputStream(selected.metadata.uri)?.bufferedReader()?.use {
|
||||
it.readText()
|
||||
} ?: throw InstallFailureException(R.string.cd_image_read_error)
|
||||
return CdImageReader(image, parseCue(cueText, image.size))
|
||||
val metadataName = selected.metadata.displayName.lowercase(Locale.US)
|
||||
val tracks = when {
|
||||
metadataName.endsWith(".cue") -> {
|
||||
val cueText = readMetadataText(contentResolver, selected.metadata)
|
||||
parseCue(cueText, image.size)
|
||||
}
|
||||
metadataName.endsWith(".ccd") -> {
|
||||
val ccdText = readMetadataText(contentResolver, selected.metadata)
|
||||
parseCcd(ccdText, image.size)
|
||||
}
|
||||
metadataName.endsWith(".mds") -> {
|
||||
val mdsBytes = readMetadataBytes(contentResolver, selected.metadata)
|
||||
parseMds(mdsBytes)
|
||||
}
|
||||
else -> throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return CdImageReader(image, tracks)
|
||||
} catch (e: Exception) {
|
||||
image.close()
|
||||
throw e
|
||||
@@ -136,23 +151,60 @@ internal class CdImageReader private constructor(
|
||||
|
||||
val imageFiles = files.filter {
|
||||
val name = it.displayName.lowercase(Locale.US)
|
||||
name.endsWith(".bin") || name.endsWith(".img")
|
||||
name.endsWith(".bin") || name.endsWith(".img") || name.endsWith(".mdf")
|
||||
}
|
||||
val cueFiles = files.filter { it.displayName.lowercase(Locale.US).endsWith(".cue") }
|
||||
if (imageFiles.size == 1 && cueFiles.isEmpty() && files.size == 1 ||
|
||||
imageFiles.isEmpty() && cueFiles.size == 1 && files.size == 1
|
||||
val metadataFiles = files.filter {
|
||||
val name = it.displayName.lowercase(Locale.US)
|
||||
name.endsWith(".cue") || name.endsWith(".ccd") || name.endsWith(".mds")
|
||||
}
|
||||
if (imageFiles.size == 1 && metadataFiles.isEmpty() && files.size == 1 ||
|
||||
imageFiles.isEmpty() && metadataFiles.size == 1 && files.size == 1
|
||||
) {
|
||||
throw InstallFailureException(R.string.missing_cd_image_metadata)
|
||||
}
|
||||
if (imageFiles.size != 1 || cueFiles.size != 1 || files.size != 2) {
|
||||
if (imageFiles.size != 1 || metadataFiles.size != 1 || files.size != 2) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
val imageBase = imageFiles.single().baseName()
|
||||
val cueBase = cueFiles.single().baseName()
|
||||
if (imageBase != cueBase) {
|
||||
val metadataBase = metadataFiles.single().baseName()
|
||||
if (imageBase != metadataBase) {
|
||||
throw InstallFailureException(R.string.missing_cd_image_metadata)
|
||||
}
|
||||
return SelectedImageFiles(imageFiles.single(), cueFiles.single())
|
||||
if (!isSupportedMetadataForImage(imageFiles.single(), metadataFiles.single())) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return SelectedImageFiles(imageFiles.single(), metadataFiles.single())
|
||||
}
|
||||
|
||||
private fun isSupportedMetadataForImage(
|
||||
image: SelectedInstallFile,
|
||||
metadata: SelectedInstallFile,
|
||||
): Boolean {
|
||||
val imageName = image.displayName.lowercase(Locale.US)
|
||||
val metadataName = metadata.displayName.lowercase(Locale.US)
|
||||
return if (imageName.endsWith(".mdf")) {
|
||||
metadataName.endsWith(".mds")
|
||||
} else {
|
||||
metadataName.endsWith(".cue") || metadataName.endsWith(".ccd")
|
||||
}
|
||||
}
|
||||
|
||||
private fun readMetadataText(
|
||||
contentResolver: ContentResolver,
|
||||
file: SelectedInstallFile,
|
||||
): String {
|
||||
return contentResolver.openInputStream(file.uri)?.bufferedReader()?.use {
|
||||
it.readText()
|
||||
} ?: throw InstallFailureException(R.string.cd_image_read_error)
|
||||
}
|
||||
|
||||
private fun readMetadataBytes(
|
||||
contentResolver: ContentResolver,
|
||||
file: SelectedInstallFile,
|
||||
): ByteArray {
|
||||
return contentResolver.openInputStream(file.uri)?.use {
|
||||
it.readBytes()
|
||||
} ?: throw InstallFailureException(R.string.cd_image_read_error)
|
||||
}
|
||||
|
||||
private fun parseCue(cueText: String, imageSize: Long): List<TrackInfo?> {
|
||||
@@ -194,6 +246,91 @@ internal class CdImageReader private constructor(
|
||||
return makeTrackInfo(cueTracks, imageSize)
|
||||
}
|
||||
|
||||
private fun parseCcd(ccdText: String, imageSize: Long): List<TrackInfo?> {
|
||||
val cueTracks = mutableListOf<CueTrack?>()
|
||||
var currentTrack: Int? = null
|
||||
for (line in ccdText.lines()) {
|
||||
val trimmed = line.trim()
|
||||
val trackMatch = Regex("""\[TRACK ([0-9]+)]""").matchEntire(trimmed)
|
||||
if (trackMatch != null) {
|
||||
currentTrack = trackMatch.groupValues[1].toInt()
|
||||
while (cueTracks.size <= currentTrack) {
|
||||
cueTracks.add(null)
|
||||
}
|
||||
cueTracks[currentTrack] = CueTrack(false, 2352, 16)
|
||||
continue
|
||||
}
|
||||
val trackNumber = currentTrack ?: continue
|
||||
val keyValue = trimmed.split("=", limit = 2)
|
||||
if (keyValue.size != 2) {
|
||||
continue
|
||||
}
|
||||
when (keyValue[0].uppercase(Locale.US)) {
|
||||
"MODE" -> if (keyValue[1] == "0") {
|
||||
cueTracks[trackNumber] = CueTrack(true, 2352, 0, cueTracks[trackNumber]?.index ?: mutableMapOf())
|
||||
}
|
||||
"INDEX 0" -> cueTracks[trackNumber]?.index?.put(
|
||||
0,
|
||||
keyValue[1].toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
)
|
||||
"INDEX 1" -> cueTracks[trackNumber]?.index?.put(
|
||||
1,
|
||||
keyValue[1].toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
)
|
||||
}
|
||||
}
|
||||
return makeTrackInfo(cueTracks, imageSize)
|
||||
}
|
||||
|
||||
private fun parseMds(mdsBytes: ByteArray): List<TrackInfo?> {
|
||||
if (mdsBytes.size < 0x70 ||
|
||||
String(mdsBytes, 0, 16, Charsets.US_ASCII).trimEnd('\u0000') != "MEDIA DESCRIPTOR"
|
||||
) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
val entries = mdsBytes[0x62].toInt() and 0xff
|
||||
if (0x70 + entries * 0x58 > mdsBytes.size) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
|
||||
val tracks = MutableList<TrackInfo?>(100) { null }
|
||||
for (i in 0 until entries) {
|
||||
val trackOffset = 0x70 + i * 0x50
|
||||
val extraOffset = 0x70 + entries * 0x50 + i * 8
|
||||
val mode = mdsBytes[trackOffset].toInt() and 0xff
|
||||
val trackNumber = mdsBytes[trackOffset + 0x04].toInt() and 0xff
|
||||
val sectorSize = readLittleEndianShort(mdsBytes, trackOffset + 0x10)
|
||||
val imageOffset = readLittleEndianInt(mdsBytes, trackOffset + 0x28)
|
||||
val sectors = readLittleEndianInt(mdsBytes, extraOffset + 0x04).toInt()
|
||||
if (trackNumber >= tracks.size) {
|
||||
continue
|
||||
}
|
||||
tracks[trackNumber] = when (mode) {
|
||||
MDS_MODE_AUDIO -> TrackInfo(
|
||||
isAudio = true,
|
||||
offset = imageOffset,
|
||||
blockSize = sectorSize,
|
||||
blockOffset = 0,
|
||||
startSector = 0,
|
||||
numSectors = sectors,
|
||||
)
|
||||
MDS_MODE_MODE1 -> TrackInfo(
|
||||
isAudio = false,
|
||||
offset = imageOffset,
|
||||
blockSize = sectorSize,
|
||||
blockOffset = 16,
|
||||
startSector = 0,
|
||||
numSectors = sectors,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (tracks.getOrNull(1)?.isAudio != false) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
|
||||
private fun makeTrackInfo(cueTracks: List<CueTrack?>, imageSize: Long): List<TrackInfo?> {
|
||||
val tracks = MutableList<TrackInfo?>(cueTracks.size) { null }
|
||||
var offset = 0L
|
||||
@@ -276,6 +413,18 @@ internal class CdImageReader private constructor(
|
||||
buffer[offset + 2] = (value shr 16).toByte()
|
||||
buffer[offset + 3] = (value shr 24).toByte()
|
||||
}
|
||||
|
||||
private fun readLittleEndianShort(buffer: ByteArray, offset: Int): Int {
|
||||
return (buffer[offset].toInt() and 0xff) or
|
||||
((buffer[offset + 1].toInt() and 0xff) shl 8)
|
||||
}
|
||||
|
||||
private fun readLittleEndianInt(buffer: ByteArray, offset: Int): Long {
|
||||
return (buffer[offset].toLong() and 0xff) or
|
||||
((buffer[offset + 1].toLong() and 0xff) shl 8) or
|
||||
((buffer[offset + 2].toLong() and 0xff) shl 16) or
|
||||
((buffer[offset + 3].toLong() and 0xff) shl 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<string name="install_error">ゲームのインストールに失敗しました。</string>
|
||||
<string name="install_progress">%s を展開中</string>
|
||||
<string name="invalid_iso9660_image">有効な ISO9660 イメージではありません。</string>
|
||||
<string name="missing_cd_image_metadata">.bin または .img ファイルと対応する .cue ファイルを一緒に選択してください。</string>
|
||||
<string name="missing_cd_image_metadata">CD イメージファイルと対応するメタデータファイルを一緒に選択してください。ファイル選択画面ではファイルを長押しすると複数選択モードに入れます。</string>
|
||||
<string name="no_data_to_import">ZIP アーカイブ中に save/ フォルダが見つかりません。</string>
|
||||
<string name="save_data_export_error">セーブデータのエクスポートに失敗しました。</string>
|
||||
<string name="save_data_export_success">エクスポートに成功しました。</string>
|
||||
@@ -29,7 +29,7 @@
|
||||
<string name="save_data_clear_success">セーブデータをクリアしました。</string>
|
||||
<string name="save_data_clear_error">セーブデータのクリアに失敗しました。</string>
|
||||
<string name="uninstall_dialog_message">「%s」をアンインストールしますか?</string>
|
||||
<string name="unsupported_cd_image">この CD イメージ形式にはまだ対応していません。現在は .iso と .bin/.img + .cue に対応しています。</string>
|
||||
<string name="unsupported_cd_image">この CD イメージ形式にはまだ対応していません。現在は .iso、.bin/.img + .cue/.ccd、.mdf/.mds に対応しています。</string>
|
||||
<string name="unsupported_zip">この形式の ZIP はサポートしていません。</string>
|
||||
<string name="zip_extraction_error">ZIP の展開に失敗しました。</string>
|
||||
<string name="usage">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<string name="install_error">Failed to install game.</string>
|
||||
<string name="install_progress">Extracting %s</string>
|
||||
<string name="invalid_iso9660_image">This file is not a valid ISO9660 image.</string>
|
||||
<string name="missing_cd_image_metadata">Select a matching .cue file with the .bin or .img file.</string>
|
||||
<string name="missing_cd_image_metadata">Select a matching metadata file with the CD image file. In the file picker, long-press a file to enter multi-select mode.</string>
|
||||
<string name="no_data_to_import">No save/ folder in the ZIP archive.</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="save_data_clear_error">Failed to clear save files.</string>
|
||||
@@ -29,7 +29,7 @@
|
||||
<string name="save_data_import_success">Imported successfully.</string>
|
||||
<string name="save_data_export_success">Exported successfully.</string>
|
||||
<string name="uninstall_dialog_message">Uninstall \"%s\"?</string>
|
||||
<string name="unsupported_cd_image">This CD image format is not supported yet. Currently only .iso and .bin/.img + .cue are supported.</string>
|
||||
<string name="unsupported_cd_image">This CD image format is not supported yet. Currently only .iso, .bin/.img + .cue/.ccd, and .mdf/.mds are supported.</string>
|
||||
<string name="unsupported_zip">This type of ZIP is not supported.</string>
|
||||
<string name="zip_extraction_error">Failed to extract ZIP.</string>
|
||||
<string name="usage">
|
||||
|
||||
Reference in New Issue
Block a user