Membongkar mesin novel visual Qlie



Terjemahan amatir dari cerpen visual, jika dibandingkan dengan terjemahan gim lain, memiliki sejumlah fitur dan melibatkan kerja dengan banyak teks. Mungkin sebagian besar dari semua novel visual dirilis dalam bahasa Jepang, hanya sedikit yang diterjemahkan ke dalam bahasa Inggris (secara resmi atau oleh amatir), dan bahkan lebih sedikit lagi yang diterjemahkan ke dalam bahasa lain.

Karena itu, ketika bekerja dengan terjemahan, Anda harus berurusan dengan mesin Jepang, yang banyak di antaranya tidak terlalu ramah untuk pelokalan. Karena itu, dengan cepat muncul kesadaran bahwa kehadiran keterampilan terjemahan, pengetahuan bahasa, banyak antusiasme dan waktu luang tidak berarti sama sekali bahwa versi terjemahan dari permainan akan segera melihat cahaya hari.

Secara sangat kasar, proses menerjemahkan game apa pun (bukan hanya cerita pendek visual) menyiratkan:

  • Membongkar sumber daya game (jika tidak ada dalam domain publik)
  • Terjemahan bagian-bagian yang diperlukan
  • Transfer kemasan terbalik

Namun, dalam kasus cerpen visual Jepang, ini biasanya terlihat seperti ini:

  • Membuka sumber daya game
  • Terjemahan bagian teks gim (skrip permainan)
  • Terjemahan bagian grafis dari permainan
  • Transfer kemasan terbalik
  • Perubahan mesin untuk membuatnya berfungsi dengan konten yang diterjemahkan

Semoga pengalaman kami bermanfaat bagi seseorang.

Kembali pada tahun 2013 (dan mungkin sebelumnya) saya memutuskan untuk menerjemahkan dari novel visual Jepang Bishoujo Mangekyou-Norowareshi Densetsu no Shoujo- (美 少女 万 華 鏡 - 呪 わ れ し 伝 説 の 少女 -). Saya sudah memiliki pengalaman menerjemahkan permainan, tetapi sebelumnya saya harus menerjemahkan hanya cerita pendek pada mesin yang relatif sederhana dan terkenal seperti Kirikiri .

Di sini, tim penerjemah kami harus membuka mesin cerita pendek ini, bahkan sebelum kita sampai ke teks yang sebenarnya.

Mari kita mulai dengan deskripsi file .exe, di mana kata-kata QLIE dan IMOSURUME disebutkan. File itu sendiri berisi baris FastMM Borland Edition 2004, 2005 le le Riche, yang berarti mesin kemungkinan besar ditulis dalam Delphi.



Googling cepat mengungkapkan bahwa Qlie adalah nama mesin visual novel yang dirilis oleh Warmth Entertainment. Rupanya IMOSURUME adalah nama internal mesin skrip, dan Qlie adalah nama komersial. Ada situs qlie.net , yang berisi daftar game yang dirilis di mesin ini dan situs resmi Warmth Entertainment.

Tapi tidak ada dalam domain publik tidak ada alat resmi untuk bekerja dengan mesin, atau dokumentasi untuk itu, yang diharapkan.

Oleh karena itu, Anda harus berurusan dengan permainan sendiri, bergantung pada utilitas tidak resmi. Untuk memulainya, Anda harus menemukan semua bagian permainan yang perlu diterjemahkan.

Arsip permainan terletak di file data0.pack, data1.pack dan data7.pack dalam subfolder \ GameData. Screensaver berada di folder \ GameData \ Movie, tetapi Anda masih bisa membiarkannya.


Editor heks menunjukkan bahwa tidak ada header dikenali untuk arsip game .pack, tetapi pada akhir file ada bagian yang mirip dengan daftar isi dan label FilePackVer3.0


Untungnya, untuk format ini, sudah ada unpacker dan bahkan belum ada. Kami menggunakan konsol exfp3_v3 dari asmodean.

Membongkar barang tidak semudah kelihatannya. Karena mesin mendukung beberapa format arsip (FilePackVer1.0, FilePackVer1.0, FilePackVer3.0), dan dalam hal ini FilePackVer3.0 digunakan, untuk membongkar yang tepat Anda juga akan memerlukan kunci file kunci khusus. Kunci, kunci yang dienkripsi arsip. Itu terletak di subfolder \ Dll


Selain itu, exfp3_v3 harus mengklarifikasi arsip dari game mana itu dibongkar.
Oleh karena itu, Anda juga perlu menentukan nomor gim dari daftar yang diusulkan oleh unpacker (gim seri Bishoujo Mangekyou ada di bawah nomor 15), atau menentukan file gim yang dapat dieksekusi permainan sebagai parameter ketiga untuk pembongkar kemasan.


Sudah setelah membongkar file game, muncul pemikiran logis: bagaimana di masa depan bagaimana cara mengemas game kembali dengan terjemahan yang siap? Lagi pula, unpacker tidak mendukung operasi mundur.
Atas permintaan kami, w8m (terima kasih banyak untuk itu) menambahkan kemampuan untuk mengemas arsip game ke dalam programnya arc_conv.exe. Sudah cukup untuk mengemas semua file yang diubah ke arsip baru (misalnya, data8.pack), letakkan di folder GameData, dan mereka akan secara otomatis menarik diri ke dalam game.

Kembali ke sumber yang belum dibongkar. File skrip gim dari arsip data0.pack dapat ditemukan di subfolder \ scenario \ ks_01 \

Semua file skrip dengan ekstensi .s dikodekan jauh dari pengkodean Shift Jis yang paling nyaman, dan mesin tidak mendukung pengkodean unicode apa pun. Garis-garis untuk terjemahan terlihat kurang lebih seperti ini:

【キリエ】 %1_kiri1478% 「へえ……分かっているじゃない」 私が献上したロシアンティーを見て、キリエは嬉しそうに目を細める。 ^cface,,赤目微笑01 【キリエ】 %1_kiri1479% 「日本人は、ジャムを紅茶に入れて飲むのが、ロシアンティーだと勘違いしている人が多いのだけれど……」 

Anda mungkin memperhatikan bahwa setiap frasa dalam bahasa Jepang didahului dengan nama pahlawan dalam tanda kurung Jepang. (【】), Yang mengucapkan frasa ini (dalam game ini ditampilkan di bagian atas jendela dengan teks). Atau, jika ini adalah kata-kata penulis, maka namanya tidak ditambahkan.


Tetapi masih ada tim layanan.

Perintah mesin dalam skrip agak mengingatkan pada bahasa markup TeX, tetapi jauh lebih intuitif dan tidak nyaman dibandingkan dengan perintah Kirikiri atau RenPy .

Inilah beberapa di antaranya:

@@@ adalah anjing rangkap tiga. Seringkali, file skrip dimulai dengan perintah ini. Rupanya memuat definisi dari file pihak ketiga.

Sebagai contoh:

 @@@Library\Avg\header.s 

@@ adalah anjing ganda. Label dalam file skrip. Anda dapat beralih ke nanti.

%1_kiri1478% - memutar file suara. Perintah-perintah ini disisipkan di antara nama pahlawan dan teks yang ditampilkan di layar. “1_kiri1478” - dalam hal ini, nama file dari folder \ voice \ dari data1.pack file Sangat menarik bahwa tim menggunakan persentase Jepang (%), daripada yang biasa.

^savedate, ^saveroute, ^savescene, - tiga tim yang kemungkinan besar digunakan dalam sistem save gim dan harus memasukkan informasi di tempat dan waktu pemain disimpan dalam save gim.

Sebagai contoh:

 ^savedate,"現在" ^saveroute,"美少女万華鏡-1-" ^savescene,"呪われし伝説の少女 オープニング" 

Yaitu, tanggal: sekarang, cabang: Bishoujo Mangekyou -1-, adegan: Pembukaan Norowareshi Densetsu no Shoujo. Data ini seharusnya ditampilkan di slot penyimpanan, tetapi tampaknya para pengembang memutuskan untuk meninggalkannya. Akibatnya, ^saveroute di semua bagian skrip, ^savedate perubahan dari "saat ini" menjadi "mimpi", dan di dalam ^savescene -hari dalam game (atau lebih tepatnya malam) perubahan.

^facewindow, - keadaan kotak teks dengan teks yang ditampilkan di layar. (Ditampilkan - 1 atau tidak - 0)

^sload, - mainkan suara dalam game dari folder \ sound \ pada saluran yang sesuai.

 sload,Env1,◆セミ01アブラゼミ 

Memainkan cicadas di Env1

Tim memiliki dua parameter opsional, yang pertama bertanggung jawab untuk mengulang suara, dan yang kedua tetap menjadi misteri, tetapi jarang digunakan dalam permainan.

 ^sload,SE1,■クチュ音01,1 

Memutar suara loopback pada saluran SE1.

^eeffect - menampilkan efek khusus pada layar selama beberapa detik. Rupanya, ini mendukung output berurutan dari beberapa efek.

 ^eeffect,WhiteFlash 

Efek dari flash putih.

^ffade - efek transisi saat mengubah layar.
Ini memiliki sejumlah parameter tambahan, tetapi hanya beberapa yang benar-benar berguna: nama efek transisi, gambar tambahan, jika perlu, dan waktu penyelesaian transisi.

 ^ffade,Overlap,,1000 

Melarutkan satu gambar ke gambar lain, dalam 1 detik.

^iload - memuat gambar latar belakang di layar. Gambar dapat diberi id untuk merujuk di masa depan.

 ^iload,BG1,0_black.png 

Keluarkan file 0_black.png sebagai latar belakang dengan id BG1

^we and ^wd - menghidupkan dan mematikan gambar di jendela.

^facewindow,1 dan ^facewindow,0 Mengaktifkan dan menonaktifkan gambar pahlawan di kotak dialog.

^mload - memutar musik di saluran tertentu.

 ^mload,BGM1,nbgm13 

Memainkan trek nbgm13 di saluran BGM1

Beberapa tim paling penting:
\jmp - lompat ke label dengan nama yang ditentukan.

^select - menampilkan jendela pilihan di layar, di mana pemain harus memilih salah satu opsi.

Sebagai contoh:

 ^select, ,  \jmp,"@@route01a"+ResultBtnInt[0] @@route01a0 

Di sini transisi akan dilakukan setelah jawaban pertanyaan, dan nomor respons (0 atau 1) dikembalikan dari ResultBtnInt [0]. Akibatnya, \jmp memindahkan cerita ke label @@ route01a + nomor respons. Yaitu, @@ route01a0 atau @@ route01a1

Fitur yang tidak menyenangkan adalah bahwa koma yang biasa dalam perintah ini berfungsi sebagai pemisah dan tidak dapat digunakan dalam opsi jawaban itu sendiri. Orang Jepang tidak memiliki masalah seperti itu, mereka menggunakan koma Jepang (、). Dalam hal ini, kita dapat mengganti koma dengan ‚(U + 201A SINGLE LOW-9 MARK QUOTATION).

Sebagai contoh:

 ^select, ‚  , ‚  

Tim yang tersisa tidak begitu penting dalam pendekatan pertama.

Tentu saja, sebelum menerjemahkan skrip, Anda harus mentranskodekannya menjadi sesuatu yang lebih nyaman, misalnya, di UTF-8, untuk menggabungkan karakter Cyrillic dan Jepang.

Setelah mengganti mesin (tentang bagian selanjutnya ini), permainan memahami teks Rusia dan Jepang. Tetapi untuk sekarang, untuk kompatibilitas, Anda perlu menyandikan karakter Jepang di Shift Jis, dan karakter Cyrillic dalam penyandian cp1251.

Kami dengan cepat membuat sketsa sebuah program dengan Python untuk transcoding dengan mempertimbangkan alfabet Cyrillic:

UTF8 ke cp1251 dan ShiftJIS
 # -*- coding: utf-8 -*- # UTF8 to cp1251 and ShiftJIS recoder # by Chtobi and Nazon, 2016 import codecs import argparse from os import path JAPANESE_CODEPAGE = 'shift_jis' UTF_CODEPAGE = 'utf-8' RUS_CODEPAGE = 'cp1251' def nonrus_handler(e): if e.object[e.start:e.end] == '~': # UTF-8: 0xEFBD9E -> SHIFT-JIS: 0x8160 japstr_byte = b'\x81\x60' elif e.object[e.start:e.end] == '-': # UTF-8: 0xEFBC8D -> SHIFT-JIS: 0x817C japstr_byte = b'\x81\x7c' else: japstr_byte = (e.object[e.start:e.end]).encode(JAPANESE_CODEPAGE) return japstr_byte, e.end if __name__ == '__main__': arg_parser = argparse.ArgumentParser(prog="Recode to cp1251 and ShiftJIS", description="Program to encode UTF8 text file to " "cp1251 for all cyrillic symbols and ShiftJIS for others. " "Output file will be inputfilename.s", usage="recode_to_cp1251_shiftjis.py file_name") arg_parser.add_argument('file_name', nargs=1, type=argparse.FileType(mode='r', bufsize=-1), help="Input text file name. Only files coded in UTF8 are allowed.\n") codecs.register_error('nonrus_handler', nonrus_handler) input_name = arg_parser.parse_args().file_name[0].name output_name = path.splitext(input_name)[0] + ".s" with open(input_name, 'rt', encoding=UTF_CODEPAGE) as input_file: with open(output_name, 'wb') as output_file: for line in input_file: for char1 in line: bytes_out = bytes(line, UTF_CODEPAGE) output_file.write(char1.encode(RUS_CODEPAGE, "nonrus_handler")) print("Done.") 


Namun, ada beberapa masalah. Program, ketika mencoba untuk mengkode ulang simbol "tilde" U (U + FF5E FULLWIDTH TILDE), menghasilkan kesalahan "UnicodeEncodeError: Codec 'Shift Jis' tidak dapat menyandikan karakter '\ uff5e' di posisi 0: urutan multibyte ilegal"

Pada awalnya, saya berdosa di Python, tetapi pada akhirnya saya menemukan nuansa yang agak tidak biasa. Ada ambiguitas antara metode korelasi pengkodean Unicode dan non-Unicode Jepang tergantung pada implementasi spesifik.

Akibatnya, Windows mengaitkan karakter Shift Jis dengan kode 0x8160 dengan unicode ~ (U + FF5E FULLWIDTH TILDE), dan transcoder lainnya (misalnya, utilitas ikonv) mengkorelasikan karakter yang sama dengan 〜 (U + 301C WAVE DASH), menurut tabel rasio Unicode resmi - ftp://ftp.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/SHIFT JIS.TXT

Untuk menentukan korespondensi antara karakter, Microsoft tampaknya memutuskan untuk menggunakan skema dari pengkodean cp932 mereka, yang merupakan versi diperpanjang dari Shift Jis.

Situasi yang sama terjadi dengan kode karakter 0x817C, yang dikodekan dalam UTF8 sebagai - (U + FF0D FULLWIDTH HYPHEN-MINUS) pada Windows, atau as - (U + 2212 MINUS SIGN) di iconv.

Karena semua file skrip pertama kali dikonversi dari Shift Jis ke UTF8 menggunakan Notepad ++ (dan ia menggunakan tabel korespondensi yang diadopsi di Windows), ketika mengkonversi kembali dari UTF8 ke Shift Jis melalui program Python kami, kesalahan konversi yang terkenal muncul.

Oleh karena itu, perlu untuk mempertimbangkan terjadinya kondisi ~ dan - terpisah.

Ada kekurangan kecil lainnya - misalnya, elipsis ... (U + 2026 ELLIPSIS HORIZONTAL) digantikan oleh elipsis Cyrillic dari cp1251, dan bukan Jepang dari Shift Jis.

Setelah menerjemahkan teks, Anda dapat melanjutkan bekerja dengan grafik game.

File grafis dari gim ini berada dalam arsip paket yang sama, tetapi setelah membongkar, mereka masih harus bekerja keras. Sebagai contoh, hampir semua gambar png dibongkar dalam bentuk file dari sampel tipe + DPNG000 + x32y0.png Dengan kata lain, gambar png dipotong menjadi strip horisontal dengan ketebalan 88 piksel dan setiap strip direkam dalam file terpisah. Nama file menunjukkan nomor seri strip (DPNG000 ... 009) dan koordinat x, y.


Saya masih bertanya-tanya mengapa ini perlu. Jika untuk kesulitan merobek sumber daya dari permainan, maka ini jelas bukan metode terbaik.

Untuk merekatkan file png yang dipotong, skrip kecil merge_dpng pada Pearl dari asmodeus, yang menggunakan ImageMagick, dibuat pada satu waktu. Sayangnya, ada masalah dengannya. Pertama, saya membutuhkan Pearl, yang tidak saya gunakan, dan bahkan setelah menginstalnya, ternyata skrip tidak berfungsi dengan benar.

Untuk alasan ini, kami menulis program serupa dengan python:

Qlie engine dpng file merger
 # -*- coding: utf-8 -*- # Qlie engine dpng files merger # by Chtobi and Nazon, 2016 # Requires ImageMagick magick.exe on the path. import os import glob import re import argparse import subprocess IMGMAGIC = os.path.dirname(os.path.abspath(__file__)) + '\\' + 'magick.exe' IMGMAGIC_PARAMS1 = ['-background', 'rgba(0,0,0,0)'] IMGMAGIC_PARAMS2 = ['-mosaic'] INPUT_FILES_MASK = '*+DPNG[0-9][0-9][0-9]+*.png' SPLIT_MASK = '+DPNG' x_y_ajusts_re = re.compile('(.+)\+DPNG[0-9][0-9][0-9]\+x(\d+)y(\d+)\.') if __name__ == '__main__': arg_parser = argparse.ArgumentParser(prog="DPNG Merger\n" "Program to merge sliced png files from QLIE engine. " "All files with mask *+DPNG[0-9][0-9][0-9]+*.png" "into the input directory will be merged and copied to the" "output directory.\n", usage="connect_png.py input_dir [output_dir]\n") arg_parser.add_argument("input_dir_param", nargs=1, help="Full path to the input directory.\n") arg_parser.add_argument("output_dir_param", nargs='?', default=os.path.dirname(os.path.abspath(__file__)), help="Full path to the output directory. " "It would be a script parent directory if not specified.\n") input_dir = arg_parser.parse_args().input_dir_param[0] output_dir = arg_parser.parse_args().output_dir_param[0] os.chdir(input_dir) all_append_files = glob.glob(INPUT_FILES_MASK) # Select only files with DPNG prep_bunches = [] for file_in_dir in all_append_files: # Check all files and put all splices that should be connected in separate list for num, bunch in enumerate(prep_bunches): name_first_part = bunch[0].partition(SPLIT_MASK)[0] # Part of the filename before +DPNG should be unique if name_first_part == file_in_dir.partition(SPLIT_MASK)[0]: prep_bunches[num].append(file_in_dir) break else: prep_bunches.append([file_in_dir]) os.chdir(os.path.dirname(os.path.abspath(__file__))) # Go to the script parent dir for prepared_bunch in prep_bunches: sorted_bunch = sorted(prepared_bunch) # Prepare -page params for imgmagic png_pages_params = [["(", "-page", "+{0}+{1}".format(*[(x_y_ajusts_re.match(part_file).group(2)), x_y_ajusts_re.match(part_file).group(3)]), input_dir+part_file, ")"] for part_file in sorted_bunch] connect_png_list = \ [imgmagick_page for imgmagick_pages in png_pages_params for imgmagick_page in imgmagick_pages] output_file = output_dir + sorted_bunch[0].partition(SPLIT_MASK)[0] + ".png" subprocess.check_output([IMGMAGIC] + IMGMAGIC_PARAMS1 + connect_png_list + IMGMAGIC_PARAMS2 + [output_file]) 


Tampaknya sekarang kita sudah mendapatkan seluruh rangkaian gambar yang muncul dalam permainan? Tidak sama sekali - jika Anda melihat semua gambar yang terhubung dari semua arsip, Anda masih akan menemukan bahwa ada beberapa yang hilang, meskipun mereka ada dalam permainan. Faktanya adalah bahwa ada jenis file lain di mesin - dengan ekstensi .b. Ini sedikit animasi dengan gambar dan suara yang direkam di dalamnya.

Sangat mudah untuk mendapatkan sumber daya yang tersimpan di dalamnya, tetapi, sayangnya, tidak ada satu pun dari file unpacker .b yang siap pakai yang berfungsi dalam kasus kami sebagaimana mestinya. Entah beberapa file tetap dibongkar, atau ada kesalahan karena nama Jepang, dan saya tidak ingin boot dari lokal Jepang.

Di sini satu lagi skrip kami berguna. Sejak itu kami tidak terbiasa dengan sesuatu seperti Kaitai Struct , kami harus bertindak hampir dari awal.

Format file .b ternyata sederhana dan, terlebih lagi, unpacker kami harus bisa membongkar sumber daya hanya dari game ini. Di gim lain di mesin Qlie, jenis sumber daya tambahan muncul di dalam file .b, tapi kami tidak akan membahasnya secara terperinci.

Jadi, buka semua file .b di hex editor dan lihat ke awal. Sebelum mengevaluasi, perhatikan bahwa urutan byte dari semua nilai numerik akan menjadi Little-endian.

  • Header file Abmp12
  • Sepuluh byte 0x00
  • Judul abdata12 bagian pertama dengan informasi overhead.
  • Delapan byte 0x00
  • Bagian ukuran abdata12, integer empat byte. Anda dapat melewatinya dengan aman.
  • Header bagian Abimage10
  • Tujuh byte 0x00
  • Jumlah file dalam satu bagian, integer byte tunggal. Dalam hal ini, ada satu file di bagian ini.
  • Bagian tajuk abgimgdat13
  • Enam byte 0x00
  • Panjang nama file di dalam bagian, bilangan bulat dua byte. Dalam hal ini, panjangnya adalah 4 byte.
  • Shift Jis nama file yang disandikan
  • Panjang catatan file checksum, bilangan bulat bita.
  • Checksum dari file itu sendiri.
  • Byte yang tidak dikenal tampaknya selalu 0x03 atau 0x02
  • Dua belas byte yang tidak diketahui, kemungkinan terkait dengan animasi
  • Ukuran file png di dalam bagian ini adalah integer empat byte.

Dan akhirnya, file png itu sendiri.


Bagian absound serupa dalam struktur dengan abimage.

Animator BMP animasi
 # -*- coding: utf-8 -*- # Extract b # AnimatedBMP extractor for Bishoujo Mangekyou game files # by Chtobi and Nazon, 2016 import glob import os import struct import argparse from collections import namedtuple b_hdr = b'abmp12'+bytes(10) signa_len = 16 b_abdata = (b'abdata10'+bytes(8), b'abdata11'+bytes(8), b'abdata12'+bytes(8), b'abdata13'+bytes(8)) b_imgdat = (b'abimgdat10'+bytes(6), b'abimgdat11'+bytes(6), b'abimgdat14'+bytes(6)) b_img = (b'abimage10'+bytes(7), b'abimage11'+bytes(7), b'abimage12'+bytes(7), b'abimage13'+bytes(7), b'abimage14'+bytes(7)) b_sound = (b'absound10'+bytes(7), b'absound11'+bytes(7), b'absound12'+bytes(7)) # not sure about structure of sound11 and sound12 b_snd = (b'absnddat11'+bytes(7), b'absnddat10'+bytes(7), b'absnddat12'+bytes(7)) Abimgdat13_pattern = namedtuple('Abimgdat13', ['signa', 'name_size_len', 'hash_size_len', 'unknown1_len', 'unknown2_len', 'data_size_len']) Abimgdat13 = Abimgdat13_pattern(signa=b'abimgdat13'+bytes(6), name_size_len=2, hash_size_len=2, unknown1_len=1, unknown2_len=12, data_size_len=4) Abimgdat14_pattern = namedtuple('Abimgdat14', ['signa', 'name_size_len', 'hash_size_len', 'unknown1_len', 'data_size_len']) Abimgdat14 = Abimgdat14_pattern(signa=b'abimgdat14'+bytes(6), name_size_len=2, hash_size_len=2, unknown1_len=77, data_size_len=4) Abimgdat_pattern = namedtuple('Abimgdat', ['name_size_len', 'hash_size_len', 'unknown1_len', 'data_size_len']) # probably, abimgdat10,abimgdat11 and others Other_imgdat = Abimgdat_pattern(name_size_len=2, hash_size_len=2, unknown1_len=1, data_size_len=4) Absnddat11_pattern = namedtuple('Absnddat11', ['signa', 'name_size_len', 'hash_size_len', 'unknown1_len', 'data_size_len']) Absnddat11 = Absnddat11_pattern(signa=b'absnddat11'+bytes(7), name_size_len=2, hash_size_len=2, unknown1_len=1, data_size_len=4) def create_parser(): arg_parser = argparse.ArgumentParser(prog='AnimatedBMP extractor\n', usage='extract_b input_file_name output_dir\n', description='AnimatedBMP extractor for QLIE engine *.b files.\n') arg_parser.add_argument('input_file_name', nargs='+', help="Input file with full path(wildcards are supported).\n") arg_parser.add_argument('output_dir', nargs=1, help="Output directory.\n") return arg_parser def check_type(file_buf): if file_buf.startswith(b'\x89' + b'PNG'): return '.png' elif file_buf.startswith(b'BM'): return '.bmp' elif file_buf.startswith(b'JFIF', 6): return '.jpg' elif file_buf.startswith(b'IMOAVI'): return '.imoavi' elif file_buf.startswith(b'OggS'): return '.ogg' elif file_buf.startswith(b'RIFF'): return '.wav' else: return '' def bytes_shiftjis_to_utf8(shiftjis_bytes): shiftjis_str = shiftjis_bytes.decode('shift_jis', 'strict') utf_str = shiftjis_str.encode('utf-8', 'strict').decode('utf-8', 'strict') return utf_str def check_signa(f_buffer): if f_buffer.endswith(b_abdata): return 'abdata' elif f_buffer.endswith(b_img): return 'abimgdat' elif f_buffer.endswith(b_sound): return 'absound' def prepare_filename(out_file_name, out_dir, postfix=''): ready_name = out_dir + os.path.basename(out_file_name) + postfix return ready_name def create_file(file_name_hndl, out_buffer): if len(out_buffer) != 0: with open(file_name_hndl, 'wb') as ext_file: ext_file.write(out_buffer) else: print("Zero file. Skipped.") def check_file_header(file_handle, bytes_num): file_handle.seek(0) readed_bytes = file_handle.read(bytes_num) if readed_bytes == b_hdr: print("File is valid abmp") return True else: print("Can't read header. Probably, wrong file...") return False if __name__ == '__main__': parser = create_parser() arguments = parser.parse_args() all_b_files = glob.glob(arguments.input_file_name[0]) output_dir = arguments.output_dir[0] for b_file in all_b_files: file_buffer = bytearray(b'') with open(b_file, 'rb') as bfile_h: check_file_header(bfile_h, len(b_hdr)) read_byte = bfile_h.read(1) file_buffer.extend(read_byte) while read_byte: read_byte = bfile_h.read(1) file_buffer.extend(read_byte) # Finding content sections signature check_result = check_signa(file_buffer) if check_result: if check_result == 'abdata': file_buffer = bytearray(b'') read_length = bfile_h.read(4) size = struct.unpack('<L', read_length)[0] file_buffer.extend(bfile_h.read(size)) # Adding _abdata to separate from other parts outfile_name = prepare_filename(b_file, output_dir, '_abdata') create_file(outfile_name, file_buffer) elif check_result == 'abimgdat': images_number = struct.unpack('B', bfile_h.read(1))[0] # Number of pictures in section for i1 in range(images_number): file_buffer = bytearray(b'') file_name = '' imgsec_hdr = bfile_h.read(signa_len) if imgsec_hdr == Abimgdat13.signa: file_name_size = struct.unpack('<H', bfile_h.read(Abimgdat13.name_size_len))[0] # Decode filename to utf8 file_name = bytes_shiftjis_to_utf8(bfile_h.read(file_name_size)) # CRC size hash_size = struct.unpack('<H', bfile_h.read(Abimgdat13.hash_size_len))[0] # Picture CRC (don't need it) pic_hash = bfile_h.read(hash_size) unknown1 = bfile_h.read(Abimgdat13.unknown1_len) unknown2 = bfile_h.read(Abimgdat13.unknown2_len) pic_size = struct.unpack('<L', bfile_h.read(Abimgdat13.data_size_len))[0] print("pic_size:", pic_size) file_buffer.extend(bfile_h.read(pic_size)) elif imgsec_hdr == Abimgdat14.signa: file_name_size = struct.unpack('<H', bfile_h.read(Abimgdat14.name_size_len))[0] file_name = bytes_shiftjis_to_utf8(bfile_h.read(file_name_size)) hash_size = struct.unpack('<H', bfile_h.read(Abimgdat14.hash_size_len))[0] pic_hash = bfile_h.read(hash_size) bfile_h.seek(Abimgdat14.unknown1_len, os.SEEK_CUR) pic_size = struct.unpack('<L', bfile_h.read(Abimgdat14.data_size_len))[0] file_buffer.extend(bfile_h.read(pic_size)) else: # probably abimgdat10, abimgdat11... file_name_size = struct.unpack('<H', bfile_h.read(Other_imgdat.name_size_len))[0] file_name = bytes_shiftjis_to_utf8(bfile_h.read(file_name_size)) hash_size = struct.unpack('<H', bfile_h.read(Other_imgdat.hash_size_len))[0] pic_hash = bfile_h.read(hash_size) bfile_h.seek(Other_imgdat.unknown1_len, os.SEEK_CUR) pic_size = struct.unpack('<L', bfile_h.read(Other_imgdat.data_size_len))[0] file_buffer.extend(bfile_h.read(pic_size)) for i, letter in enumerate(file_name): # Replace any unusable symbols from filename with _ if letter == '<' or letter == '>' or letter == '*' or letter == '/': file_name = file_name.replace(letter, "_") # Checking file signature and adding proper extension outfile_name = prepare_filename(b_file, output_dir, '_' + file_name + check_type(file_buffer)) create_file(outfile_name, file_buffer) file_buffer = bytearray(b'') elif check_result == 'absound': sound_files_number = struct.unpack('B', bfile_h.read(1))[0] for i2 in range(sound_files_number): file_buffer = bytearray(b'') file_name = '' sndsec_hdr = bfile_h.read(signa_len) if sndsec_hdr == Absnddat11.signa: file_name_size = struct.unpack('<H', bfile_h.read(Absnddat11.name_size_len))[0] file_name = bytes_shiftjis_to_utf8(bfile_h.read(file_name_size)) hash_size = struct.unpack('<H', bfile_h.read(Absnddat11.hash_size_len))[0] snd_hash = bfile_h.read(hash_size) unknown1 = bfile_h.read(Absnddat11.unknown1_len) snd_size = struct.unpack('<L', bfile_h.read(Absnddat11.data_size_len))[0] file_buffer.extend(bfile_h.read(snd_size)) else: file_name_size = struct.unpack('<H', bfile_h.read(Absnddat11.name_size_len))[0] file_name = bytes_shiftjis_to_utf8(bfile_h.read(file_name_size)) hash_size = struct.unpack('<H', bfile_h.read(Absnddat11.hash_size_len))[0] snd_hash = bfile_h.read(hash_size) unknown1 = bfile_h.read(Absnddat11.unknown1_len) snd_size = struct.unpack('<L', bfile_h.read(Absnddat11.data_size_len))[0] file_buffer.extend(bfile_h.read(snd_size)) for i, letter in enumerate(file_name): if letter == '<' or letter == '>' or letter == '*' or letter == '/': file_name[i] = '_' outfile_name = prepare_filename(b_file, output_dir, '_' + file_name + check_type(file_buffer)) print("create absound") create_file(outfile_name, file_buffer) file_buffer = bytearray(b'') 


Script akan secara otomatis membuka file png, jpg, bmp, ogg dan wav yang ditemukan. Tapi selain itu, file imoavi yang tidak dikenal juga ditemukan di dalamnya.

Intinya adalah bahwa dalam permainan semua animasi dibuat baik sebagai video penuh dalam format ogv, atau sebagai gambar animasi mesin yang direkam dalam file .b, atau sebagai urutan animasi file jpg dalam format imoavi.

Dalam hal ini, kami juga tertarik pada gambar jpg, jadi kami harus menghadapinya juga.

Ada dua bagian dalam imoavi: SUARA dan FILM. Di bagian FILM, 47 byte setelah header, ada empat byte ukuran file jpg. File ditulis satu demi satu dalam bentuk aslinya, dipisahkan oleh urutan 19 byte, di mana ukuran file selanjutnya direkam.

Imoavi bersuara dalam permainan tidak menemukan, jadi bagian SUARA selalu kosong.

Yah, karena kami mulai menarik semua sumber daya permainan, pada saat yang sama sebuah skrip kecil ditulis untuk menarik jpg dari imoavi.

Ekstraktor Imoavi
 # -*- coding: utf-8 -*- # Extract imoavi # Imoavi extractor for Bishoujo Mangekyou game files # by Chtobi and Nazon, 2016 import glob import os import struct import argparse imoavi_hdr = b'IMOAVI' hdr_len = len(imoavi_hdr) def create_file(file_name, out_buffer, wr_mode='wb'): if len(out_buffer) != 0: with open(file_name, wr_mode) as ext_file: ext_file.write(out_buffer) else: print("Zero file. Skipped.") def prepare_filename(file_name, out_dir, postfix=''): ready_name = out_dir + os.path.basename(file_name) + postfix return ready_name def create_parser(): arg_parser = argparse.ArgumentParser(prog='Imoavi extractor\n', usage='extract_imoavi input_file_name output_dir\n', description='Imoavi extractor for QLIE engine *.imoavi files.\n') arg_parser.add_argument('input_file_name', nargs='+', help="Input file with full path(wildcards are supported).\n") arg_parser.add_argument('output_dir', nargs='+', help="Output directory.\n") return arg_parser if __name__ == '__main__': parser = create_parser() arguments = parser.parse_args() all_imoavi = glob.glob(arguments.input_file_name[0]) output_dir = arguments.output_dir[0] for imoavi_f in all_imoavi: file_buffer = bytearray(b'') with open(imoavi_f, 'rb') as imoavi_h: # Read imoavi file header imoavi_h.read(hdr_len) imoavi_h.seek(2, os.SEEK_CUR) # 0x00 imoavi_h.seek(1, os.SEEK_CUR) # 0x64 imoavi_h.seek(3, os.SEEK_CUR) # 0x00 imoavi_h.seek(5, os.SEEK_CUR) # SOUND imoavi_h.seek(3, os.SEEK_CUR) # 0x00 imoavi_h.seek(1, os.SEEK_CUR) # 0x64 imoavi_h.seek(11, os.SEEK_CUR) imoavi_h.seek(5, os.SEEK_CUR) # Movie imoavi_h.seek(3, os.SEEK_CUR) # 00 ?? imoavi_h.seek(1, os.SEEK_CUR) # 0x64 imoavi_h.seek(3, os.SEEK_CUR) # 0x00 ?? imoavi_h.seek(4, os.SEEK_CUR) # ?? imoavi_h.seek(1, os.SEEK_CUR) # Number of jpg files in section imoavi_h.seek(4, os.SEEK_CUR) # 0x00 imoavi_h.seek(1, os.SEEK_CUR) # 0x05 ??? imoavi_h.seek(2, os.SEEK_CUR) # 0x00 ?? imoavi_h.seek(4, os.SEEK_CUR) # 720 ?? imoavi_h.seek(4, os.SEEK_CUR) # Full size without header? to_next_size = struct.unpack('<L', imoavi_h.read(4))[0] # Bytes till next header imoavi_h.seek(16, os.SEEK_CUR) # 0x00 jpg_size = struct.unpack('<L', imoavi_h.read(4))[0] imoavi_h.seek(4, os.SEEK_CUR) # 0x00 file_num = 0 file_buffer.extend(imoavi_h.read(jpg_size)) outfile_name = prepare_filename(imoavi_f, output_dir, '_' + (str(file_num)).zfill(3) + '.jpg') create_file(outfile_name, file_buffer) while to_next_size != 0: file_buffer = bytearray(b'') to_next_size = struct.unpack('<L', imoavi_h.read(4))[0] if to_next_size == 24: # 0x1C header for index part file_buffer.extend(imoavi_h.read(to_next_size)) outfile_name = prepare_filename(imoavi_f, output_dir, '_' + '.index') create_file(outfile_name, file_buffer, 'ab') # concatenate with index file else: imoavi_h.seek(2, os.SEEK_CUR) # unknown imoavi_h.seek(2, os.SEEK_CUR) # Unknown, almost always FF FF or FF FE file_num = struct.unpack('B', imoavi_h.read(1))[0] # File number imoavi_h.seek(11, os.SEEK_CUR) # 0x00 jpg_size = struct.unpack('<L', imoavi_h.read(4))[0] imoavi_h.seek(4, os.SEEK_CUR) # 0x00 file_buffer.extend(imoavi_h.read(jpg_size)) outfile_name = prepare_filename(imoavi_f, output_dir, '_' + (str(file_num)).zfill(3) + '.jpg') create_file(outfile_name, file_buffer) 


Setelah membongkar, Anda dapat memastikan bahwa animasi dari layar splash di menu disimpan hanya dalam file 1_ タ イ ト ル 画面 ム ー ー ー ー .b dalam format imoavi.


Itu semua dengan sumber daya game.

Sayangnya, proses penerjemahan mengungkapkan beberapa nuansa yang lebih tidak menyenangkan yang tidak dapat diatasi. Gim, seperti yang sudah saya tulis, tidak mendukung penyandian Unicode. Oleh karena itu, semua teks yang diterjemahkan ditampilkan dengan spasi huruf yang salah. Ada beberapa masalah dengan file backpacking dan memulai permainan tanpa mengubah sistem encoding ke Jepang.

Pada titik tertentu, kami (atau lebih tepatnya, orang yang bertanggung jawab atas bagian teknis terjemahan dalam tim kami) berpikir: mungkin kita tidak boleh berkeliaran dengan mesin yang lama, tetapi memasukkan novel ke mesin Renpy, pada saat yang sama mendapatkan cross-platform?
Mungkin kami sedang terburu-buru, tetapi pada suatu titik, sangat disayangkan untuk berhenti dari apa yang kami mulai dan tidak ada yang tersisa untuk dilakukan selain menyelesaikan terjemahan.

Apa yang kami temui saat porting?
Tentang ini di bagian kedua.

Tautan:

Skrip

bitbucket kami Tentang mesin Qlie Jepang,

Tabel penyandian Shift Jis

Baca lebih lanjut tentang masalah transcoding dari Shift Jis ke UTF-8

asmodean utilitas exfp3_v3

Source: https://habr.com/ru/post/id426431/


All Articles