utage: mission key generation and name finding

should be compatible with bingo's xdusuitecsharp
This commit is contained in:
louis 2019-05-15 05:17:37 -04:00
parent 71e8c9d257
commit 6c455fe30b
6 changed files with 266 additions and 52 deletions

View file

@ -1,11 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import os
import sys import sys
from os import path import utage
from adx import adx from adx import adx
from utage import utage
LANGUAGES = ["jpn", "eng", "rus"]
def main(): def main():
parser = argparse.ArgumentParser(description="Unpack and process XDU data") parser = argparse.ArgumentParser(description="Unpack and process XDU data")
@ -43,6 +45,20 @@ def main():
dest="hex", action="store_true", default=False) dest="hex", action="store_true", default=False)
parser_utage_crypt.add_argument("TARGET", parser_utage_crypt.add_argument("TARGET",
help="Input", type=str) help="Input", type=str)
# utage translate
parser_utage_translate = parser_utage_subparsers.add_parser("translate", help="Generate keyed tsv and json file")
parser_utage_translate.add_argument("INPUT",
help="Input", type=str)
parser_utage_translate.add_argument("TSVDIR", nargs='?',
help="_t.tsv output directory", type=str, default=".")
parser_utage_translate.add_argument("JSONDIR", nargs='?',
help="json output directory", type=str, default=".")
# utage names
parser_utage_names = parser_utage_subparsers.add_parser("names", help="Generate and update name files")
parser_utage_names.add_argument("INPUT",
help="Input", type=str)
parser_utage_names.add_argument("OLD", nargs='?',
help="Directory with old files", type=str, default="")
args = parser.parse_args() args = parser.parse_args()
@ -55,12 +71,24 @@ def main():
key = bytearray(args.key, "utf-8") key = bytearray(args.key, "utf-8")
else: else:
key = bytearray.fromhex(args.key) key = bytearray.fromhex(args.key)
if path.isfile(args.TARGET): if os.path.isfile(args.TARGET):
utage.crypt_file(args.TARGET, key, args.encrypt, args.ncomp) utage.crypt.crypt_file(args.TARGET, key, args.encrypt, args.ncomp)
elif path.isdir(args.TARGET): elif os.path.isdir(args.TARGET):
utage.crypt_dir(args.TARGET, key, args.encrypt, args.ncomp) utage.crypt.crypt_dir(args.TARGET, key, args.encrypt, args.ncomp)
else: else:
raise FileNotFoundError("Could not find {}".format(args.TARGET)) raise FileNotFoundError("Could not find {}".format(args.TARGET))
elif args.subcommand == "translate":
if os.path.isfile(args.INPUT):
utage.translate.translate_file(args.INPUT, args.TSVDIR, args.JSONDIR)
elif os.path.isdir(args.INPUT):
utage.translate.translate_dir(args.INPUT, args.TSVDIR, args.JSONDIR)
else:
raise FileNotFoundError("Could not find {}".format(args.INPUT))
elif args.subcommand == "names":
if os.path.isdir(args.INPUT):
utage.names.update_names(args.INPUT, args.OLD, LANGUAGES)
else:
raise FileNotFoundError(args.INPUT)
return 0 return 0

3
utage/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from . import crypt
from . import names
from . import translate

View file

@ -1,3 +1,51 @@
import io
import os
import traceback
from functools import partial
from glob import glob
from multiprocessing.pool import Pool
from . import crypt
from . import translate
def crypt_dir(dir_in, key, encrypt=False, no_compress=False):
if not encrypt:
files = glob(os.path.join(dir_in, "**/*.utage"), recursive=True)
else:
files = glob(os.path.join(dir_in, "**/*.tsv"), recursive=True)
if len(files) == 0:
raise FileNotFoundError("No valid files found in directory: " + dir_in)
pcrypt = partial(crypt_file, key=key, encrypt=encrypt, no_compress=no_compress)
with Pool() as p:
p.map(pcrypt, files)
def crypt_file(file_in, key, encrypt=False, no_compress=False):
with open(file_in, "rb") as inf:
in_data = inf.read()
try:
if encrypt:
if not file_in.endswith(".png") and not file_in.endswith(".jpg") and not file_in.endswith(".tsv"):
raise ValueError("Invalid File Type for {}".format(file_in))
if not file_in.endswith(".png") and not no_compress:
enc_data = crypt.compress(in_data)
enc_data = crypt.xor_crypt(enc_data, key)
with io.open(file_in + ".utage", "wb") as output:
output.write(enc_data)
else:
if not file_in.endswith(".utage"):
raise ValueError("Invalid File Type for {}".format(file_in))
dec_data = crypt.xor_crypt(in_data, key)
if not file_in.endswith(".png.utage") and not file_in.endswith(".jpg.utage") and not no_compress:
dec_data = crypt.decompress(dec_data)
with io.open(file_in.replace(".utage", ""), "wb") as output:
output.write(dec_data)
except Exception as e:
traceback.print_exc()
print("Error processing file: " + file_in)
def decompress(data): def decompress(data):
osize = 0 osize = 0
isize = len(data) isize = len(data)

83
utage/names.py Normal file
View file

@ -0,0 +1,83 @@
import csv
import json
import io
import os
import re
from functools import partial
from glob import glob
from multiprocessing import Pool
def update_names(dir_in, old_path, languages):
names = extract_names(dir_in)
# we have to update japanese like the rest because CustomData exists
# and POKELABO deleted the wedding gear event stuff from the game files
for x in languages:
langfile = os.path.join(old_path, "nametranslations_{}.json".format(x))
if os.path.isfile(langfile):
with open(langfile, "r") as lang_file:
lang_dict = json.load(lang_file)
if x == "jpn":
out_dict = dict(zip(names, names)).update(lang_dict)
else:
out_dict = dict.fromkeys(names, "").update(lang_dict)
elif x == "jpn": # bootstrap
out_dict = dict(zip(names, names))
else:
out_dict = dict.fromkeys(names, "")
with io.open(langfile, "w", newline='\n') as lang_file: # you're using git right
json.dump(lang_dict, lang_file, ensure_ascii=False, indent='\t', sort_keys=True)
def extract_names(dir_in):
char_tsv_path = os.path.join(dir_in, "Diva", "Settings", "Character.tsv")
if not os.path.isfile(char_tsv_path):
raise FileNotFoundError("Character.tsv not found")
names, sets = read_char_tsv(char_tsv_path)
files = [f for f in glob(os.path.join(dir_in, "**/*.tsv"), recursive=True) if re.search(r'/[0-9]{9}\.tsv$', f)]
if len(files) == 0:
raise FileNotFoundError("No valid files found in directory: " + dir_in)
# tfw gil
# it's still faster on my machine so i'm keeping it
read_partial = partial(read_mission, char_names=names, char_sets=sets)
with Pool() as p:
new_names = p.map(read_partial, files)
for x in new_names:
names.update(x)
return names
def read_char_tsv(char_tsv_path):
char_names = set()
char_sets = set()
with open(char_tsv_path, "r") as char_tsv_file:
char_tsv = csv.DictReader(char_tsv_file, delimiter="\t", quotechar="\"")
for row in char_tsv:
if row['CharacterName'].startswith("//"):
continue
if row['CharacterName'] and row['NameText'] and row['CharacterName'].strip() and row['NameText'].strip():
char_names.add(row['NameText'])
char_sets.add(row['CharacterName'])
return char_names, char_sets
def read_mission(tsv_path, char_names, char_sets):
new_names = set()
with open(tsv_path, "r") as tsv_file:
tsv = csv.DictReader(tsv_file, delimiter="\t", quotechar="\"")
if ('Arg1' not in tsv.fieldnames) or ('Text' not in tsv.fieldnames) or ('Command' not in tsv.fieldnames):
return new_names
for row in tsv:
if row['Command'] and row['Command'].startswith("//"):
continue
if row['Text'] and row['Arg1']:
if (row['Arg1'] not in char_names) and (row['Arg1'] not in char_sets):
new_names.add(row['Arg1'])
return new_names

98
utage/translate.py Normal file
View file

@ -0,0 +1,98 @@
import csv
import errno
import io
import json
import os
import re
import traceback
from functools import partial
from glob import glob
from multiprocessing.pool import Pool
def translate_dir(dir_in, tsv_out_dir, json_out_dir):
# we only want to key files that are mission ids
files = [f for f in glob(os.path.join(dir_in, "**/*.tsv"), recursive=True) if re.search(r'/[0-9]{9}\.tsv$', f)]
if len(files) == 0:
raise FileNotFoundError("No valid files found in directory: " + dir_in)
ptrans = partial(translate_file, tsv_out_dir=tsv_out_dir, json_out_dir=json_out_dir)
with Pool() as p:
p.map(ptrans, files)
def translate_file(file_in, tsv_out_dir, json_out_dir):
try:
if not file_in.endswith(".tsv"):
raise ValueError("Invalid File Type for {}".format(file_in))
# we need to get the event folder
# ie, Utage/>>>main01<<</Scenario/whatever.tsv
try:
event_folder = os.path.normpath(file_in).split(os.sep)[-3]
except Exception as e:
traceback.print_exc()
print("Error processing tsv: Please reference from Utage root")
raise
id_num = os.path.splitext(os.path.basename(file_in))[0]
tsv_output_path = os.path.join(tsv_out_dir, event_folder, "Scenario", "{}_t.tsv".format(id_num))
json_output_path = os.path.join(json_out_dir, event_folder, "{}_translations_jpn.json".format(id_num))
# need to create output paths and avoid races when threading
if not os.path.exists(os.path.dirname(tsv_output_path)):
try:
os.makedirs(os.path.dirname(tsv_output_path))
except OSError as e:
if e.errno != errno.EEXIST:
raise
if not os.path.exists(os.path.dirname(json_output_path)):
try:
os.makedirs(os.path.dirname(json_output_path))
except OSError as e:
if e.errno != errno.EEXIST:
raise
with open(file_in, "r") as tsv_file:
tsv = csv.DictReader(tsv_file, delimiter="\t", quotechar="\"")
tsv_keyed, json_str = process_tsv(tsv, id_num)
# csv handles newlines, don't set it in io.open
with io.open(tsv_output_path, "w", newline='') as tsv_out:
writer = csv.DictWriter(tsv_out, delimiter="\t", quotechar="\"", lineterminator="\n", fieldnames=tsv.fieldnames, extrasaction='ignore')
writer.writeheader()
for row in tsv_keyed:
writer.writerow(row)
with io.open(json_output_path, "w", newline='\n') as json_out:
# we don't want to sort these because they're _1, ..., _10, etc
json.dump(json_str, json_out, ensure_ascii=False, indent='\t', sort_keys=False)
except Exception as e:
traceback.print_exc()
print("Error processing file: " + file_in)
def process_tsv(tsv, id_num):
i = 0
tsv_keyed = []
key_dict = {}
for row in tsv:
try:
if row['Command'].startswith("//"):
tsv_keyed.append(row)
continue
if row['Text'] and row['Text'].strip():
key = "{}_{}".format(id_num, i)
i += 1
row['English'] = key
key_dict[key] = row['Text']
tsv_keyed.append(row)
except Exception as e:
traceback.print_exc()
print("Error processing tsv: " + id_num)
raise
return tsv_keyed, key_dict

View file

@ -1,46 +0,0 @@
import glob
import io
import traceback
from functools import partial
from multiprocessing.pool import Pool
from os import path
from . import crypt
def crypt_dir(dir_in, key, encrypt=False, no_compress=False):
if not encrypt:
files = glob.glob(path.join(dir_in, "**/*.utage"), recursive=True)
else:
files = glob.glob(path.join(dir_in, "**/*.tsv"), recursive=True)
if len(files) == 0:
raise FileNotFoundError("No valid files found in directory: " + dir_in)
pcrypt = partial(crypt_file, key=key, encrypt=encrypt, no_compress=no_compress)
with Pool() as p:
p.map(pcrypt, files)
def crypt_file(file_in, key, encrypt=False, no_compress=False):
with open(file_in, "rb") as inf:
in_data = inf.read()
try:
if encrypt:
if not file_in.endswith(".png") and not file_in.endswith(".tsv"):
raise ValueError("Invalid File Type for {}".format(file_in))
if not file_in.endswith(".png") and not no_compress:
enc_data = crypt.compress(in_data)
enc_data = crypt.xor_crypt(enc_data, key)
with io.open(file_in + ".utage", "wb") as output:
output.write(enc_data)
else:
if not file_in.endswith(".utage"):
raise ValueError("Invalid File Type for {}".format(file_in))
dec_data = crypt.xor_crypt(in_data, key)
if not file_in.endswith(".png.utage") and not no_compress:
dec_data = crypt.decompress(dec_data)
with io.open(file_in.replace(".utage", ""), "wb") as output:
output.write(dec_data)
except Exception as e:
traceback.print_exc()
print("Error processing file: " + file_in)