├── .gitignore ├── LCSEPackageUtility ├── build.gradle ├── settings.gradle └── src │ ├── main │ └── kotlin │ │ └── charlie │ │ └── lcsetools │ │ └── pkgutil │ │ ├── LCSEPackage.kt │ │ ├── LCSEPackageMain.kt │ │ └── LCSEPatch.kt │ └── test │ └── kotlin │ └── charlie │ └── lcsetools │ └── pkgutil │ └── LCSEPackageTest.kt ├── LCSESNXUtility ├── .idea │ ├── eclipseCodeFormatter.xml │ ├── kotlinc.xml │ └── libraries │ │ └── KotlinJavaRuntime.xml ├── build.gradle ├── settings.gradle └── src │ ├── main │ └── kotlin │ │ └── charlie │ │ └── lcsetools │ │ └── snxutil │ │ ├── BatchRunner.kt │ │ ├── gettext │ │ └── GetTextWriter.kt │ │ ├── parsed │ │ ├── LCSEParsedScript.kt │ │ ├── LCSESNXParser.kt │ │ └── LCSESNXWriter.kt │ │ ├── persistence │ │ └── PersistenceHelper.kt │ │ └── raw │ │ ├── LCSERawSNXScript.kt │ │ └── LCSESNXProcessor.kt │ └── test │ └── kotlin │ └── charlie │ └── lcsetools │ └── snxutil │ ├── gettext │ └── GetTextWriterTest.kt │ ├── parsed │ └── LCSESNXParserKtTest.kt │ └── raw │ └── LCSESNXProcessorTest.kt ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | \.gradle/ 25 | build/ 26 | \.idea/ 27 | out/ 28 | -------------------------------------------------------------------------------- /LCSEPackageUtility/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.github.cqjjjzr' 2 | version 'rv4' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.7.0' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | apply plugin: 'kotlin' 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 23 | implementation group: 'commons-io', name: 'commons-io', version: '2.11.0' 24 | implementation group: 'commons-cli', name: 'commons-cli', version: '1.5.0' 25 | 26 | testImplementation group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit', version: kotlin_version 27 | testImplementation group: 'junit', name: 'junit', version:'4.13.2' 28 | } 29 | 30 | jar { 31 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 32 | 33 | manifest { 34 | attributes "Main-Class": "charlie.lcsetools.pkgutil.LCSEPackageMainKt" 35 | } 36 | 37 | from { 38 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LCSEPackageUtility/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'LCSEPackageUtility' 2 | 3 | -------------------------------------------------------------------------------- /LCSEPackageUtility/src/main/kotlin/charlie/lcsetools/pkgutil/LCSEPackage.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.pkgutil 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.ByteOrder 5 | import java.nio.channels.FileChannel 6 | import java.nio.charset.Charset 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import java.nio.file.StandardOpenOption 10 | import java.util.* 11 | import kotlin.experimental.xor 12 | 13 | var keyIndex = 0x02020202 14 | fun indexDecryptInt(original: Int) = original xor keyIndex 15 | val indexEncryptInt = ::indexDecryptInt 16 | 17 | var keySNX = 0x03030303 18 | fun snxDecryptInt(original: Int) = original xor keySNX 19 | val snxEncryptInt = ::snxDecryptInt 20 | internal fun Int.expandByteToInt(): Int { 21 | return this + (this shl 8) + (this shl 16) + (this shl 24) 22 | } 23 | 24 | val SHIFT_JIS = Charset.forName("SHIFT-JIS")!! 25 | 26 | fun createIfNotExist(listPath: Path) { 27 | if (!Files.exists(listPath)) Files.createFile(listPath) 28 | } 29 | 30 | class LCSEIndexList { 31 | companion object { 32 | fun readFromBuffer(buffer: ByteBuffer): LCSEIndexList { 33 | val arr = ByteArray(buffer.limit()) 34 | buffer.position(0) 35 | buffer[arr, 0, buffer.limit()] 36 | 37 | val buf = ByteBuffer.wrap(arr).order(ByteOrder.LITTLE_ENDIAN) 38 | val entriesCount = indexDecryptInt(buf.int) 39 | 40 | if (arr.size != 4 + entriesCount * LCSEIndexEntry.ENTRY_SIZE) 41 | throw IllegalArgumentException("文件长度错误!") 42 | 43 | return LCSEIndexList().apply { 44 | repeat(entriesCount) { 45 | entries += LCSEIndexEntry.readFromBuffer(buf) 46 | } 47 | } 48 | } 49 | } 50 | 51 | var entries: MutableList = ArrayList() 52 | 53 | fun getByteArray(): ByteArray { 54 | return ByteBuffer.allocate(4 + entries.size * LCSEIndexEntry.ENTRY_SIZE) 55 | .order(ByteOrder.LITTLE_ENDIAN) 56 | .apply { 57 | putInt(indexEncryptInt(entries.size)) 58 | entries.forEach { put(it.getByteArray()) } 59 | } 60 | .array() 61 | } 62 | 63 | fun writePatchedPackage(patches: Iterable, originalPackage: FileChannel, listPath: Path, packagePath: Path) { 64 | Files.createDirectories(listPath.parent) 65 | 66 | createIfNotExist(listPath) 67 | 68 | if (Files.exists(packagePath)) Files.delete(packagePath) 69 | Files.createFile(packagePath) 70 | 71 | val newList = LCSEIndexList() 72 | FileChannel.open(packagePath, StandardOpenOption.WRITE).use { packageChannel -> 73 | entries.forEach { oldEntry -> 74 | val applicablePatches = patches 75 | .filter { it.filename == oldEntry.filename && it.type == oldEntry.type } 76 | if (applicablePatches.isNotEmpty()) { 77 | val patch = applicablePatches.first() 78 | Files.newByteChannel(patch.newFilePath).use { patchChannel -> 79 | ByteBuffer.allocate(Files.size(patch.newFilePath).toInt()).apply { 80 | patchChannel.read(this) 81 | flip() 82 | if (patch.type == LCSEResourceType.SCRIPT) { 83 | val arr = array()!! 84 | arr.forEachIndexed { i, original -> 85 | arr[i] = original xor keySNX.toByte() 86 | } 87 | } 88 | packageChannel.write(this) 89 | } 90 | newList.entries += LCSEIndexEntry( 91 | (packageChannel.position() - patchChannel.size()).toInt(), 92 | Files.size(patch.newFilePath).toInt(), 93 | oldEntry.filename, 94 | oldEntry.type) 95 | } 96 | } else { 97 | originalPackage.position(oldEntry.offset.toLong()) 98 | ByteBuffer.allocate(oldEntry.length).apply { 99 | originalPackage.read(this) 100 | flip() 101 | packageChannel.write(this) 102 | } 103 | newList.entries += LCSEIndexEntry( 104 | (packageChannel.position() - oldEntry.length).toInt(), 105 | oldEntry.length, 106 | oldEntry.filename, 107 | oldEntry.type) 108 | } 109 | } 110 | } 111 | 112 | Files.write(listPath, newList.getByteArray(), StandardOpenOption.TRUNCATE_EXISTING) 113 | } 114 | 115 | override fun toString(): String { 116 | return "IndexList(entries=$entries)" 117 | } 118 | } 119 | 120 | class LCSEIndexEntry(var offset: Int, 121 | var length: Int, 122 | var filename: String, 123 | var type: LCSEResourceType) { 124 | companion object { 125 | val ENTRY_SIZE = 76 126 | val FILENAME_SIZE = 0x40 127 | 128 | fun readFromBuffer(buffer: ByteBuffer): LCSEIndexEntry { 129 | val arr = ByteArray(ENTRY_SIZE) 130 | buffer[arr, 0, ENTRY_SIZE] 131 | 132 | val buf = ByteBuffer.wrap(arr).order(ByteOrder.LITTLE_ENDIAN) 133 | val offset = indexDecryptInt(buf.int) 134 | val length = indexDecryptInt(buf.int) 135 | var nameBuf = ByteArray(FILENAME_SIZE) 136 | buf[nameBuf, 0, FILENAME_SIZE] 137 | nameBuf = nameBuf 138 | .takeWhile { it != 0.toByte() } 139 | .map { it xor keyIndex.toByte() } 140 | .toByteArray() 141 | val filename = String(nameBuf, SHIFT_JIS) 142 | val type = LCSEResourceType.forId(buf.int) 143 | 144 | return LCSEIndexEntry(offset, length, filename, type) 145 | } 146 | } 147 | 148 | val fullFilename get() = filename + type.extensionName 149 | 150 | fun getByteArray(): ByteArray { 151 | return ByteBuffer.allocate(ENTRY_SIZE) 152 | .order(ByteOrder.LITTLE_ENDIAN) 153 | .apply { 154 | putInt(indexEncryptInt(offset)) 155 | putInt(indexEncryptInt(length)) 156 | val srcName = filename 157 | .toByteArray(SHIFT_JIS) 158 | .map { it xor keyIndex.toByte() } 159 | .toByteArray() 160 | val tempBuf = ByteArray(FILENAME_SIZE) 161 | System.arraycopy(srcName, 0, tempBuf, 0, minOf(srcName.size, FILENAME_SIZE - 1)) 162 | put(tempBuf) 163 | putInt(type.typeId) 164 | }.array() 165 | } 166 | 167 | fun extractFromChannel(channel: FileChannel) 168 | = channel.map(FileChannel.MapMode.READ_ONLY, offset.toLong(), length.toLong()) 169 | .let { if (!it.hasArray()) ByteArray(it.limit()).apply { it[this, 0, size] } else it.array()!! } 170 | .let { 171 | if (type == LCSEResourceType.SCRIPT) { 172 | it.map { it xor keySNX.toByte() }.toByteArray() 173 | } else it 174 | } 175 | 176 | fun extractFromChannelToFile(channel: FileChannel, outDirectory: Path) { 177 | Files.createDirectories(outDirectory) 178 | outDirectory.resolve(fullFilename).apply { 179 | createIfNotExist(this) 180 | Files.write(this, extractFromChannel(channel), StandardOpenOption.TRUNCATE_EXISTING) 181 | } 182 | } 183 | 184 | override fun toString(): String { 185 | return "[IndexEntry(offset=$offset, length=$length, filename='$filename', type=$type)]" 186 | } 187 | } 188 | 189 | enum class LCSEResourceType 190 | constructor(internal val typeId: Int, 191 | internal val extensionName: String) { 192 | SCRIPT(1, ".snx"), 193 | BMP_PICTURE(2, ".bmp"), 194 | PNG_PICTURE(3, ".png"), 195 | WAVE_AUDIO(4, ".wav"), 196 | OGG_AUDIO(5, ".ogg"), 197 | UNKNOWN_1(-1, ".dat"), 198 | UNKNOWN_2(0, ".dat2"); 199 | 200 | companion object { 201 | fun forId(typeId: Int): LCSEResourceType { 202 | return LCSEResourceType.values() 203 | .filter { it.typeId == typeId } 204 | .firstOrNull() ?: SCRIPT 205 | } 206 | 207 | fun forExtensionName(extensionName: String): LCSEResourceType { 208 | return LCSEResourceType.values() 209 | .filter { it.extensionName == 210 | if (extensionName.startsWith(".")) 211 | extensionName.lowercase() 212 | else "." + extensionName.lowercase() } 213 | .firstOrNull() ?: UNKNOWN_1 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /LCSEPackageUtility/src/main/kotlin/charlie/lcsetools/pkgutil/LCSEPackageMain.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.pkgutil 2 | 3 | import org.apache.commons.cli.* 4 | import java.io.File 5 | import java.nio.channels.FileChannel 6 | import java.nio.file.Paths 7 | import kotlin.system.exitProcess 8 | 9 | val VERSION = "rv4" 10 | 11 | val opts = Options().apply { 12 | addOption("h", "help", false, "显示帮助") 13 | addOptionGroup(OptionGroup().apply { 14 | addOption(Option("u", "unpack", false, "解包。")) 15 | addOption(Option("r", "patch", false, "封包(需与-e配合使用)。")) 16 | isRequired = true 17 | }) 18 | addOption(Option("s", "process-snx", false, "是否处理SNX格式数据")) 19 | addOption(Option("p", "process-png", false, "是否处理PNG格式数据")) 20 | addOption(Option("b", "process-bmp", false, "是否处理BMP格式数据")) 21 | addOption(Option("w", "process-wav", false, "是否处理WAV格式数据")) 22 | addOption(Option("o", "process-ogg", false, "是否处理OGG格式数据")) 23 | 24 | addOption(Option("l", "list", true, "指定.lst清单文件").apply { isRequired = true; argName = "lst-file" }) 25 | addOption(Option("a", "package", true, "指定lcsebody数据包文件").apply { isRequired = true; argName = "package-file" }) 26 | addOption(Option("d", "out-dir", true, "指定输出目录").apply { argName = "out-dir" }) 27 | addOption(Option("e", "patch-dir", true, "指定用以替换数据包中文件的文件所在目录") 28 | .apply { argName = "patch-dir" }) 29 | addOption(Option("k", "key", true, "指定.lst清单文件的的加密key,格式为16进制两位数字")) 30 | addOption(Option("K", "key-snx", true, "指定SNX文件的的加密key,格式为16进制两位数字")) 31 | } 32 | 33 | fun main(vararg args: String) { 34 | println("LC-ScriptEngine资源包封包处理实用工具 $VERSION\n\tBy Charlie Jiang\n\n") 35 | try { 36 | DefaultParser().parse(opts, args).apply { 37 | if (options.isEmpty() || hasOption('h') 38 | || (hasOption('r') && !hasOption('e')) || (hasOption('u') && hasOption('e'))) { 39 | printUsageAndBoom() 40 | } 41 | if (hasOption('k')) 42 | keyIndex = getOptionValue('k').toInt(16).expandByteToInt() 43 | if (hasOption('K')) 44 | keySNX = getOptionValue('K').toInt(16).expandByteToInt() 45 | 46 | FileChannel.open(Paths.get(getOptionValue('l').removeSurrounding("\""))).use { 47 | it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()).let { listBuffer -> 48 | FileChannel.open(Paths.get(getOptionValue('a').removeSurrounding("\""))).use { packageChannel -> 49 | val outDirectory = 50 | if (hasOption('d')) Paths.get(getOptionValue('d').removeSurrounding("\"").removeSuffix("\"")) 51 | else if (hasOption('u')) Paths.get(".", "extracted") else Paths.get(".", "patched") 52 | if (hasOption('u')) { 53 | LCSEIndexList.readFromBuffer(listBuffer) 54 | .entries 55 | .filter { it.type != LCSEResourceType.SCRIPT || hasOption('s') } 56 | .filter { it.type != LCSEResourceType.PNG_PICTURE || hasOption('p') } 57 | .filter { it.type != LCSEResourceType.BMP_PICTURE || hasOption('b') } 58 | .filter { it.type != LCSEResourceType.WAVE_AUDIO || hasOption('w') } 59 | .filter { it.type != LCSEResourceType.OGG_AUDIO || hasOption('o') } 60 | .forEach { 61 | it.extractFromChannelToFile(packageChannel, outDirectory) 62 | print("提取:${it.fullFilename} \r") 63 | } 64 | } else if (hasOption('r')) { 65 | val patches = File(getOptionValue('e').removeSurrounding("\"")).walkTopDown() 66 | .filter { it.isFile } 67 | .map { try { LCSEPatch(it.toPath()) } catch(e: Exception) { null } } 68 | .filterNotNull() 69 | .filter { it.type != LCSEResourceType.SCRIPT || hasOption('s') } 70 | .filter { it.type != LCSEResourceType.PNG_PICTURE || hasOption('p') } 71 | .filter { it.type != LCSEResourceType.BMP_PICTURE || hasOption('b') } 72 | .filter { it.type != LCSEResourceType.WAVE_AUDIO || hasOption('w') } 73 | .filter { it.type != LCSEResourceType.OGG_AUDIO || hasOption('o') } 74 | .asIterable() 75 | LCSEIndexList 76 | .readFromBuffer(listBuffer) 77 | .writePatchedPackage( 78 | patches, 79 | packageChannel, 80 | outDirectory.resolve(Paths.get(getOptionValue('l').removeSurrounding("\"")).fileName), 81 | outDirectory.resolve(Paths.get(getOptionValue('a').removeSurrounding("\"")).fileName)) 82 | 83 | } 84 | 85 | } 86 | } 87 | } 88 | } 89 | } catch(e: ParseException) { 90 | e.printStackTrace() 91 | printUsageAndBoom() 92 | } 93 | 94 | println("\n完成。") 95 | } 96 | 97 | fun printUsageAndBoom(): Nothing { 98 | HelpFormatter().printHelp("java -jar LCSEPackage.jar <-u/r/h> <-l xxx> <-a xxx> [其他开关/参数]", opts) 99 | exitProcess(0) 100 | } -------------------------------------------------------------------------------- /LCSEPackageUtility/src/main/kotlin/charlie/lcsetools/pkgutil/LCSEPatch.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.pkgutil 2 | 3 | import org.apache.commons.io.FilenameUtils 4 | import java.nio.file.Path 5 | 6 | data class LCSEPatch(val newFilePath: Path) { 7 | val filename: String = FilenameUtils.getBaseName(newFilePath.toString()) 8 | val type: LCSEResourceType = LCSEResourceType.forExtensionName(FilenameUtils.getExtension(newFilePath.toString())) 9 | } 10 | -------------------------------------------------------------------------------- /LCSEPackageUtility/src/test/kotlin/charlie/lcsetools/pkgutil/LCSEPackageTest.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.pkgutil 2 | 3 | import org.junit.Ignore 4 | import org.junit.Test 5 | import java.nio.channels.FileChannel 6 | import java.nio.file.Files 7 | import java.nio.file.Paths 8 | import java.nio.file.StandardOpenOption 9 | 10 | class LCSEIndexTest { 11 | @Test 12 | fun dummy() { } 13 | 14 | @Ignore 15 | fun test() { 16 | val channel = FileChannel.open(Paths.get("D:", "无限錬姦", "mugen", "lcsebody1.lst")) 17 | val buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()) 18 | 19 | Paths.get("D:", "无限錬姦", "mugen", "lcsebody1_exported.lst").apply { 20 | if (!Files.exists(this)) Files.createFile(this) 21 | Files.write(this, LCSEIndexList.readFromBuffer(buf).getByteArray(), StandardOpenOption.TRUNCATE_EXISTING) 22 | } 23 | } 24 | 25 | @Ignore 26 | fun test1() { 27 | //val channel = FileChannel.open(Paths.get("D:", "无限錬姦", "mugen", "lcsebody1.lst")) 28 | val channel = FileChannel.open(Paths.get("D:", "无限錬姦", "DISK1", "WinRoot", "Liquid", "mugen", "lcsebody1.lst")) 29 | val archiveChannel = FileChannel.open(Paths.get("D:", "无限錬姦", "DISK1", "WinRoot", "Liquid", "mugen", "lcsebody1")) 30 | val buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()) 31 | 32 | LCSEIndexList.readFromBuffer(buf).entries 33 | .filter { it.filename == "_TITLE" && it.type == LCSEResourceType.SCRIPT } 34 | .forEach { 35 | it.extractFromChannelToFile(archiveChannel, Paths.get("D:", "无限錬姦", "mugen", "n")) 36 | } 37 | } 38 | 39 | @Ignore 40 | fun test2() { 41 | val channel = FileChannel.open(Paths.get("D:", "无限錬姦", "DISK1", "WinRoot", "Liquid", "mugen", "lcsebody1.lst")) 42 | val archiveChannel = FileChannel.open(Paths.get("D:", "无限錬姦", "DISK1", "WinRoot", "Liquid", "mugen", "lcsebody1")) 43 | val buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()) 44 | 45 | val outDirectory = Paths.get("D:", "无限錬姦", "mugen", "m") 46 | LCSEIndexList.readFromBuffer(buf).writePatchedPackage( 47 | Paths.get("D:", "无限錬姦", "mugen", "patches").toFile() 48 | .walk().filter { it.isFile }.map { LCSEPatch(it.toPath()) }.asIterable(), archiveChannel, 49 | outDirectory.resolve("lcsebody1.lst"), 50 | outDirectory.resolve("lcsebody1")) 51 | channel.close() 52 | archiveChannel.close() 53 | } 54 | } -------------------------------------------------------------------------------- /LCSESNXUtility/.idea/eclipseCodeFormatter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | -------------------------------------------------------------------------------- /LCSESNXUtility/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /LCSESNXUtility/.idea/libraries/KotlinJavaRuntime.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LCSESNXUtility/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.github.cqjjjzr' 2 | version 'rv1' 3 | 4 | buildscript { 5 | ext { 6 | kotlinVersion = '1.3.11' 7 | } 8 | repositories { 9 | mavenCentral() 10 | } 11 | dependencies { 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 13 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 14 | } 15 | } 16 | 17 | apply plugin: 'kotlin' 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" 25 | compile group: 'org.fedorahosted.tennera', name: 'jgettext', version: '0.14' 26 | 27 | testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit', version:'1.1.0' 28 | testCompile group: 'junit', name: 'junit', version:'4.12' 29 | } -------------------------------------------------------------------------------- /LCSESNXUtility/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'LCSESNXUtility' 2 | 3 | -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/BatchRunner.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil 2 | 3 | import charlie.lcsetools.snxutil.gettext.patchScript 4 | import charlie.lcsetools.snxutil.parsed.generateRawSNXScript 5 | import charlie.lcsetools.snxutil.parsed.parseRawSNXScript 6 | import charlie.lcsetools.snxutil.raw.readSNXScript 7 | import charlie.lcsetools.snxutil.raw.writeSNXScript 8 | import java.io.File 9 | import java.io.StringReader 10 | import java.nio.channels.FileChannel 11 | import java.nio.charset.Charset 12 | import java.nio.file.Files 13 | import java.nio.file.Paths 14 | import java.nio.file.StandardOpenOption 15 | 16 | fun main(args: Array) { 17 | File("""D:\gal\MUGENRANKAN\mugen_ere\dictInjected1""").walkTopDown().forEach { poFile -> 18 | if (!poFile.isFile || poFile.extension != "po") return@forEach 19 | println(poFile.absolutePath) 20 | 21 | val txt = poFile.readText().replace("\\v", "") 22 | val originalPath = Paths.get("D:\\gal\\MUGENRANKAN\\mugen_ere\\lcsebody1Extracted\\${poFile.nameWithoutExtension}.snx") 23 | val resultPath = Paths.get("D:\\gal\\MUGENRANKAN\\mugen_ere\\patchedInjectSNX2\\${poFile.nameWithoutExtension}.snx") 24 | if (!Files.exists(originalPath)) { 25 | println("error: Not found: $originalPath") 26 | return 27 | } 28 | 29 | FileChannel.open(originalPath, StandardOpenOption.READ).use { 30 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 31 | val parsedSNX = parseRawSNXScript(readSNXScript(originalBuf)) 32 | patchScript(parsedSNX, StringReader(txt), poFile.nameWithoutExtension) 33 | 34 | if (!Files.exists(resultPath)) Files.createFile(resultPath) 35 | FileChannel.open(resultPath, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE).use { channel -> 36 | channel.write(writeSNXScript(generateRawSNXScript(parsedSNX))) 37 | } 38 | } 39 | } 40 | 41 | /*val originalPath = Paths.get("D:\\gal\\无限錬姦\\mugen\\originalSNX\\CHOICE_01.snx") 42 | val resultPath = Paths.get("D:\\gal\\无限錬姦\\mugen\\patchedSNX\\CHOICE_01.snx") 43 | if (!Files.exists(originalPath)) { 44 | println("error: Not found: $originalPath") 45 | return 46 | } 47 | 48 | FileChannel.open(originalPath, StandardOpenOption.READ).use { 49 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 50 | val raw = readSNXScript(originalBuf) 51 | val parsedSNX = parseRawSNXScript(raw) 52 | //patchScript(parsedSNX, poFile.toPath()) 53 | 54 | if (!Files.exists(resultPath)) Files.createFile(resultPath) 55 | FileChannel.open(resultPath, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE).use { channel -> 56 | channel.write(writeSNXScript(generateRawSNXScript(parsedSNX))) 57 | } 58 | }*/ 59 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/gettext/GetTextWriter.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.gettext 2 | 3 | import charlie.lcsetools.snxutil.parsed.LCSEChoiceString 4 | import charlie.lcsetools.snxutil.parsed.LCSEDialogString 5 | import charlie.lcsetools.snxutil.parsed.LCSEParsedScript 6 | import charlie.lcsetools.snxutil.parsed.LCSESpeaker 7 | import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream 8 | import org.fedorahosted.tennera.jgettext.Catalog 9 | import org.fedorahosted.tennera.jgettext.Message 10 | import org.fedorahosted.tennera.jgettext.PoParser 11 | import org.fedorahosted.tennera.jgettext.PoWriter 12 | import java.io.File 13 | import java.io.Reader 14 | import java.nio.file.Files 15 | import java.nio.file.Path 16 | import java.nio.file.StandardOpenOption 17 | 18 | fun writeCatalog(catalog: Catalog, path: Path): Boolean { 19 | if (catalog.isEmpty) { 20 | println("[WARNING] Empty file: $path") 21 | return false 22 | } 23 | if (!Files.exists(path)) Files.createFile(path) 24 | val stream = ByteOutputStream() 25 | PoWriter().write(catalog, stream, Charsets.UTF_8) 26 | 27 | val bytes = stream.bytes 28 | val res = bytes.sliceArray(0 until (bytes.indexOf(0x00).takeIf { it != -1 } ?: bytes.size)) 29 | 30 | Files.write(path, res, StandardOpenOption.TRUNCATE_EXISTING) 31 | return true 32 | } 33 | 34 | fun writePoTemplate(script: LCSEParsedScript, path: Path): Boolean { 35 | if (script.strings.isEmpty()) { 36 | println("[WARNING] Empty file: $path") 37 | return false 38 | } 39 | 40 | return writeCatalog(Catalog().apply { 41 | script.speakers.forEach { 42 | addMessage(Message().apply { 43 | msgctxt = "speaker" 44 | msgid = it.name 45 | msgstr = "" 46 | addComment(it.ordinal.toString()) 47 | }) 48 | } 49 | script.strings 50 | .forEach { 51 | when (it) { 52 | is LCSEDialogString -> { 53 | addMessage(Message().apply { 54 | msgctxt = "dialog" 55 | msgid = it.content.removePrefix("\u3000") + "|" + it.ordinal 56 | msgstr = "" 57 | addComment("${it.ordinal}|${it.speakerOrdinal}|${it.content.startsWith("\u3000")}") 58 | }) 59 | } 60 | is LCSEChoiceString -> { 61 | addMessage(Message().apply { 62 | msgctxt = "choice" 63 | msgid = it.content.removePrefix("\u3000") + "|" + it.ordinal 64 | msgstr = "" 65 | addComment("${it.ordinal}|${it.content.startsWith("\\u3000")}") 66 | }) 67 | } 68 | } 69 | } 70 | }, path) 71 | } 72 | 73 | fun convertDialogOrChoiceString(msg: Message, context: String, ordinal: Int): String { 74 | return buildString { 75 | if (msg.comments.first().endsWith("true")) append("\u3000") 76 | //append('[') 77 | //append(context) 78 | //append('$') 79 | //append(ordinal) 80 | //append("] ") 81 | (if (msg.msgstr.isNotEmpty()) 82 | msg.msgstr 83 | else msg.msgid) 84 | .substringBeforeLast('|') 85 | .replace("......", "\u2026\u2026") 86 | .let(::append) 87 | } 88 | } 89 | 90 | fun patchScript(script: LCSEParsedScript, reader: Reader, context: String) { 91 | val catalog = PoParser().parseCatalog(reader, false) 92 | val speakerMsgs = catalog.filter { it.msgctxt == "speaker" } 93 | val dialogMsgs = catalog.filter { it.msgctxt == "dialog" && it.msgstr.isNotEmpty() } 94 | val choiceMsgs = catalog.filter { it.msgctxt == "choice" && it.msgstr.isNotEmpty() } 95 | script.speakers.replaceAll { original -> 96 | if (original.name == "") original 97 | else LCSESpeaker(original.ordinal, speakerMsgs.find { it.msgid == original.name }?.msgstr ?: throw Exception("bad file $context")) 98 | } 99 | script.strings.replaceAll { original -> 100 | when (original) { 101 | is LCSEDialogString -> { 102 | LCSEDialogString(original.ordinal, original.speakerOrdinal, 103 | dialogMsgs 104 | .find { it.msgid.substringBefore('|') == original.content 105 | || original.ordinal == it.comments.first().substringBefore('|').toInt() } 106 | ?.let { 107 | convertDialogOrChoiceString(it, context, original.ordinal) 108 | } ?: original.content, original.withTrailer) 109 | } 110 | is LCSEChoiceString -> { 111 | LCSEChoiceString(original.ordinal, 112 | choiceMsgs.find { 113 | original.ordinal == it.comments.first().substringBefore('|').toInt() 114 | }?.let { 115 | convertDialogOrChoiceString(it, context, original.ordinal) 116 | } ?: original.content) 117 | } 118 | else -> original 119 | } 120 | } 121 | } 122 | 123 | fun buildDictionary(dir: File): Map { 124 | val result = mutableMapOf() 125 | val reader = PoParser() 126 | dir.walkTopDown().filter { it.isFile && it.extension == "po" }.forEach { file -> 127 | val catalog = reader.parseCatalog(file) 128 | catalog.forEach { 129 | if (it.msgid.contains("Content-Transfer-Encoding")) 130 | it.msgid = "" 131 | if (it.msgstr.contains("Content-Transfer-Encoding")) 132 | it.msgstr = "" 133 | val key = it.msgid.substringBeforeLast('|').trim() 134 | if (result.containsKey(key) && result[key] != it.msgstr) { 135 | println("[WARNING] Inconsistent translation for ${key}: ${it.msgstr} <-> ${result[key]} in ${file.name}") 136 | } 137 | result[key] = it.msgstr 138 | } 139 | } 140 | 141 | return result 142 | } 143 | 144 | fun injectDictionary(template: Path, dest: Path, dict: Map) { 145 | try { 146 | val catalog = PoParser().parseCatalog(template.toFile()) 147 | catalog.forEach { 148 | val key = it.msgid.substringBeforeLast('|').trim() 149 | val t = dict[key] ?: return@forEach 150 | it.msgstr = t 151 | } 152 | writeCatalog(catalog, dest) 153 | } catch (ex: Exception) { 154 | println("[WARNING] Failed to inject $template") 155 | ex.printStackTrace() 156 | } 157 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/parsed/LCSEParsedScript.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.parsed 2 | 3 | import charlie.lcsetools.snxutil.raw.LCSERawSNXInstruction 4 | import charlie.lcsetools.snxutil.raw.LCSERawSNXScript 5 | import java.io.Serializable 6 | 7 | class LCSEParsedScript: Serializable { 8 | val strings: MutableList = ArrayList() 9 | val instructions: MutableList = ArrayList() 10 | 11 | val speakers: MutableList = ArrayList() 12 | } 13 | 14 | interface LCSEInstruction: Serializable { 15 | fun toRawInstruction(context: LCSERawSNXScript): LCSERawSNXInstruction 16 | } 17 | 18 | interface LCSEString: Serializable { 19 | val ordinal: Int 20 | fun convertToByteArray(context: LCSEParsedScript): ByteArray 21 | } 22 | 23 | data class LCSEUnknownInstruction(val instruction: Int, 24 | val param1: Int, 25 | val param2: Int): LCSEInstruction { 26 | override fun toRawInstruction(context: LCSERawSNXScript): LCSERawSNXInstruction 27 | = LCSERawSNXInstruction(instruction, param1, param2) 28 | } 29 | 30 | 31 | internal val STRING_REF_INST = 0x00000011 32 | internal val STRING_REF_PARAM1 = 0x00000002 33 | data class LCSEStringReferInstruction(val stringOrdinary: Int): LCSEInstruction { 34 | override fun toRawInstruction(context: LCSERawSNXScript): LCSERawSNXInstruction 35 | = LCSERawSNXInstruction(STRING_REF_INST, STRING_REF_PARAM1, context.strings.find { it.ordinal == stringOrdinary }!!.offset) 36 | } 37 | 38 | internal val DISPLAY_TEXT_INST = 0x0000000D 39 | internal val DISPLAY_TEXT_PARAM1 = 0x0000002C 40 | internal val DISPLAY_TEXT_PARAM2 = 0x00000000 41 | internal val DISPLAY_TEXT_RAW = LCSERawSNXInstruction(DISPLAY_TEXT_INST, DISPLAY_TEXT_PARAM1, DISPLAY_TEXT_PARAM2) 42 | class LCSEStringDisplayInstruction: LCSEInstruction { 43 | override fun toRawInstruction(context: LCSERawSNXScript): LCSERawSNXInstruction = DISPLAY_TEXT_RAW 44 | } 45 | 46 | internal val CHOICE_INST = 0x0000000D 47 | internal val CHOICE_PARAM1 = 0x0000004F 48 | internal val CHOICE_PARAM2 = 0x00000000 49 | internal val CHOICE_RAW = LCSERawSNXInstruction(CHOICE_INST, CHOICE_PARAM1, CHOICE_PARAM2) 50 | class LCSEChoiceInstruction: LCSEInstruction { 51 | override fun toRawInstruction(context: LCSERawSNXScript): LCSERawSNXInstruction = CHOICE_RAW 52 | } 53 | 54 | data class LCSESpeaker(val ordinal: Int, 55 | val name: String): Serializable 56 | 57 | data class LCSEDialogString(override val ordinal: Int, 58 | val speakerOrdinal: Int, 59 | val content: String, 60 | val withTrailer: Boolean = false): LCSEString { 61 | override fun convertToByteArray(context: LCSEParsedScript): ByteArray { 62 | return (context.speakers.find { it.ordinal == speakerOrdinal }!!.name + 63 | '\u0001' + content.replace('\n', '\u0001') + 64 | if (withTrailer) "\u0002\u0003\u0000" else "\u0000") 65 | .toByteArray(GBK) 66 | } 67 | } 68 | 69 | data class LCSEChoiceString(override val ordinal: Int, 70 | val content: String): LCSEString { 71 | override fun convertToByteArray(context: LCSEParsedScript): ByteArray { 72 | return (content + "\u0000").toByteArray(GBK) 73 | } 74 | } 75 | 76 | data class LCSESystemString(override val ordinal: Int, 77 | val content: String): LCSEString { 78 | override fun convertToByteArray(context: LCSEParsedScript): ByteArray { 79 | return (content + "\u0000").toByteArray(GBK) 80 | } 81 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/parsed/LCSESNXParser.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.parsed 2 | 3 | import charlie.lcsetools.snxutil.raw.LCSERawSNXScript 4 | import charlie.lcsetools.snxutil.raw.readSNXScript 5 | import java.nio.channels.FileChannel 6 | import java.nio.charset.Charset 7 | import java.nio.file.Paths 8 | import java.nio.file.StandardOpenOption 9 | import java.util.* 10 | 11 | val SHIFT_JIS = Charset.forName("Shift-JIS")!! 12 | val GBK = Charset.forName("GBK")!! 13 | 14 | fun parseRawSNXScript(script: LCSERawSNXScript): LCSEParsedScript 15 | = LCSEParsedScript().apply { 16 | val dialogStringOrdinals = ArrayList() 17 | val choiceStringOrdinals = ArrayList() 18 | script.instructions.forEach { (instruction, param1, param2) -> 19 | instructions += 20 | if (instruction == STRING_REF_INST && param1 == STRING_REF_PARAM1) 21 | LCSEStringReferInstruction(script.strings.indexOfFirst { it.offset == param2 }) 22 | else if (instruction == DISPLAY_TEXT_INST && param1 == DISPLAY_TEXT_PARAM1 && param2 == DISPLAY_TEXT_PARAM2) 23 | LCSEStringDisplayInstruction() 24 | else if (instruction == CHOICE_INST && param1 == CHOICE_PARAM1 && param2 == CHOICE_PARAM2) 25 | LCSEChoiceInstruction() 26 | else 27 | LCSEUnknownInstruction(instruction, param1, param2) 28 | } 29 | 30 | val stringStack = Stack() 31 | instructions.forEach { 32 | if (it is LCSEStringReferInstruction) 33 | stringStack.push(it.stringOrdinary) 34 | else if (it is LCSEStringDisplayInstruction && !stringStack.empty()) 35 | dialogStringOrdinals += stringStack.pop() 36 | else if (it is LCSEChoiceInstruction) { 37 | val str2 = stringStack.pop() // LIFO 38 | val str1 = stringStack.pop() 39 | 40 | choiceStringOrdinals += str1 41 | choiceStringOrdinals += str2 42 | } 43 | } 44 | 45 | script.strings.forEachIndexed { ordinal, content -> 46 | when { 47 | dialogStringOrdinals.contains(ordinal) -> this.strings += parseDialogString(byteArrayToString(content.content), this) 48 | choiceStringOrdinals.contains(ordinal) -> this.strings += LCSEChoiceString(strings.size, byteArrayToString(content.content)) 49 | else -> this.strings += LCSESystemString(strings.size, byteArrayToString(content.content)) 50 | } 51 | } 52 | } 53 | 54 | fun byteArrayToString(arr: ByteArray) = String(arr, 0, arr.indexOfFirst { it == 0x00.toByte() }, SHIFT_JIS) 55 | 56 | fun parseDialogString(str: String, context: LCSEParsedScript): LCSEString { 57 | val withTrailer = str.endsWith("\u0002\u0003") 58 | val elements = str.replace("\u0001", "\n") 59 | .removeSuffix("\u0002\u0003") 60 | .split('\n', limit = 2) 61 | if (elements.size < 2) 62 | return LCSESystemString(context.strings.size, elements[0]) 63 | val speakerOrdinal = (context.speakers.find { it.name == elements[0] } 64 | ?: LCSESpeaker(context.speakers.size, elements[0]).apply { context.speakers += this }).ordinal 65 | 66 | return LCSEDialogString(context.strings.size, speakerOrdinal, elements[1], withTrailer) 67 | } 68 | 69 | fun main(args: Array) { 70 | FileChannel.open(Paths.get("D:\\gal\\无限錬姦\\mugen\\originalSNX\\AGO01_04_03.snx"), StandardOpenOption.READ).use { 71 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 72 | val parsedSNX = parseRawSNXScript(readSNXScript(originalBuf)) 73 | } 74 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/parsed/LCSESNXWriter.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.parsed 2 | 3 | import charlie.lcsetools.snxutil.raw.LCSERawSNXInstruction 4 | import charlie.lcsetools.snxutil.raw.LCSERawSNXScript 5 | import charlie.lcsetools.snxutil.raw.LCSERawSNXString 6 | 7 | private val DISPLAY_INST_CACHE = LCSERawSNXInstruction(DISPLAY_TEXT_INST, DISPLAY_TEXT_PARAM1, DISPLAY_TEXT_PARAM2) 8 | private val CHOICE_INST_CACHE = LCSERawSNXInstruction(CHOICE_INST, CHOICE_PARAM1, CHOICE_PARAM2) 9 | fun generateRawSNXScript(script: LCSEParsedScript): LCSERawSNXScript { 10 | return LCSERawSNXScript().apply { 11 | var ptr = 0 12 | script.strings.forEachIndexed { ordinal, str -> 13 | val arr = str.convertToByteArray(script) 14 | strings += LCSERawSNXString(ordinal, ptr, arr) 15 | ptr += 4 16 | ptr += arr.size 17 | } 18 | 19 | script.instructions.forEach { inst -> 20 | instructions += inst.toRawInstruction(this) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/persistence/PersistenceHelper.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.persistence 2 | 3 | import charlie.lcsetools.snxutil.parsed.LCSEParsedScript 4 | import java.io.ObjectInputStream 5 | import java.io.ObjectOutputStream 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | import java.nio.file.StandardOpenOption 9 | 10 | fun loadParsedScript(path: Path) = ObjectInputStream(Files.newInputStream(path, StandardOpenOption.READ)).readObject() 11 | as LCSEParsedScript 12 | 13 | fun saveParsedScript(script: LCSEParsedScript, path: Path) 14 | = ObjectOutputStream(Files.newOutputStream(path.apply { if (!Files.exists(this)) Files.createFile(this) }, 15 | StandardOpenOption.TRUNCATE_EXISTING)).writeObject(script) -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/raw/LCSERawSNXScript.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.raw 2 | 3 | import java.util.* 4 | import kotlin.collections.ArrayList 5 | 6 | class LCSERawSNXScript { 7 | val instructions = ArrayList() 8 | val strings = ArrayList() 9 | 10 | val stringTableLength get() = strings.map { it.content.size + 4 }.sum() 11 | 12 | override fun toString(): String { 13 | return "LCSERawSNXScript(instructions=$instructions, strings=$strings)" 14 | } 15 | } 16 | 17 | data class LCSERawSNXInstruction(val instruction: Int, 18 | val param1: Int, 19 | val param2: Int) { 20 | companion object { 21 | const val INSTRUCTION_LENGTH = 4 * 3 22 | } 23 | 24 | override fun toString(): String { 25 | return "LCSERawSNXInstruction(0x${Integer.toHexString(instruction)}, 0x${Integer.toHexString(param1)}, 0x${Integer.toHexString(param2)})" 26 | } 27 | } 28 | 29 | data class LCSERawSNXString(val ordinal: Int, 30 | val offset: Int, 31 | val content: ByteArray) { 32 | override fun equals(other: Any?): Boolean { 33 | return if (other !is LCSERawSNXString) false 34 | else offset == other.offset && Arrays.equals(content, other.content) 35 | } 36 | 37 | override fun hashCode(): Int{ 38 | var result = offset 39 | result = 31 * result + Arrays.hashCode(content) 40 | return result 41 | } 42 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/main/kotlin/charlie/lcsetools/snxutil/raw/LCSESNXProcessor.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.raw 2 | 3 | import charlie.lcsetools.snxutil.raw.LCSERawSNXInstruction.Companion.INSTRUCTION_LENGTH 4 | import java.nio.ByteBuffer 5 | import java.nio.ByteOrder 6 | 7 | @Suppress("UsePropertyAccessSyntax") 8 | fun readSNXScript(buffer: ByteBuffer): LCSERawSNXScript { 9 | val leBuffer = buffer.order(ByteOrder.LITTLE_ENDIAN) 10 | return LCSERawSNXScript().apply { 11 | val instructionsCount = leBuffer.getInt() 12 | val stringTableLength = leBuffer.getInt() 13 | 14 | if (leBuffer.limit() != 4 + 4 + instructionsCount * INSTRUCTION_LENGTH + stringTableLength) 15 | throw IllegalArgumentException("文件格式错误!") 16 | 17 | instructions.ensureCapacity(instructionsCount) 18 | repeat(instructionsCount) { 19 | instructions += LCSERawSNXInstruction(leBuffer.getInt(), leBuffer.getInt(), leBuffer.getInt()) 20 | } 21 | 22 | val stringTableStartingOffset = leBuffer.position() 23 | var ordinal = 0 24 | while (leBuffer.remaining() >= 4) { 25 | val length = leBuffer.getInt() 26 | val tempBuf = ByteArray(length) 27 | leBuffer[tempBuf, 0, length] 28 | strings += LCSERawSNXString(ordinal++, leBuffer.position() - 4 - length - stringTableStartingOffset, tempBuf) 29 | } 30 | } 31 | } 32 | 33 | fun writeSNXScript(script: LCSERawSNXScript): ByteBuffer { 34 | val stringTableLength = script.stringTableLength 35 | return ByteBuffer.allocate(4 + 4 36 | + script.instructions.size * INSTRUCTION_LENGTH 37 | + stringTableLength).order(ByteOrder.LITTLE_ENDIAN).apply { 38 | putInt(script.instructions.size) 39 | putInt(stringTableLength) 40 | script.instructions.forEach { 41 | putInt(it.instruction) 42 | putInt(it.param1) 43 | putInt(it.param2) 44 | } 45 | 46 | script.strings.sortedBy { it.offset }.forEach { 47 | putInt(it.content.size) 48 | put(it.content, 0, it.content.size) 49 | } 50 | 51 | flip() 52 | } 53 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/test/kotlin/charlie/lcsetools/snxutil/gettext/GetTextWriterTest.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.gettext 2 | 3 | import charlie.lcsetools.snxutil.parsed.generateRawSNXScript 4 | import charlie.lcsetools.snxutil.parsed.parseRawSNXScript 5 | import charlie.lcsetools.snxutil.raw.readSNXScript 6 | import charlie.lcsetools.snxutil.raw.writeSNXScript 7 | import org.junit.Ignore 8 | import org.junit.Test 9 | import java.io.File 10 | import java.nio.channels.FileChannel 11 | import java.nio.file.Files 12 | import java.nio.file.Paths 13 | import java.nio.file.StandardOpenOption 14 | 15 | class GetTextWriterTest { 16 | @Ignore 17 | fun writePoTemplateTest() { 18 | /*FileChannel.open(Paths.get("""D:\无限錬姦\mugen\originalSNX\AGO01_01.snx""")).use { 19 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 20 | val parsedSNX = parseRawSNXScript(readSNXScript(originalBuf)) 21 | 22 | writePoTemplate(parsedSNX, Paths.get("""D:\无限錬姦\mugen\AGO01_01.po""")) 23 | }*/ 24 | 25 | File("""D:\无限錬姦\mugen\originalSNX\""").walkTopDown().filter { !it.isDirectory } 26 | .forEach { file -> 27 | FileChannel.open(file.toPath()).use { 28 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 29 | val parsedSNX = parseRawSNXScript(readSNXScript(originalBuf)) 30 | 31 | writePoTemplate(parsedSNX, Paths.get("""D:\无限錬姦\mugen\templates\""" + file.nameWithoutExtension + ".pot")) 32 | } 33 | } 34 | } 35 | 36 | @Test 37 | fun patchScriptTest() { 38 | FileChannel.open(Paths.get("""D:\无限錬姦\mugen\originalSNX\AGO05_01_01.snx""")).use { 39 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 40 | val parsedSNX = parseRawSNXScript(readSNXScript(originalBuf)) 41 | patchScript(parsedSNX, Paths.get("""D:\无限錬姦\mugen\第五章01 01\第五章01 01\AGO05_01_01.po""")) 42 | Paths.get("""D:\无限錬姦\mugen\AGO05_01_01.snx""").apply { 43 | if (!Files.exists(this)) Files.createFile(this) 44 | FileChannel.open(this, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE).use { 45 | it.write(writeSNXScript(generateRawSNXScript(parsedSNX))) 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/test/kotlin/charlie/lcsetools/snxutil/parsed/LCSESNXParserKtTest.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.parsed 2 | 3 | import charlie.lcsetools.snxutil.raw.readSNXScript 4 | import charlie.lcsetools.snxutil.raw.writeSNXScript 5 | import org.junit.Test 6 | import java.nio.channels.FileChannel 7 | import java.nio.file.Files 8 | import java.nio.file.Paths 9 | import java.nio.file.StandardOpenOption 10 | 11 | /** 12 | * @author Charlie Jiang 13 | * * 14 | * @since rv1 15 | */ 16 | class LCSESNXParserKtTest { 17 | @Test 18 | fun parseRawSNXScriptTest() { 19 | FileChannel.open(Paths.get("""D:\无限錬姦\mugen\originalSNX\AGO01_01.snx""")).use { 20 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 21 | val originalContent = ByteArray(it.size().toInt()) 22 | originalBuf[originalContent, 0, it.size().toInt()] 23 | originalBuf.position(0) 24 | 25 | val parsedSNX = parseRawSNXScript(readSNXScript(originalBuf)) 26 | Paths.get("""D:\无限錬姦\mugen\AGO01_01.snx""").apply { 27 | if (!Files.exists(this)) Files.createFile(this) 28 | FileChannel.open(this, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE).use { 29 | it.write(writeSNXScript(generateRawSNXScript(parsedSNX))) 30 | } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /LCSESNXUtility/src/test/kotlin/charlie/lcsetools/snxutil/raw/LCSESNXProcessorTest.kt: -------------------------------------------------------------------------------- 1 | package charlie.lcsetools.snxutil.raw 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import java.nio.channels.FileChannel 6 | import java.nio.file.Paths 7 | 8 | class LCSESNXProcessorTest { 9 | @Test 10 | fun readSNXScriptTest() { 11 | FileChannel.open(Paths.get("""D:\无限錬姦\mugen\originalSNX\AGO01_01.snx""")).use { 12 | val originalBuf = it.map(FileChannel.MapMode.READ_ONLY, 0, it.size()) 13 | val originalContent = ByteArray(it.size().toInt()) 14 | originalBuf[originalContent, 0, it.size().toInt()] 15 | 16 | originalBuf.position(0) 17 | val newBuf = writeSNXScript(readSNXScript(originalBuf)) 18 | val newContent = ByteArray(newBuf.limit()) 19 | newBuf[newContent, 0, newBuf.limit()] 20 | 21 | Assert.assertArrayEquals(originalContent, newContent) 22 | 23 | 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LCSELocalizationTools 2 | 3 | Tools to translate Virtual Novels(Galgame)/TAVG written by LC-ScriptEngine. 4 | 一些翻译 LC-ScriptEngine 编写的 Galgame 的工具。 5 | 6 | 测试用游戏: 7 | Test game: 無限煉姦~淫辱にまみれし不死姫の輪舞~ 8 | 9 | (以下文档分中英文版) 10 | (Document below has both Chinese and English ver.) 11 | 12 | ## Chinese ver. 13 | 14 | ### LCSE Package Utility (LC-ScriptEngine资源包封包处理实用工具) 15 | 16 | 处理 LC-ScriptEngine 游戏封包(一个大文件带着一个.lst的清单文件)的工具。所有程序说明都是中文,下面是详细使用: 17 | 18 | 本工具有两个模式,解包和封包。解包会将包中的文件提取为独立文件,封包会利用将原版包中的一些文件利用一系列新文件进行替换来产生一个新的封包(并不会修改原版封包)。 19 | 20 | #### 通用参数 21 | 22 | 两个模式都必须指定 `-l(--list)` 参数提供 .lst 清单文件,`-a(--package)` 提供封包文件。你还可以指定一个可选的 `-d(--out-dir)` 参数来设置输出目录。 23 | 24 | 通过 `-k (--key)` 可以指定 `.lst` 文件的加密 key,而 `-K (--key-snx)` 可以指定 `.snx` 文件的 key。这两个参数的格式均为两位 16 进制数。 25 | 26 | 请总是使用 `-h` 指令查看最全面的参数列表! 27 | 28 | 你需要选择你想要处理的资源文件类型。下面是用来设置是否处理这些类型的开关: 29 | 30 | ``` 31 | -s --process-snx 处理SNX脚本 32 | -p --process-png 处理PNG图片 33 | -b --process-bmp 处理BMP图片 34 | -w --process-wav 处理WAV音频 35 | -o --process-ogg 处理OGG音频 36 | ``` 37 | 38 | #### 解包模式 39 | 40 | 使用解包模式你必须指定 `-u(--unpack)` 开关,且不能指定 `-e(--patch-dir)` 参数。 41 | 42 | 例子: 43 | 44 | ```cmd 45 | java -jar .\LCSEPackageUtility-rv1.jar --unpack --list lcsebody1.lst --package lcsebody1 -s -d "D:\mugen\extracted" --key 02 46 | ``` 47 | 48 | 意味着解包 lcsebody1 封包文件,利用 lcsebody1.lst 清单文件,只解包SNX格式脚本,输出到 D:\mugen\extracted 目录。 49 | 50 | #### 封包模式 51 | 52 | 使用封包模式你需要指定 `-r(--patch)` 开关,且必须指定 `-e(--patch-dir)` 来提供包含你希望用来替换掉原封包中文件的文件的文件夹。 53 | 54 | 例子: 55 | 56 | ```cmd 57 | java -jar .\LCSEPackageUtility-rv1.jar --patch --patch-dir "D:\mugen\patches" --list lcsebody1.lst --package lcsebody1 -s -d "D:\mugen\patched" --key 02 58 | ``` 59 | 60 | 意味着利用原封包文件 lcsebody1 和清单 lcsebody1.lst,用 D:\mugen\patches 替换掉原封包中的同名文件,生成一个新的封包和对应清单到 D:\mugen\patched,且仅替换SNX格式脚本。 61 | 62 | ## English ver. 63 | 64 | ### LCSE Package Utility 65 | 66 | A tool to process the package of LC-ScriptEngine games (a big file with a .lst file). All string written in Chiense, here's the English usage: 67 | 68 | First, we have 2 modes, Unpack and Patch. Unpack is to extract files from the package to dedicated files. Patch is to replace files in the package, and then create a new archive (modify the original version of pkg is not recommended). 69 | 70 | #### Common args 71 | 72 | For both modes you need to specify the -l(--list) value to provide .lst file, the -a(--package) to provide package file. You can also specify a optional -d(--out-dir) value to set the out directory. 73 | 74 | Use `-k (--key)` and `-K (--key-snx)` to specify encryption key for `.lst` and `.snx` files, respectively. The format is 2-digits hex number e.g. `-k 0F`. 75 | 76 | Please always use `-h` to view the full arg list! 77 | 78 | You need to choose the resource file type that you needed to process. Below is switches to decide that if process them or not. 79 | 80 | ``` 81 | -s --process-snx Process SNX Script. 82 | -p --process-png Process PNG Picture. 83 | -b --process-bmp Process BMP Pirture. 84 | -w --process-wav Process WAV Audio. 85 | -o --process-ogg Process OGG Audio. 86 | ``` 87 | 88 | #### Using Unpack Mode 89 | 90 | To use the Unpack mode you need to specify -u(--unpack) switch, no -e(--patch-dir) allowed. 91 | 92 | Example: 93 | 94 | ```cmd 95 | java -jar .\LCSEPackageUtility-rv1.jar --unpack --list lcsebody1.lst --package lcsebody1 -s -d "D:\mugen\extracted" 96 | ``` 97 | 98 | Means unpack the lcsebody1 package with lcsebody1.lst list file, extract SNX scripts only, to `D:\mugen\extracted` folder. 99 | 100 | #### Using Patch Mode 101 | 102 | To use the Patch mode you need to specify -r(--patch) switch, -e(--patch-dir) required to set a directory contains files you want to replace the files in the package. 103 | 104 | Example: 105 | 106 | ```cmd 107 | java -jar .\LCSEPackageUtility-rv1.jar --patch --patch-dir "D:\mugen\patches" --list lcsebody1.lst --package lcsebody1 -s -d "D:\mugen\patched" 108 | ``` 109 | 110 | Means from the lcsebody1 package with lcsebody1.lst file, create a new package to `D:\mugen\patched` folder that some files in the original package is replaced with files in `D:\mugen\patches` folder. --------------------------------------------------------------------------------