Desmontando o mecanismo de romance visual Qlie



Uma tradução amadora de contos visuais, quando comparada com traduções de outros jogos, possui vários recursos e envolve trabalhar com muito texto. Talvez a grande maioria de todos os romances visuais tenha sido lançada em japonês, apenas alguns foram traduzidos para o inglês (oficialmente ou por amadores), e menos ainda foram traduzidos para outros idiomas.

Portanto, ao trabalhar com tradução, você precisa lidar com os mecanismos japoneses, muitos dos quais não são muito amigáveis ​​para os localizadores. Por causa disso, rapidamente se percebe que a presença de habilidades de tradução, conhecimento do idioma, grande entusiasmo e tempo livre não significa que a versão traduzida do jogo em breve verá a luz do dia.

Muito grosso modo, o processo de tradução de qualquer jogo (não apenas histórias curtas visuais) implica:

  • Descompactando recursos do jogo (se não estiverem em domínio público)
  • Tradução das peças necessárias
  • Transferência de embalagem reversa

No entanto, no caso de contos visuais japoneses, isso geralmente se parece com:

  • Desempacotando recursos do jogo
  • Tradução da parte do texto do jogo (script do jogo)
  • Tradução da parte gráfica do jogo
  • Transferência de embalagem reversa
  • Alteração do mecanismo para fazê-lo funcionar com conteúdo traduzido

Espero que nossa experiência seja útil para alguém.

Em 2013 (e possivelmente mais cedo), decidi traduzir do japonês o romance visual Bishoujo Mangekyou-Norowareshi Densetsu no Shoujo- (美 少女 万 華 鏡 - 呪 わ れ し 伝 説 少女 -). Eu já tinha experiência em traduzir jogos, mas antes tinha que traduzir apenas histórias curtas em mecanismos relativamente simples e conhecidos como Kirikiri .

Aqui, nossa equipe de tradutores teve que abrir o mecanismo deste conto, mesmo antes de chegarmos ao texto propriamente dito.

Vamos começar com uma descrição do arquivo .exe, onde são mencionadas as palavras QLIE e IMOSURUME. O arquivo em si contém a linha FastMM Borland Edition 2004, 2005 Pierre le Riche, o que significa que o mecanismo provavelmente está escrito em Delphi.



Uma rápida pesquisa revela que Qlie é o nome do mecanismo de romance visual lançado pela Warmth Entertainment. Aparentemente, IMOSURUME é o nome interno do mecanismo de script e Qlie é o nome comercial. Existe um site qlie.net , que lista os jogos lançados neste mecanismo e o site oficial da Warmth Entertainment.

Mas em nenhum lugar do domínio público não há ferramentas oficiais para trabalhar com o mecanismo, nem documentação para isso, o que é esperado.

Portanto, você deve lidar com o jogo, contando com utilitários não oficiais. Para começar, você deve encontrar todas as partes do jogo que precisam ser traduzidas.

Os arquivos do jogo estão localizados nos arquivos data0.pack, data1.pack e data7.pack na subpasta \ GameData. Os protetores de tela estão na pasta \ GameData \ Movie, mas você ainda pode deixá-los em paz.


O editor hexadecimal mostra que não há cabeçalhos reconhecíveis para os arquivos .pack do jogo, mas no final do arquivo há uma peça semelhante ao sumário e ao rótulo FilePackVer3.0


Felizmente, para este formato, já existe um desempacotador e nem mesmo um. Usamos o console exfp3_v3 da asmodean.

Desembalar não é tão fácil quanto parece. Como o mecanismo suporta vários formatos de arquivo morto (FilePackVer1.0, FilePackVer1.0, FilePackVer3.0) e, neste caso, o FilePackVer3.0 é usado, para a descompactação correta, você também precisará de um arquivo de chave especial key.fkey, que criptografou o arquivo. Ele está localizado na subpasta \ Dll


Além disso, exfp3_v3 deve esclarecer o arquivo de qual jogo está descompactando.
Portanto, você também precisa especificar o número do jogo na lista proposta pelo desempacotador (os jogos da série Bishoujo Mangekyou estão no número 15) ou especificar o arquivo executável do jogo como o terceiro parâmetro para o desempacotador.


Depois de descompactar os arquivos do jogo, surgiu um pensamento lógico: como no futuro como empacotar o jogo com uma tradução pronta? Afinal, o desembalador não suporta a operação reversa.
A nosso pedido, o w8m (muito obrigado por isso) adicionou a capacidade de compactar arquivos do jogo em seu programa arc_conv.exe. É o suficiente para empacotar todos os arquivos alterados em um novo arquivo (por exemplo, data8.pack), colocá-lo na pasta GameData e eles entrarão automaticamente no jogo.

Voltar para os recursos descompactados. Os arquivos de script do jogo do arquivo data0.pack podem ser encontrados na subpasta \ scenery \ ks_01 \

Todos os arquivos de script com a extensão .s são codificados longe da codificação Shift Jis mais conveniente, e o mecanismo não suporta nenhuma codificação unicode. As linhas da tradução se parecem aproximadamente com estas:

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

Você pode perceber que cada frase em japonês é precedida pelo nome do herói entre colchetes japoneses. (【】), Que pronuncia esta frase (no jogo ela é exibida no topo da janela com texto). Ou, se essas são as palavras do autor, o nome não é adicionado.


Mas ainda existem equipes de serviço.

Os comandos do mecanismo no script lembram um pouco a linguagem de marcação TeX, mas são muito mais intuitivos e inconvenientes em comparação com os comandos Kirikiri ou RenPy .

Aqui estão alguns deles:

@@@ é um cão triplo. Geralmente, os arquivos de script começam com este comando. Aparentemente, carregando definições de arquivos de terceiros.

Por exemplo:

 @@@Library\Avg\header.s 

@@ é um cachorro duplo. O rótulo no arquivo de script. Você pode mudar para isso mais tarde.

%1_kiri1478% - reproduz o arquivo de voz. Esses comandos são inseridos entre o nome do herói e o texto exibido na tela. “1_kiri1478” - nesse caso, o nome do arquivo da pasta \ voice \ do arquivo data1.pack É interessante que a equipe use a porcentagem em japonês (%), em vez da usual.

^savedate, ^saveroute, ^savescene, - três equipes que provavelmente são usadas no sistema de salvamento do jogo e devem inserir informações sobre o local e a hora em que o jogador foi salvo no jogo salvo.

Por exemplo:

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

Ou seja, data: presente, filial: Bishoujo Mangekyou -1-, cena: Norowareshi Densetsu no Shoujo Opening. Esses dados deveriam ter sido exibidos no slot de salvamento, mas aparentemente os desenvolvedores decidiram abandoná-lo. Como resultado, o ^saveroute em todas as partes do script, ^savedate alterações do "momento presente" para "sonhos" e, na ^savescene os dias no jogo (ou melhor, as noites) mudam.

^facewindow, - estado da caixa de texto com o texto exibido na tela. (Mostrado - 1 ou não - 0)

^sload, - reproduz sons do jogo na pasta \ sound \ no canal correspondente.

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

Tocando cigarras no Env1

A equipe tem dois parâmetros opcionais, o primeiro é responsável por repetir o som e o segundo permanece um mistério, mas raramente é usado no jogo.

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

Reproduzir som de loopback no canal SE1.

^eeffect - exibe um efeito especial na tela por um determinado número de segundos. Aparentemente, ele suporta saída sequencial de vários efeitos.

 ^eeffect,WhiteFlash 

O efeito de um flash branco.

^ffade - efeito de transição ao alterar a tela.
Ele tem vários parâmetros adicionais, mas apenas alguns são realmente úteis: o nome do efeito de transição, uma imagem adicional, se necessário, e o tempo de conclusão da transição.

 ^ffade,Overlap,,1000 

Dissolvendo uma imagem em outra, em 1 segundo.

^iload - carrega a imagem de fundo na tela. É possível atribuir à imagem um ID para referência futura.

 ^iload,BG1,0_black.png 

Arquivo de saída 0_black.png como plano de fundo com o ID BG1

^we e ^wd - liga e desliga a imagem na janela.

^facewindow,1 e ^facewindow,0 Ativa e desativa a imagem do herói na caixa de diálogo.

^mload - reproduz música em um canal específico.

 ^mload,BGM1,nbgm13 

Reproduzindo a faixa nbgm13 no canal BGM1

Algumas das equipes mais importantes:
\jmp - pula para o rótulo com o nome especificado.

^select - exibe a janela de seleção na tela, onde o jogador deve escolher uma das opções.

Por exemplo:

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

Aqui a transição será realizada após a resposta à pergunta e o número da resposta (0 ou 1) é retornado de ResultBtnInt [0]. Como resultado, \jmp moverá a história para o rótulo @@ route01a + número de resposta. Ou seja, @@ route01a0 ou @@ route01a1

Um recurso desagradável é que a vírgula usual nesses comandos serve como separador e não pode ser usada nas próprias opções de resposta. Os japoneses não têm esse problema, eles usam a vírgula japonesa (、). Nesse caso, podemos substituir a vírgula por ‚(U + 201A ÚNICA MARCA DE COTAÇÃO BAIXA-9).

Por exemplo:

 ^select, ‚  , ‚  

As demais equipes não são tão importantes na primeira aproximação.

Obviamente, antes de traduzir o script, você deve transcodificá-lo para algo mais conveniente, por exemplo, em UTF-8, para combinar caracteres cirílicos e japoneses.

Depois de mudar o mecanismo (sobre a próxima parte), o jogo percebe o texto em russo e o japonês. Mas, por enquanto, para compatibilidade, você precisa codificar caracteres japoneses em Shift Jis e caracteres cirílicos na codificação cp1251.

Esboçamos rapidamente um programa em Python para transcodificação, levando em consideração o alfabeto cirílico:

UTF8 para cp1251 e 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.") 


No entanto, houve alguns problemas. O programa, ao tentar recodificar o símbolo “til” U (U + FF5E LARGURA TOTAL), gerou um erro “UnicodeEncodeError: o codec 'Shift Jis' não pode codificar o caractere '\ uff5e' na posição 0: sequência ilegal de multibytes”

No começo, pequei no Python, mas no final descobri uma nuance bastante incomum. Há uma ambiguidade entre os métodos de correlação das codificações em japonês Unicode e não Unicode, dependendo da implementação específica.

Como resultado, o Windows associa o caractere Shift Jis ao código 0x8160 ao unicode ~ (U + FF5E FULLWIDTH TILDE) e outros transcodificadores (por exemplo, utilitário iconv) correlacionam o mesmo caractere com 〜 (U + 301C WAVE DASH), de acordo com a tabela oficial de taxas Unicode - ftp://ftp.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/SHIFT JIS.TXT

Para determinar a correspondência entre os caracteres, a Microsoft aparentemente decidiu usar os esquemas da codificação cp932, que é uma versão estendida do Shift Jis.

A mesma situação ocorre com o código de caractere 0x817C, codificado em UTF8 como - (U + FF0D HYPHEN-MINUS) no Windows ou como - (U + 2212 MENOS SIGN) no iconv.

Como todos os arquivos de script foram convertidos pela primeira vez do Shift Jis para UTF8 usando o Notepad ++ (e ele usa a tabela de correspondência adotada no Windows), ao converter novamente de UTF8 para Shift Jis por meio do nosso programa Python, o notório erro de conversão apareceu.

Portanto, era necessário levar em consideração a ocorrência de ~ e - condições separadas.

Havia outras falhas menores - por exemplo, as reticências ... (U + 2026 ELLIPSIS HORIZONTAL) foram substituídas pelas reticências cirílicas da cp1251, e não as japonesas da Shift Jis.

Depois de traduzir o texto, você pode continuar trabalhando com gráficos de jogos.

Os arquivos gráficos do jogo estão nos mesmos arquivos, mas após descompactar, eles ainda precisam trabalhar duro. Por exemplo, quase todas as imagens png são descompactadas como arquivos do tipo sample + DPNG000 + x32y0.png Em outras palavras, as imagens png são cortadas em faixas horizontais com 88 cm de espessura e cada faixa é gravada em um arquivo separado. O nome do arquivo mostra o número de série da faixa (DPNG000 ... 009) e as coordenadas x, y.


Ainda estou me perguntando por que isso era necessário. Se pela dificuldade de extrair recursos do jogo, esse claramente não é o melhor método.

Para colar os arquivos png recortados, um pequeno script merge_dpng no Pearl da asmodeus, que usa o ImageMagick, foi criado ao mesmo tempo. Infelizmente, houve problemas com ele. Primeiro, eu precisava do Pearl, que não usava, e mesmo depois de instalá-lo, o script não estava funcionando corretamente.

Por esse motivo, escrevemos um programa semelhante em python:

Qlie engine dpng fusão de arquivos
 # -*- 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]) 


Parece que agora temos todo o conjunto de imagens que aparece no jogo? Nem um pouco - se você olhar para todas as fotos conectadas de todos os arquivos, ainda verá que algumas estão faltando, embora estejam no jogo. O fato é que existe outro tipo de arquivo no mecanismo - com a extensão .b. É um pouco de animação com imagens e sons gravados dentro.

É muito fácil obter os recursos armazenados, mas, infelizmente, nenhum dos descompactadores de arquivos .b já funcionou no nosso caso, como deveria. Alguns arquivos permaneceram descompactados ou houve erros devido a nomes em japonês, e eu não queria inicializar a partir do código do idioma japonês.

Aqui mais um script foi útil. Desde então, não estávamos familiarizados com algo como o Kaitai Struct , tivemos que agir quase do zero.

O formato dos arquivos .b acabou sendo simples e, além disso, nosso desempacotador foi obrigado a poder desempacotar recursos somente deste jogo. Em outros jogos no mecanismo Qlie, tipos adicionais de recursos apareceram nos arquivos .b, mas não iremos nos aprofundar neles em detalhes.

Portanto, abra qualquer arquivo .b em um editor hexadecimal e observe o início. Antes de avaliar, observe que a ordem dos bytes de todos os valores numéricos será Little-endian.

  • Cabeçalho do arquivo Abmp12
  • Dez bytes 0x00
  • O título da primeira seção abdata12 com informações gerais.
  • Oito bytes 0x00
  • Tamanho da seção abdata12, número inteiro de quatro bytes. Você pode ignorá-lo com segurança.
  • Cabeçalho da seção Abimage10
  • Sete bytes 0x00
  • Número de arquivos em uma seção, número inteiro de byte único. Nesse caso, há um arquivo na seção
  • Cabeçalho da seção abgimgdat13
  • Seis bytes 0x00
  • O comprimento do nome do arquivo dentro da seção, um número inteiro de dois bytes. Nesse caso, o comprimento é de 4 bytes.
  • Nome do arquivo codificado Shift Jis
  • Comprimento do registro da soma de verificação do arquivo, número inteiro de byte duplo.
  • A soma de verificação do próprio arquivo.
  • O byte desconhecido parece ser sempre 0x03 ou 0x02
  • Doze bytes desconhecidos, possivelmente relacionados à animação
  • O tamanho do arquivo png dentro da seção é um número inteiro de quatro bytes.

E, finalmente, o próprio arquivo png.


A seção absound é semelhante em estrutura à imagem.

Extrator 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'') 


O script deve descompactar automaticamente os arquivos png, jpg, bmp, ogg e wav encontrados. Mas além disso, arquivos imoavi desconhecidos também são encontrados dentro.

A conclusão é que, no jogo, todas as animações são feitas como um vídeo completo no formato ogv ou como imagens animadas por mecanismo que são gravadas em arquivos .b ou como sequências animadas de arquivos jpg no formato imoavi.

Nesse caso, também estávamos interessados ​​em imagens jpg, então tivemos que lidar com elas também.

Existem duas seções no imoavi: SOUND e MOVIE. Na seção FILME, 47 bytes após o cabeçalho, há quatro bytes do tamanho do arquivo jpg. Os arquivos são gravados um após o outro em sua forma original, separados por uma sequência de 19 bytes, onde o tamanho do próximo arquivo é gravado.

Os imoavi dublados no jogo não apareceram, então a seção SOUND está sempre vazia.

Bem, desde que começamos a extrair todos os recursos do jogo, ao mesmo tempo um pequeno script foi escrito para extrair jpg do imoavi.

Extrator de 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) 


Após descompactar, você pode garantir que a animação da tela inicial no menu seja armazenada apenas no arquivo 1_ no formato imoavi (arquivo 1_ タ イ ト 画面 画面 ム ー ビ .b).


Isso é tudo com recursos do jogo.

Infelizmente, o processo de tradução revelou várias nuances mais desagradáveis ​​que não puderam ser superadas. O jogo, como já escrevi, não suporta codificações Unicode. Portanto, todo o texto traduzido é exibido com o espaçamento incorreto das letras. Houve mais alguns problemas com arquivos de mochila e início de um jogo sem alterar a codificação do sistema para japonês.

Em algum momento, nós (ou melhor, o responsável pela parte técnica da tradução em nossa equipe) pensamos: talvez não devêssemos ficar com o mecanismo antigo, mas portar o romance para o mecanismo Renpy, ao mesmo tempo em que recebemos plataformas cruzadas?
Talvez estivéssemos com pressa, mas em algum momento foi uma pena abandonar o que começamos e não havia mais nada a fazer além de terminar a tradução.

O que encontramos durante a portabilidade?
Sobre isso na segunda parte.

Links:

Nossos scripts de bitbucket

Sobre a

tabela de codificação Shift Jis do mecanismo japonês Qlie

Leia mais sobre o problema da transcodificação de Shift Jis para UTF-8

Utilitário asmodean exfp3_v3

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


All Articles