Qlieビゞュアルノベル゚ンゞンの解䜓









芖芚的な短線小説のアマチュア翻蚳は、他のゲヌムの翻蚳ず比范するず、倚くの機胜を備えおおり、倚くのテキストを操䜜する必芁がありたす。 おそらく、すべおのビゞュアルノベルの倧郚分は日本語でリリヌスされ、英語に翻蚳されたものはわずか公匏たたはアマチュアであり、他の蚀語に翻蚳されたものはさらに少なかったでしょう。



そのため、翻蚳を扱う際には、日本語゚ンゞンに察凊する必芁がありたすが、その倚くはロヌカラむザヌにあたり銎染みがありたせん。 このため、翻蚳スキル、蚀語の知識、倚くの熱意ず自由な時間の存圚は、ゲヌムの翻蚳バヌゞョンがたもなく日の目を芋るこずを意味するものではないこずにすぐに気付きたす。



非垞に倧たかに蚀っお、ゲヌムを翻蚳するプロセス芖芚的な短線小説だけでなくは、





ただし、日本の芖芚的な短線小説の堎合、これは通垞次のようになりたす。





私たちの経隓が誰かに圹立぀こずを願っおいたす。



2013幎おそらくそれ以前に、ビゞュアル小説「矎少女䞇華鏡-ノロワレシ䌝説の少女-矎少女䞇華鏡-呪われし䌝䌝の少女-」を日本語から翻蚳するこずにしたした。 私はすでにゲヌムの翻蚳の経隓がありたしたが、 キリキリのような比范的単玔で有名な゚ンゞンで短線を翻蚳するだけでした。



ここで、私たちの翻蚳者チヌムは、実際のテキスト自䜓に到達する前であっおも、この短線小説の゚ンゞンを開かなければなりたせんでした。



.exeファむルの説明から始めたしょう。ここには、QLIEずIMOSURUMEずいう蚀葉が蚘茉されおいたす。 ファむル自䜓には、行FastMM Borland Edition 2004、2005 Pierre le Richeが含たれおいたす。これは、゚ンゞンがDelphiで蚘述されおいる可胜性が高いこずを意味したす。















簡単なグヌグル怜玢で、QlieがWarmth Entertainmentがリリヌスしたビゞュアルノベル゚ンゞンの名前であるこずが明らかになりたした。 どうやらIMOSURUMEはスクリプト゚ンゞンの内郚名であり、Qlieは商甚名です。 この゚ンゞンでリリヌスされたゲヌムずWarmth Entertainmentの公匏Webサむトをリストするサむトqlie.netがありたす。



しかし、パブリックドメむンのどこにも、゚ンゞンを操䜜するための公匏ツヌルも、ドキュメントもありたせん。



したがっお、非公匏のナヌティリティに䟝存しお、自分でゲヌムを凊理する必芁がありたす。 たず、翻蚳が必芁なゲヌムのすべおの郚分を芋぀ける必芁がありたす。



ゲヌムアヌカむブは、\ GameDataサブフォルダヌのdata0.pack、data1.packおよびdata7.packファむルにありたす。 スクリヌンセヌバヌは\ GameData \ Movieフォルダヌにありたすが、そのたたにしおおくこずができたす。









16進゚ディタヌは、.packゲヌムアヌカむブの認識可胜なヘッダヌがないこずを瀺したすが、ファむルの最埌に目次ずラベルFilePackVer3.0に䌌た郚分がありたす









幞いなこずに、この圢匏には既にアンパッカヌがあり、1぀でもありたせん。 asmodeanのコン゜ヌルexfp3_v3を䜿甚したした。



開梱は芋た目ほど簡単ではありたせん。 ゚ンゞンはいく぀かのアヌカむブ圢匏FilePackVer1.0、FilePackVer1.0、FilePackVer3.0をサポヌトしおいるため、この堎合はFilePackVer3.0が䜿甚されたす。適切な解凍には、アヌカむブを暗号化する特別なキヌファむルkey.fkeyも必芁です。 \ Dllサブフォルダヌにありたす









さらに、exfp3_v3は、展開するゲヌムのアヌカむブを明確にする必芁がありたす。

そのため、アンパッカヌが提案するリストからゲヌム番号を指定する必芁がありたす矎少女䞇華鏡シリヌズのゲヌムは15番䞋にありたす、たたはアンパッカヌの3番目のパラメヌタヌずしおゲヌム実行可胜ファむルを指定したす。









すでにゲヌムファむルを解凍した埌、論理的な考えが浮䞊したした将来、どのようにゲヌムをすぐに翻蚳しおパックするか 結局、アンパッカヌは逆の操䜜をサポヌトしおいたせん。

リク゚ストに応じお、w8mありがずうございたすは、ゲヌムアヌカむブをプログラムarc_conv.exeにパックする機胜を远加したした。 倉曎されたすべおのファむルを新しいアヌカむブdata8.packなどにパックし、GameDataフォルダヌに配眮するだけで十分です。自動的にゲヌムに匕き蟌たれたす。



展開されたリ゜ヌスに戻りたす。 data0.packアヌカむブのゲヌムスクリプトファむルは、サブフォルダヌ\シナリオ\ ks_01 \にありたす。



拡匵子が.sのすべおのスクリプトファむルは、最も䟿利なShift Jis゚ンコヌディングずはかけ離れお゚ンコヌドされ、゚ンゞンはUnicode゚ンコヌディングをサポヌトしおいたせん。 翻蚳の行は、ほが次のようになりたす。



【キリ゚】 1_kiri1478 「ぞえ  分かっおいるじゃない」 私が献䞊したロシアンティヌを芋お、キリ゚は嬉しそうに目を现める。 ^cface,,赀目埮笑01 【キリ゚】 1_kiri1479 「日本人は、ゞャムを玅茶に入れお飲むのが、ロシアンティヌだず勘違いしおいる人が倚いのだけれど  」
      
      





日本語の各フレヌズの前には、日本語の括匧で囲たれた䞻人公の名前が付いおいるこずがありたす。 【】、このフレヌズを発音したすゲヌムでは、りィンドりの䞊郚にテキストで衚瀺されたす。 たたは、これらが著者の蚀葉である堎合、名前は远加されたせん。









しかし、ただサヌビスチヌムがありたす。



スクリプト内の゚ンゞンコマンドは、TeXマヌクアップ蚀語を連想させたすが、 KirikiriたたはRenPyコマンドに比べおはるかに盎感的で䞍䟿です。



それらのいく぀かを次に瀺したす。



@@@



はトリプルドッグです。 倚くの堎合、スクリプトファむルはこのコマンドで始たりたす。 サヌドパヌティのファむルから明らかに定矩をロヌドしおいたす。



䟋



 @@@Library\Avg\header.s
      
      





@@



はダブルドッグです。 スクリプトファむルのラベル。 埌で切り替えるこずができたす。



1_kiri1478



-音声ファむルを再生したす。 これらのコマンドは、ヒヌロヌの名前ず画面に衚瀺されるテキストの間に挿入されたす。 「1_kiri1478」-この堎合、data1.packファむルの\ voice \フォルダヌのファむル名チヌムが通垞の割合ではなく、日本の割合を䜿甚するのは興味深いこずです。



^savedate, ^saveroute, ^savescene,



-ゲヌムのセヌブシステムで䜿甚される可胜性が最も高い3぀のチヌムで、セヌブゲヌムでプレむダヌがセヌブされた堎所ず時間に関する情報を入力する必芁がありたす。



䟋



 ^savedate,"珟圚" ^saveroute,"矎少女䞇華鏡" ^savescene,"呪われし䌝説の少女 オヌプニング"
      
      





぀たり、日付珟圚、支店矎少女䞇華鏡-1-、シヌンノロワレシ䌝説の少女オヌプニング。 このデヌタは保存スロットに衚瀺されるはずでしたが、明らかに開発者はそれを攟棄するこずに決めたした。 その結果、 ^saveroute



スクリプトのすべおの郚分で^saveroute



です。 ^savedate



「珟圚の瞬間」から「倢」ぞの倉曎を^savescene



でゲヌム内の日数たたは倜を倉曎したす。



^facewindow,



-テキストが画面に衚瀺されおいるテキストボックスの状態。 衚瀺-1かどうか-0



^sload,



-察応するチャンネルの\ sound \フォルダからゲヌム内のサりンドを再生したす。



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





Env1でセミを再生する



チヌムには2぀のオプションパラメヌタがあり、1぀目はサりンドのルヌプを担圓し、2぀目は謎のたたですが、ゲヌムではほずんど䜿甚されたせん。



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





チャンネルSE1でルヌプバックサりンドを再生したす。



^eeffect



特定の秒数の間、画面に特殊効果を衚瀺したす。 どうやら、それはいく぀かの効果の順次出力をサポヌトしおいたす。



 ^eeffect,WhiteFlash
      
      





癜い閃光の効果。



^ffade



画面を倉曎したずきのトランゞション効果。

たくさんの远加パラメヌタヌがありたすが、実際に圹立぀のはほんのわずかですトランゞション効果の名前、必芁に応じお远加の画像、トランゞション完了時間。



 ^ffade,Overlap,,1000
      
      





1秒で1぀の画像を別の画像に分解したす。



^iload



画面に背景画像をロヌドしたす。 むメヌゞには、将来参照するためのIDを割り圓おるこずができたす。



 ^iload,BG1,0_black.png
      
      





出力ファむル0_black.pngをID BG1の背景ずしお



^we



ず^wd



りィンドり内の画像のオンずオフを^wd



たす。



^facewindow,1



および^facewindow,0



ダむアログボックスでヒヌロヌ画像のオンずオフを^facewindow,0



たす。



^mload



特定のチャンネルで音楜を再生したす。



 ^mload,BGM1,nbgm13
      
      





チャンネルBGM1でトラックnbgm13を再生する



最も重芁なチヌムの䞀郚

\jmp



指定された名前のラベルにゞャンプしたす。



^select



プレむダヌがオプションのいずれかを遞択する必芁がある遞択りィンドりを画面に衚瀺したす。



䟋



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





ここでは、質問ぞの回答埌に遷移が実行され、回答番号0たたは1がResultBtnInt [0]から返されたす。 その結果、 \jmp



ストヌリヌをラベル@@ route01a + response numberに移動したす。 ぀たり、@@ route01a0たたは@@ route01a1



䞍快な機胜は、これらのコマンドの通垞のコンマが区切り文字ずしお機胜し、回答オプション自䜓で䜿甚できないこずです。 日本人はそのような問題はありたせん、圌らは日本語のコンマ、を䜿甚したす。 この堎合、カンマを「U + 201A SINGLE LOW-9 QUOTATION MARK」に眮き換えるこずができたす。



䟋



 ^select, ‚  , ‚ 
      
      





残りのチヌムは、最初の近䌌ではそれほど重芁ではありたせん。



もちろん、スクリプトを翻蚳する前に、キリル文字ず日本語の文字を結合するために、より䟿利なもの、たずえばUTF-8に倉換する必芁がありたす。



゚ンゞンを倉曎するずこの次の郚分に぀いお、ゲヌムはロシア語のテキストず日本語の䞡方を認識したす。 ただし、珟時点では、互換性のために、Shift Jisで日本語文字を゚ンコヌドし、cp1251゚ンコヌドでキリル文字を゚ンコヌドする必芁がありたす。



キリル文字を考慮しおトランスコヌドするためのPythonのプログラムをすばやくスケッチしたした。



UTF8からcp1251および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.")
      
      







しかし、いく぀かの問題がありたした。 プログラムは、「チルダ」蚘号〜U + FF5E FULLWIDTH TILDEを再コヌディングしようずするず、゚ラヌ「UnicodeEncodeError 'Shift Jis' codec ca n't encoding character '\ uff5e' in position 0illegal multibyte sequence」を生成したした



最初はPythonに察しお眪を犯したしたが、最終的にはかなり珍しいニュアンスを芋぀けたした。 特定の実装に応じお、ナニコヌドず非ナニコヌドの日本語゚ンコヌディングの盞関方法には䞍確実性がありたす。



その結果、Windowsはコヌド0x8160のShift Jis文字をナニコヌド〜U + FF5E FULLWIDTH TILDEに関連付け、他のトランスコヌダヌたずえばiconvナヌティリティは、公匏のUnicode比率衚に埓っお、同じ文字を〜U + 301C WAVE DASHず関連付けたす-ftp://ftp.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/SHIFT JIS.TXT



文字間の察応を決定するために、マむクロ゜フトは明らかに、Shift Jisの拡匵バヌゞョンであるcp932゚ンコヌディングのスキヌムを䜿甚するこずを決定したした。



同じ状況は、Windowsでは-U + FF0D FULLWIDTH HYPHEN-MINUS、たたはiconvでは-U + 2212 MINUS SIGNずしおUTF8に倉換される文字コヌド0x817Cでも発生したす。



すべおのスクリプトファむルは最初にNotepad ++を䜿甚しおShift JisからUTF8に倉換されたためWindowsで採甚された察応衚を䜿甚、Pythonプログラムを介しおUTF8からShift Jisに倉換するず、悪名高い倉換゚ラヌが発生したした。



そのため、〜ずの別々の条件の発生を考慮する必芁がありたした。



その他の小さな欠陥がありたした。たずえば、省略蚘号U + 2026氎平楕円は、Shift Jisの日本人ではなく、cp1251のキリル語の省略蚘号に眮き換えられたした。



テキストを翻蚳した埌、ゲヌムグラフィックスの䜜業に進むこずができたす。



ゲヌムのグラフィックファむルは同じパックアヌカむブ内にありたすが、展開埌も匕き続き䞀生懞呜䜜業する必芁がありたす。 たずえば、ほがすべおのpng画像は、sample + DPNG000 + x32y0.pngタむプのファむルの圢匏で解凍されたす。぀たり、png画像は88 cmの厚さの氎平ストリップに切り分けられ、各ストリップは個別のファむルに曞き蟌たれたす。 ファむル名には、ストリップのシリアル番号DPNG000 ... 009ずx、y座暙が衚瀺されたす。









なぜこれが必芁なのか、ただ疑問に思っおいたす。 ゲヌムからリ゜ヌスをリッピングするのが難しい堎合、これは明らかに最善の方法ではありたせん。



カットされたpngファむルを接着するために、䞀床にImageMagickを䜿甚するasmodeusのPearlに小さなスクリプトmerge_dpngが䜜成されたした。 残念ながら、圌には問題がありたした。 たず、私は䜿甚しなかったPearlが必芁でした。むンストヌルした埌でも、スクリプトが正しく機胜しおいないこずがわかりたした。



このため、Pythonで同様のプログラムを䜜成したした。



Qlie゚ンゞンのdpngファむルのマヌゞ
 # -*- 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])
      
      







ゲヌムに衚瀺される䞀連の写真がすべお揃ったように芋えたすか すべおではありたせん-すべおのアヌカむブから接続されたすべおの写真を芋るず、ゲヌム内にあるにもかかわらず、䞀郚が欠萜しおいるこずがわかりたす。 実際、゚ンゞンには別の皮類のファむルがあり、拡匵子は.bです。 画像ず音声が内郚に蚘録されたアニメヌションです。



リ゜ヌスを内郚に保存するのは非垞に簡単ですが、残念ながら、この堎合の既補の.bファむルのアンパッカヌは、うたく機胜しおいたせんでした。 いく぀かのファむルが解凍されたたたであるか、日本語名による゚ラヌがありたしたが、日本語ロケヌルから起動したくありたせんでした。



もう1぀のスクリプトが圹に立ちたした。 それ以来、 Kaitai Structのようなものに慣れおいたせんでした。ほずんどれロから行動しなければなりたせんでした。



.bファむル圢匏はシンプルであるこずが刀明し、さらに、このゲヌムからのみリ゜ヌスをアンパックできるようにするために、アンパッカヌが必芁でした。 Qlie゚ンゞン䞊の他のゲヌムでは、远加の皮類のリ゜ヌスが.bファむル内に珟れたしたが、それらに぀いおは詳しく説明したせん。



そのため、16進゚ディタヌで.bファむルを開き、先頭を確認したす。 評䟡する前に、すべおの数倀のバむト順がリトル゚ンディアンになるこずに泚意しおください。





そしお最埌に、pngファむル自䜓。









アブサりンドセクションの構造はアブむメヌゞに䌌おいたす。



AnimatedBMP゚クストラクタヌ
 # -*- 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'')
      
      







スクリプトは、芋぀かったpng、jpg、bmp、ogg、wavファむルを自動的に解凍したす。 しかし、これに加えお、未知のimoaviファむルも内郚にありたす。



芁するに、ゲヌムでは、すべおのアニメヌションはogv圢匏の本栌的なビデオずしお、たたは.bファむルに蚘録される゚ンゞンアニメヌション画像ずしお、たたはimoavi圢匏のjpgファむルのアニメヌションシヌケンスずしお䜜成されたす。



この堎合、私たちはjpg画像にも興味があったので、それらにも察凊しなければなりたせんでした。



imoaviには、SOUNDずMOVIEの2぀のセクションがありたす。 ヘッダヌの埌の47バむトのMOVIEセクションには、4バむトのjpgファむルサむズがありたす。 ファむルは、19バむトのシヌケンスで区切られた元の圢匏で次々に曞き蟌たれ、次のファむルのサむズが蚘録されたす。



ゲヌム内の有声のimoaviは出䌚わなかったため、SOUNDセクションは垞に空です。



ゲヌムのすべおのリ゜ヌスを匕き出し始めたので、同時にimoaviからjpgを匕き出すための小さなスクリプトが䜜成されたした。



むモアビ抜出噚
 # -*- 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)
      
      







解凍埌、メニュヌのスプラッシュ画面からのアニメヌションがimoavi圢匏のファむル1_タむトル画面ムヌビヌ.bに保存されおいるこずを確認できたす。









それはすべおゲヌムリ゜ヌスにありたす。



残念ながら、翻蚳プロセスでは克服できないいく぀かの䞍快なニュアンスが明らかになりたした。すでに曞いたように、このゲヌムはUnicode゚ンコヌドをサポヌトしおいたせん。したがっお、翻蚳されたテキストはすべお、間違った文字間隔で衚瀺されたす。システム゚ンコヌディングを日本語に倉曎せずにファむルをバックパックし、ゲヌムを開始するず、さらにいく぀かの問題が発生したした。



ある時点で、私たちたたは、チヌムの翻蚳の技術的な郚分を担圓した人は考えたしたおそらく、叀い゚ンゞンをいじるのではなく、クロスプラットフォヌムを取埗しながら、Renpy゚ンゞンにストヌリヌを移怍するべきでしょうか

おそらく私たちは急いでいたのでしょうが、ある時点で、私たちが始めたものをやめるのは残念であり、翻蚳を終える以倖に䜕も残されおいたせんでした。



移怍䞭に䜕に遭遇したしたか

第二郚でこれに぀いお。



参照



Bitbucketスクリプト



日本語Qlie゚ンゞンに぀いお



Shift Jis ゚ンコヌドテヌブルShift Jis



からUTF-8ぞの倉換問題に぀いお詳しく読む



asmodean exfp3_v3ナヌティリティ



All Articles