تفكيك محرك الرواية المرئية Qlie



إن ترجمة الهواة للقصص المرئية القصيرة ، عند مقارنتها بترجمات الألعاب الأخرى ، لها عدد من الميزات وتتضمن العمل مع الكثير من النصوص. ربما تم إصدار الغالبية العظمى من الروايات البصرية باللغة اليابانية ، ولم يُترجم سوى عدد قليل منها إلى الإنجليزية (رسميًا أو بواسطة هواة) ، وتمت ترجمة عدد أقل إلى لغات أخرى.

لذلك ، عند العمل مع الترجمة ، يجب عليك التعامل مع المحركات اليابانية ، والعديد منها ليس مناسبًا جدًا للموطنين. وبسبب هذا ، فإنه يأتي بسرعة إلى إدراك أن وجود مهارات الترجمة ، ومعرفة اللغة ، والحماس الكبير ووقت الفراغ لا يعني أن النسخة المترجمة من اللعبة سترى النور قريبًا.

تقريبيًا جدًا ، تعني عملية ترجمة أي لعبة (وليس فقط القصص القصيرة المرئية):

  • تفريغ موارد اللعبة (إذا لم تكن في المجال العام)
  • ترجمة الأجزاء الضرورية
  • نقل التعبئة العكسية

ومع ذلك ، في حالة القصص القصيرة البصرية اليابانية ، يبدو هذا عادةً كما يلي:

  • تفريغ موارد اللعبة
  • ترجمة جزء النص من اللعبة (نص البرنامج النصي)
  • ترجمة الجزء الرسومي من اللعبة
  • نقل التعبئة العكسية
  • تعديل المحرك ليعمل مع المحتوى المترجم

آمل أن تكون تجربتنا مفيدة لشخص ما.

في عام 2013 (وربما قبل ذلك) قررت أن أترجم من اليابانية الرواية المرئية Bishoujo Mangekyou-Norowareshi Densetsu no Shoujo- (美 少女 万 華 鏡 - 呪 わ れ し 伝 説 の 少女 -). كانت لدي خبرة بالفعل في ترجمة الألعاب ، ولكن قبل أن أضطر إلى ترجمة القصص القصيرة فقط على محركات بسيطة ومعروفة مثل Kirikiri .

هنا ، كان على فريق المترجمين لدينا فتح محرك هذه القصة القصيرة ، حتى قبل أن نصل إلى النص الفعلي نفسه.

لنبدأ بوصف لملف exe. حيث ورد ذكر الكلمات QLIE و IMOSURUME. يحتوي الملف نفسه على سطر FastMM Borland Edition 2004، 2005 Pierre le Riche ، مما يعني أن المحرك مكتوب على الأرجح في دلفي.



يكشف googling السريع أن Qlie هو اسم محرك الرواية المرئية الذي أصدرته Warmth Entertainment. يبدو أن IMOSURUME هو الاسم الداخلي لمحرك النص البرمجي ، و Qlie هو الاسم التجاري. يوجد موقع qlie.net ، الذي يسرد الألعاب التي تم إصدارها على هذا المحرك والموقع الرسمي لـ Warmth Entertainment.

ولكن في أي مكان في المجال العام ، لا توجد أدوات رسمية للعمل مع المحرك ، ولا وثائق له ، وهو أمر متوقع.

لذلك ، يجب عليك التعامل مع اللعبة بنفسك ، بالاعتماد على المرافق غير الرسمية. بادئ ذي بدء ، يجب أن تجد جميع أجزاء اللعبة التي تحتاج إلى الترجمة.

توجد أرشيفات الألعاب في ملفات data0.pack و data1.pack و data7.pack في المجلد الفرعي \ GameData. توجد شاشات التوقف في المجلد \ GameData \ Movie ، ولكن لا يزال بإمكانك تركها بمفردها.


يظهر محرر hex أنه لا توجد رؤوس يمكن التعرف عليها لأرشيفات حزم اللعبة. ولكن في نهاية الملف توجد قطعة مشابهة لجدول المحتويات والملصق FilePackVer3.0


لحسن الحظ ، لهذا التنسيق ، هناك بالفعل فاتح حزم ولا حتى واحد. استخدمنا وحدة التحكم exfp3_v3 من asmodean.

التفريغ ليس سهلاً كما قد يبدو. نظرًا لأن المحرك يدعم العديد من تنسيقات الأرشيف (FilePackVer1.0 و FilePackVer1.0 و FilePackVer3.0) ، وفي هذه الحالة يتم استخدام FilePackVer3.0 ، وللفك الصحيح ، ستحتاج أيضًا إلى مفتاح ملف خاص key.fkey يقوم بتشفير الأرشيف. وهو موجود في المجلد الفرعي \ Dll


بالإضافة إلى ذلك ، يجب أن يوضح exfp3_v3 الأرشيف الذي يتم من خلاله تفريغ اللعبة.
لذلك ، تحتاج أيضًا إلى تحديد رقم اللعبة من القائمة التي اقترحها فاتح الحزم (توجد ألعاب سلسلة Bishoujo Mangekyou تحت الرقم 15) ، أو تحديد ملف اللعبة القابل للتنفيذ كمعلمة ثالثة لحزمة الحزم.


بالفعل بعد تفريغ ملفات اللعبة ، ظهرت فكرة منطقية: كيف في المستقبل كيف نعيد اللعبة بترجمة جاهزة؟ بعد كل شيء ، لا يدعم فاتح الحزم العملية العكسية.
بناءً على طلبنا ، أضاف 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" - في هذه الحالة ، اسم الملف من المجلد \ voice \ في ملف data1.pack من المثير للاهتمام أن يستخدم الفريق النسبة اليابانية (%) ، بدلاً من النسبة المعتادة.

^savedate, ^saveroute, ^savescene, - ثلاثة فرق يتم استخدامها على الأرجح في نظام حفظ اللعبة ويجب إدخال معلومات عن المكان والوقت الذي تم فيه حفظ اللاعب في لعبة الحفظ.

على سبيل المثال:

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

أي ، التاريخ: الحاضر ، الفرع: Bishoujo Mangekyou -1- ، المشهد: Norowareshi Densetsu no Shoujo Opening. كان يجب عرض هذه البيانات في فتحة الحفظ ، ولكن يبدو أن المطورين قرروا التخلي عنها. نتيجة لذلك ، ^saveroute في جميع أجزاء البرنامج النصي ، و ^savedate التغييرات من "اللحظة الحالية" إلى "الأحلام" ، وفي ^savescene أيام اللعبة (أو بالأحرى الليالي).

^facewindow, - حالة مربع النص مع النص المعروض على الشاشة. (تظهر - 1 أم لا - 0)

^sload, - تشغيل أصوات داخل اللعبة من المجلد \ sound \ على القناة المقابلة.

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

لعب السيكادا على Env1

يحتوي الفريق على معلمتين اختياريتين ، الأولى مسؤولة عن تكرار الصوت ، والثانية تظل غامضة ، ولكن نادرًا ما يتم استخدامها في اللعبة.

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

قراءة صوت الاسترجاع على القناة SE1.

^eeffect - عرض تأثير خاص على الشاشة لعدد معين من الثواني. على ما يبدو ، فإنه يدعم الإخراج المتسلسل لعدة تأثيرات.

 ^eeffect,WhiteFlash 

تأثير وميض أبيض.

^ffade - تأثير الانتقال عند تغيير الشاشة.
يحتوي على مجموعة كاملة من المعلمات الإضافية ، ولكن القليل منها فقط مفيد حقًا: اسم تأثير الانتقال ، وصورة إضافية ، إذا لزم الأمر ، ووقت إكمال الانتقال.

 ^ffade,Overlap,,1000 

تذويب صورة في أخرى ، في ثانية واحدة.

^iload - قم بتحميل صورة الخلفية على الشاشة. يمكن تعيين الصورة معرف للإشارة إليها في المستقبل.

 ^iload,BG1,0_black.png 

ملف الإخراج 0_black.png كخلفية بالمعرف BG1

^we و ^wd - قم بتشغيل وإيقاف الصورة في النافذة.

^facewindow,1 and ^facewindow,0 يقوم بتشغيل وإيقاف صورة البطل في مربع الحوار.

^mload - تشغيل الموسيقى على قناة معينة.

 ^mload,BGM1,nbgm13 

تشغيل المسار nbgm13 على قناة BGM1

من أهم الفرق:
\jmp - الانتقال إلى التسمية بالاسم المحدد.

^select - يعرض نافذة التحديد على الشاشة ، حيث يجب على اللاعب اختيار أحد الخيارات.

على سبيل المثال:

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

هنا سيتم تنفيذ الانتقال بعد الإجابة على السؤال ، ويتم إرجاع رقم الرد (0 أو 1) من ResultBtnInt [0]. نتيجة لذلك ، \jmp القصة إلى التسميةroute01a + رقم الرد. أي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 (U + FF5E FULLWIDTH TILDE) ، نتج عن خطأ "UnicodeEncodeError: لا يمكن لبرنامج ترميز" Shift Jis "ترميز الحرف" \ uff5e "في الموضع 0: تسلسل متعدد البايت غير قانوني"

في البداية ، أخطأت في Python ، ولكن في النهاية اكتشفت فارقًا بسيطًا إلى حد ما. هناك عدم يقين بين طرق ارتباط ترميزات Unicode وترميزات يابانية غير Unicode حسب التطبيق المحدد.

ونتيجة لذلك ، يربط Windows حرف Shift Jis برمز 0x8160 مع unicode ~ (U + FF5E FULLWIDTH TILDE) ، وترتبط أجهزة التحويل الأخرى (على سبيل المثال ، فائدة iconv) نفس الحرف مع 〜 (U + 301C WAVE DASH) ، وفقًا لجدول نسبة Unicode الرسمية ، وفقًا لجدول نسبة Unicode الرسمية - ftp://ftp.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/SHIFT JIS.TXT

لتحديد المراسلات بين الأحرف ، قررت Microsoft على ما يبدو استخدام المخططات من ترميز cp932 ، وهو إصدار موسع من Shift Jis.

يحدث نفس الموقف مع رمز الحرف 0x817C ، الذي تم ترميزه في UTF8 كـ - (U + FF0D FULLWIDTH HYPHEN-MINUS) على Windows ، أو - (U + 2212 MINUS SIGN) في iconv.

نظرًا لأن جميع ملفات البرامج النصية تم تحويلها أولاً من Shift Jis إلى UTF8 باستخدام Notepad ++ (ويستخدم جدول المراسلات المعتمد في Windows) ، عند التحويل مرة أخرى من UTF8 إلى Shift Jis من خلال برنامج Python ، ظهر خطأ التحويل سيئ السمعة.

لذلك ، كان من الضروري مراعاة حدوث separate و - شروط منفصلة.

كانت هناك عيوب طفيفة أخرى - على سبيل المثال ، تم حذف القطع الناقص (U + 2026 القطع الأفقي) بواسطة القطع الناقص السيريلي من cp1251 ، وليس اليابانيين من Shift Jis.

بعد ترجمة النص ، يمكنك المتابعة للعمل مع رسومات اللعبة.

توجد الملفات الرسومية للعبة في أرشيفات الحزمة نفسها ، ولكن بعد تفريغها ، لا يزال عليها العمل بجد. على سبيل المثال ، يتم تفريغ جميع صور png تقريبًا كملفات من نوع العينة + DPNG000 + x32y0.png بعبارة أخرى ، يتم قطع صور png إلى شرائح أفقية بسمك 88 سم ويتم كتابة كل شريط في ملف منفصل. يظهر اسم الملف الرقم التسلسلي للشريط (DPNG000 ... 009) وإحداثيات س ، ص.


ما زلت أتساءل لماذا كان هذا ضروريًا. إذا كان الأمر يتعلق بصعوبة نسخ الموارد من اللعبة ، فمن الواضح أن هذه ليست أفضل طريقة.

ولصق ملفات png المقطوعة ، تم إنشاء ملف نصي صغير merge_dpng على Pearl من asmodeus ، والذي يستخدم ImageMagick ، ​​في وقت واحد. لسوء الحظ ، كانت هناك مشاكل معه. أولاً ، كنت بحاجة إلى بيرل ، الذي لم أستخدمه ، وحتى بعد تثبيته ، اتضح أن النص لم يكن يعمل بشكل صحيح.

لهذا السبب ، كتبنا برنامجًا مشابهًا في 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 الجاهزة في حالتنا كما ينبغي. إما أن بعض الملفات بقيت بدون حزم ، أو كانت هناك أخطاء بسبب الأسماء اليابانية ، ولم أرغب في التمهيد من اللغة اليابانية.

هنا كان نص آخر مفيدًا. منذ ذلك الحين لم نكن على دراية بشيء مثل Kaitai Struct ، كان علينا أن نتصرف من الصفر تقريبًا.

تبين أن تنسيق ملفات .b بسيط ، علاوة على ذلك ، كان مطلوبًا من فاتح الحزم لدينا أن يكون قادرًا على فك حزم الموارد من هذه اللعبة فقط. في ألعاب أخرى على محرك Qlie ، ظهرت أنواع إضافية من الموارد داخل ملفات .b ، لكننا لن نتحدث عنها بالتفصيل.

لذا ، افتح أي ملف .b في محرر سداسي وانظر إلى البداية. قبل التقييم ، لاحظ أن ترتيب البايت لجميع القيم الرقمية سيكون Little-endian.

  • رأس ملف Abmp12
  • عشرة بايت 0x00
  • عنوان القسم الأول abdata12 بالمعلومات العامة.
  • ثمانية بايت 0x00
  • abdata12 مقطع المقطع ، عدد صحيح أربعة بايت. يمكنك تخطيه بأمان.
  • رأس مقطع Abimage10
  • سبعة بايت 0x00
  • عدد الملفات في قسم ، عدد صحيح أحادي البايت. في هذه الحالة ، يوجد ملف واحد في القسم.
  • عنوان المقطع abgimgdat13
  • ستة بايت 0x00
  • طول اسم الملف داخل القسم ، عدد صحيح ثنائي البايت. في هذه الحالة ، الطول 4 بايت.
  • اسم الملف المشفر لـ Shift Jis
  • طول سجل المجموع الاختباري للملف ، عدد صحيح مزدوج البايت.
  • المجموع الاختباري للملف نفسه.
  • يبدو أن البايت غير المعروف هو 0x03 أو 0x02
  • اثنا عشر بايت غير معروفة ، ربما تتعلق بالرسوم المتحركة
  • حجم ملف png داخل القسم هو عدد صحيح من أربعة بايت.

وأخيرًا ، ملف png نفسه.


قسم الغياب مشابه في التركيب.

مستخرج BMP متحرك
 # -*- 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 ، أو كتسلسلات متحركة لملفات jpg بتنسيق imoavi.

في هذه الحالة ، كنا مهتمين أيضًا بصور jpg ، لذلك كان علينا التعامل معها أيضًا.

هناك قسمان في imoavi: الصوت والصورة. في المقطع MOVIE ، 47 بايت بعد الرأس ، هناك أربعة بايت من حجم ملف jpg. تتم كتابة الملفات الواحدة تلو الأخرى في شكلها الأصلي ، مفصولة بتسلسل 19 بايت ، حيث يتم تسجيل حجم الملف التالي.

لم يتم العثور على imoavi المعبر عنه في اللعبة ، لذلك قسم SOUND فارغ دائمًا.

حسنًا ، منذ أن بدأنا في سحب جميع موارد اللعبة ، في نفس الوقت تم كتابة نص صغير لسحب jpg من imoavi.

مستخرج 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) 


, , 1_タイトル画面ムービー.b imoavi.


.

, , . , , . , . .

- (, , ) : , , Renpy, ?
, , - , , .

?
.

:

bitbucket

Qlie

Shift Jis

Shift Jis UTF-8

exfp3_v3 asmodean

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


All Articles