├── .gitignore ├── Asset_Download.py ├── Asset_Extract.py ├── Enemy_Parser.py ├── LICENSE ├── Process_DL_Data.py ├── Process_DL_Images.py ├── README.md ├── Wyrmprint_Alpha.png └── duplicate_page.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*/* 2 | *.lnk 3 | *.txt -------------------------------------------------------------------------------- /Asset_Download.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import errno 4 | import aiohttp 5 | import asyncio 6 | import argparse 7 | 8 | def merge_path_dir(path): 9 | new_dir = os.path.dirname(path).replace('/', '_') 10 | return new_dir + '/' + os.path.basename(path) 11 | 12 | def check_target_path(target): 13 | if not os.path.exists(os.path.dirname(target)): 14 | try: 15 | os.makedirs(os.path.dirname(target)) 16 | except OSError as exc: # Guard against race condition 17 | if exc.errno != errno.EEXIST: 18 | raise 19 | 20 | async def download(session, source, target): 21 | if os.path.exists(target): 22 | return 23 | print('Download', source, target) 24 | async with session.get(source) as resp: 25 | assert resp.status == 200 26 | check_target_path(target) 27 | with open(target, 'wb') as f: 28 | f.write(await resp.read()) 29 | 30 | def read_manifest_by_filter_str(manifest, filter_str): 31 | manifest_set = set() 32 | with open(manifest, 'r') as m: 33 | for l in m: 34 | sp = l.split('|') 35 | if not filter_str or filter_str in sp[1]: 36 | # yield sp[0].strip(), 'output/'+merge_path_dir(sp[1].strip()) 37 | # yield sp[0].strip(), './'+sp[1].strip() 38 | mtuple = sp[0].strip(), 'download/'+merge_path_dir(sp[1].strip()) 39 | manifest_set.add(mtuple) 40 | return manifest_set 41 | 42 | def read_manifest_by_file_list(manifest, file_list): 43 | manifest_set = set() 44 | with open(manifest, 'r') as m: 45 | for l in m: 46 | sp = l.split('|') 47 | label = sp[1].strip() 48 | if label in file_list: 49 | mtuple = sp[0].strip(), 'download/'+merge_path_dir(label) 50 | manifest_set.add(mtuple) 51 | file_list.remove(label) 52 | if len(file_list) == 0: 53 | break 54 | return manifest_set 55 | 56 | async def main(manifest, filter_str, old_manifest=None, file_list=[]): 57 | if file_list: 58 | manifest_set = read_manifest_by_file_list(manifest, file_list) 59 | else: 60 | manifest_set = read_manifest_by_filter_str(manifest, filter_str) 61 | if old_manifest is not None: 62 | manifest_old = read_manifest_by_filter_str(old_manifest, filter_str) 63 | manifest_set = manifest_set - manifest_old 64 | async with aiohttp.ClientSession() as session: 65 | await asyncio.gather(*[ 66 | download(session, source, target) 67 | for source, target in manifest_set 68 | ]) 69 | 70 | if __name__ == '__main__': 71 | parser = argparse.ArgumentParser(description='Download asset files from manifest.') 72 | parser.add_argument('-m', type=str, help='manifest file', required=True) 73 | parser.add_argument('-l', type=str, help='list of files, exact names', default=[], action='extend', nargs='+') 74 | parser.add_argument('-f', type=str, help='filter string', default=None) 75 | parser.add_argument('-o', type=str, help='older manifest', default=None) 76 | args = parser.parse_args() 77 | 78 | loop = asyncio.get_event_loop() 79 | loop.run_until_complete(main(args.m, args.f, args.o, [l.strip() for l in args.l])) -------------------------------------------------------------------------------- /Asset_Extract.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import errno 4 | import json 5 | from UnityPy import AssetsManager 6 | 7 | def check_target_path(target): 8 | if not os.path.exists(os.path.dirname(target)): 9 | try: 10 | os.makedirs(os.path.dirname(target)) 11 | except OSError as exc: # Guard against race condition 12 | if exc.errno != errno.EEXIST: 13 | raise 14 | 15 | def write_mono(f, data): 16 | f.write(data.dump()) 17 | 18 | do_filter = False 19 | def filter_dict(d): 20 | if do_filter: 21 | return dict(filter(lambda elem: elem[1] != 0, d.items())) 22 | else: 23 | return d 24 | 25 | def process_json(tree): 26 | while isinstance(tree, dict): 27 | if 'dict' in tree: 28 | tree = tree['dict'] 29 | elif 'list' in tree: 30 | tree = tree['list'] 31 | elif 'entriesValue' in tree and 'entriesHashCode' in tree: 32 | return {k: process_json(v) for k, v in zip(tree['entriesHashCode'], tree['entriesValue'])} 33 | else: 34 | return tree 35 | return tree 36 | 37 | def write_json(f, data): 38 | tree = data.read_type_tree() 39 | json.dump(process_json(tree), f, indent=2) 40 | 41 | write = write_json 42 | mono_ext = '.json' 43 | 44 | def unpack_Texture2D(data, dest): 45 | print('Texture2D', dest, flush=True) 46 | dest, _ = os.path.splitext(dest) 47 | dest = dest + '.png' 48 | check_target_path(dest) 49 | 50 | img = data.image 51 | img.save(dest) 52 | 53 | def unpack_MonoBehaviour(data, dest): 54 | print('MonoBehaviour', dest, flush=True) 55 | dest, _ = os.path.splitext(dest) 56 | dest = dest + mono_ext 57 | check_target_path(dest) 58 | 59 | with open(dest, 'w', encoding='utf8', newline='') as f: 60 | write(f, data) 61 | 62 | def unpack_GameObject(data, destination_folder): 63 | dest = os.path.join(destination_folder, os.path.splitext(data.name)[0]) 64 | print('GameObject', dest, flush=True) 65 | mono_list = [] 66 | for idx, obj in enumerate(data.components): 67 | obj_type_str = str(obj.type) 68 | if obj_type_str in unpack_dict: 69 | subdata = obj.read() 70 | if obj_type_str == 'MonoBehaviour': 71 | if mono_ext == '.json': 72 | json_data = subdata.read_type_tree() 73 | if json_data: 74 | mono_list.append(json_data) 75 | else: 76 | mono_list.append(data.dump()) 77 | elif obj_type_str == 'GameObject': 78 | unpack_dict[obj_type_str](subdata, os.path.join(dest, '{:02}'.format(idx))) 79 | if len(mono_list) > 0: 80 | dest += mono_ext 81 | check_target_path(dest) 82 | with open(dest, 'w', encoding='utf8', newline='') as f: 83 | if mono_ext == '.json': 84 | json.dump(mono_list, f, indent=2) 85 | else: 86 | for m in mono_list: 87 | f.write(m) 88 | f.write('\n') 89 | 90 | unpack_dict = { 91 | 'Texture2D': unpack_Texture2D, 92 | 'MonoBehaviour': unpack_MonoBehaviour, 93 | 'GameObject': unpack_GameObject, 94 | 'AnimationClip': unpack_MonoBehaviour, 95 | 'AnimatorOverrideController': unpack_MonoBehaviour 96 | } 97 | 98 | def unpack_asset(file_path, destination_folder, root=None, source_folder=None): 99 | # load that file via AssetsManager 100 | am = AssetsManager(file_path) 101 | 102 | # iterate over all assets and named objects 103 | for asset in am.assets.values(): 104 | for obj in asset.objects.values(): 105 | # only process specific object types 106 | # print(obj.type, obj.container) 107 | obj_type_str = str(obj.type) 108 | if obj_type_str in unpack_dict: 109 | # parse the object data 110 | data = obj.read() 111 | 112 | # create destination path 113 | if root and source_folder: 114 | intermediate = root.replace(source_folder, '') 115 | if len(intermediate) > 0 and intermediate[0] == '\\': 116 | intermediate = intermediate[1:] 117 | else: 118 | intermediate = '' 119 | if obj_type_str == 'GameObject': 120 | dest = os.path.join(destination_folder, intermediate) 121 | unpack_dict[obj_type_str](data, dest) 122 | elif data.name: 123 | dest = os.path.join(destination_folder, intermediate, data.name) 124 | unpack_dict[obj_type_str](data, dest) 125 | 126 | 127 | def unpack_all_assets(source_folder, destination_folder): 128 | # iterate over all files in source folder 129 | for root, _, files in os.walk(source_folder): 130 | for file_name in files: 131 | # generate file_path 132 | file_path = os.path.join(root, file_name) 133 | unpack_asset(file_path, destination_folder, root=root, source_folder=source_folder) 134 | 135 | 136 | if __name__ == '__main__': 137 | parser = argparse.ArgumentParser(description='Extract asset files.') 138 | parser.add_argument('-i', type=str, help='input dir', default='./download') 139 | parser.add_argument('-o', type=str, help='output dir', default='./extract') 140 | parser.add_argument('-mode', type=str, help='export format, default json, can also use mono', default='json') 141 | args = parser.parse_args() 142 | if args.mode == 'mono': 143 | write = write_mono 144 | mono_ext = '.mono' 145 | if os.path.isdir(args.i): 146 | unpack_all_assets(args.i, args.o) 147 | else: 148 | unpack_asset(args.i, args.o) -------------------------------------------------------------------------------- /Enemy_Parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from collections import OrderedDict, defaultdict 4 | import argparse 5 | import csv 6 | import os 7 | import re 8 | 9 | QUEST_NAME_REGEX = { 10 | re.compile(r'TUTORIAL_'): (lambda: 'Prologue'), 11 | re.compile(r'MAIN_(\d+)_(\d+)_E_'): 12 | (lambda x, y: 'Chapter {}: {}'.format(int(x), MAIN_DIFFICULTY.get(y, y))), 13 | re.compile(r'WALL_\d+_(\d+)_(\d+)_E_'): 14 | (lambda x, y: 'The Mercurial Gauntlet ({}): Lv. {}'.format( 15 | ELEMENTAL_TYPES.get(x.lstrip('0'), x), int(y))), 16 | re.compile(r'RARE_\d+_(\d+)_E(\d+)'): 17 | (lambda x, y: get_rare_enemy_quest_name(x,y)), 18 | 19 | # Simple TextLabel lookup. 20 | re.compile(r'EXP_\d+_(\d+)_E'): 21 | (lambda x: get_label('QUEST_NAME_2010101{}'.format(x))), 22 | re.compile(r'WEEKLY_(\d+)_(\d+)_E'): 23 | (lambda x, y: get_label('QUEST_NAME_202{}01{}'.format(x, y))), 24 | re.compile(r'DRAGONBATTLE_(\d+)_(\d+)_E'): 25 | (lambda x, y: get_label('QUEST_NAME_203{}01{}'.format(x, y))), 26 | re.compile(r'DRAGONBATTLE_ULT_(\d+)_0(\d)_E'): 27 | (lambda x, y: get_label('QUEST_NAME_210{}010{}'.format( 28 | x, QUEST_DIFFICULTY_OVERRIDES['DRAGONBATTLE_ULT'].get(y, y))) + ' (Co-op)'), 29 | re.compile(r'DRAGONBATTLE_ULT_(\d+)_1(\d)_E'): 30 | (lambda x, y: get_label('QUEST_NAME_210{}010{}'.format( 31 | x, QUEST_DIFFICULTY_OVERRIDES['DRAGONBATTLE_ULT'].get(y, y))) + ' (Solo)'), 32 | re.compile(r'EMPIRE_(\d+)_(\d+)_E'): 33 | (lambda x, y: get_label('QUEST_NAME_211{}01{}'.format(x, y))), 34 | re.compile(r'ASTRAL_(\d+)_(\d+)_E'): 35 | (lambda x, y: get_label('QUEST_NAME_217{}01{}'.format(x, y))), 36 | re.compile(r'AGITO_(\d+)_0(\d)_E'): 37 | (lambda x, y: get_label('QUEST_NAME_219{}010{}'.format(x, y)) + ' (Co-op)'), 38 | re.compile(r'AGITO_(\d+)_1(\d)_E'): 39 | (lambda x, y: get_label('QUEST_NAME_219{}010{}'.format(x, y)) + ' (Solo)'), 40 | re.compile(r'AGITO_ABS_(\d+)_04_E'): 41 | (lambda x: get_label('QUEST_NAME_225{}0101'.format(x)) + ' (Co-op)'), 42 | re.compile(r'AGITO_ABS_(\d+)_14_E'): 43 | (lambda x: get_label('QUEST_NAME_225{}0101'.format(x)) + ' (Solo)'), 44 | re.compile(r'VOIDBATTLE_(\d+)_0(\d)_E'): 45 | (lambda x, y: get_label('QUEST_NAME_300{}010{}'.format( 46 | QUEST_NAME_OVERRIDES['VOIDBATTLE'].get(x, x), y)) + ' (Co-op)'), 47 | re.compile(r'VOIDBATTLE_(\d+)_1(\d)_E'): 48 | (lambda x, y: get_label('QUEST_NAME_300{}010{}'.format( 49 | QUEST_NAME_OVERRIDES['VOIDBATTLE'].get(x, x), y)) + ' (Solo)'), 50 | # SinDom 51 | re.compile(r'DIABOLOS_(\d+)_0(\d)_E'): 52 | (lambda x, y: get_label('QUEST_NAME_228{}010{}'.format(x, y)) + ' (Co-op)'), 53 | re.compile(r'DIABOLOS_(\d+)_1(\d)_E'): 54 | (lambda x, y: get_label('QUEST_NAME_228{}010{}'.format(x, y)) + ' (Solo)'), 55 | # SinDom Legend 56 | re.compile(r'DIABOLOS_ABS_(\d+)_04_E'): 57 | (lambda x: get_label('QUEST_NAME_232{}0101'.format(x)) + ' (Co-op)'), 58 | re.compile(r'DIABOLOS_ABS_(\d+)_14_E'): 59 | (lambda x: get_label('QUEST_NAME_232{}0101'.format(x)) + ' (Solo)'), 60 | # Trials of the Mighty 61 | re.compile(r'SUBDUE_(\d+)_0(\d)_E'): 62 | (lambda x, y: get_label('QUEST_NAME_32{}010{}'.format(x, y)) + ' (Co-op)'), 63 | re.compile(r'SUBDUE_(\d+)_1(\d)_E'): 64 | (lambda x, y: get_label('QUEST_NAME_32{}010{}'.format(x, y)) + ' (Solo)'), 65 | 66 | # Phraeganoth 67 | re.compile(r'RAID_01_0([56])_E_(\d)\d'): (lambda x, y: get_label(f'QUEST_NAME_204200{x}0{int(y)+1}')), 68 | 69 | # Timeworn Torment 70 | re.compile(r'RAID_10_01_E_0\d'): (lambda: get_label(f'QUEST_NAME_204100301')), 71 | re.compile(r'RAID_10_03_E_0\d'): (lambda: get_label(f'QUEST_NAME_204100302')), 72 | re.compile(r'RAID_10_04_E_0\d'): (lambda: get_label(f'QUEST_NAME_204100401')), 73 | re.compile(r'RAID_10_05_E_0\d'): (lambda: get_label(f'QUEST_NAME_204100501')), 74 | re.compile(r'RAID_10_06_E_0\d'): (lambda: get_label(f'QUEST_NAME_204100601')), 75 | re.compile(r'RAID_10_06_E_1\d'): (lambda: get_label(f'QUEST_NAME_204100602')), 76 | re.compile(r'RAID_10_06_E_2\d'): (lambda: get_label(f'QUEST_NAME_204100603')), 77 | re.compile(r'RAID_10_01_E_18'): (lambda: get_label(f'QUEST_NAME_204100201')), 78 | re.compile(r'RAID_10_03_E_11'): (lambda: get_label(f'QUEST_NAME_204100202')), 79 | re.compile(r'RAID_10_01_E_1[1-3]'): (lambda: get_label(f'QUEST_NAME_204100101')), 80 | re.compile(r'RAID_10_01_E_1[4-7]'): (lambda: get_label(f'QUEST_NAME_204100102')), 81 | 82 | # Doomsday Getaway 83 | re.compile(r'RAID_09_01_E_0\d'): (lambda: get_label(f'QUEST_NAME_204090301')), # Scylla Clash: Beginner 84 | re.compile(r'RAID_09_03_E_0\d'): (lambda: get_label(f'QUEST_NAME_204090302')), # Scylla Clash: Expert 85 | re.compile(r'RAID_09_04_E_0\d'): (lambda: get_label(f'QUEST_NAME_204090401')), # Scylla Clash EX 86 | re.compile(r'RAID_09_05_E_0\d'): (lambda: get_label(f'QUEST_NAME_204090501')), # Scylla Clash: Nightmare 87 | re.compile(r'RAID_09_06_E_0\d'): (lambda: get_label(f'QUEST_NAME_204090601')), # Scylla Clash: Omega (Solo) 88 | re.compile(r'RAID_09_06_E_1\d'): (lambda: get_label(f'QUEST_NAME_204090602')), # Scylla Clash: Omega Level 1 (Raid) 89 | re.compile(r'RAID_09_06_E_2\d'): (lambda: get_label(f'QUEST_NAME_204090603')), # Scylla Clash: Omega Level 2 (Raid) 90 | re.compile(r'RAID_09_01_E_11'): (lambda: get_label(f'QUEST_NAME_204090201')), # Assault on Admiral Pincers: Beginner 91 | re.compile(r'RAID_09_03_E_11'): (lambda: get_label(f'QUEST_NAME_204090202')), # Assault on Admiral Pincers: Expert 92 | 93 | # Rhythmic Resolution 94 | re.compile(r'BUILD_23_01_E_0\d'): (lambda: get_label(f'QUEST_NAME_208230101')), 95 | re.compile(r'BUILD_23_02_E_0\d'): (lambda: get_label(f'QUEST_NAME_208230102')), 96 | re.compile(r'BUILD_23_03_E_(\d)\d'): (lambda x: get_label(f'QUEST_NAME_20823030{int(x)+1}')), 97 | re.compile(r'BUILD_23_04_E_\d\d'): (lambda: get_label(f'QUEST_NAME_208230401')), 98 | re.compile(r'BUILD_23_05_E_[01]\d'): (lambda: get_label(f'QUEST_NAME_208230501')), 99 | re.compile(r'BUILD_23_05_E_[23]\d'): (lambda: get_label(f'QUEST_NAME_208230502')), 100 | re.compile(r'BUILD_23_05_E_[45]\d'): (lambda: get_label(f'QUEST_NAME_208230601')), 101 | 102 | # FEH collab 103 | re.compile(r'CLB_01_01_1([1-3])_E_\d\d'): (lambda x: get_label('QUEST_NAME_21401030{}'.format(x))), 104 | re.compile(r'CLB_01_03_0([1-3])_E_\d\d'): (lambda x: get_label('QUEST_NAME_21403060{}'.format(x))), 105 | re.compile(r'CLB_01_03_(08|09|10|23)_E_\d\d'): (lambda x: get_label(f'QUEST_NAME_21403110{int(x)-7 if x != "23" else 4}')), 106 | re.compile(r'CLB_01_03_(11|12|13|24)_E_\d\d'): (lambda x: get_label(f'QUEST_NAME_21403120{int(x)-10 if x != "24" else 4}')), 107 | re.compile(r'CLB_01_03_(14|15|16|25)_E_\d\d'): (lambda x: get_label(f'QUEST_NAME_21403130{int(x)-13 if x != "25" else 4}')), 108 | re.compile(r'CLB_01_03_(17|18|19|26)_E_\d\d'): (lambda x: get_label(f'QUEST_NAME_21403140{int(x)-16 if x != "26" else 4}')), 109 | re.compile(r'CLB_01_03_(20|21|22|27)_E_\d\d'): (lambda x: get_label(f'QUEST_NAME_21403150{int(x)-19 if x != "27" else 4}')), 110 | } 111 | QUEST_NAME_OVERRIDES = { 112 | 'VOIDBATTLE': { 113 | '10': '11', 114 | '11': '10', 115 | }, 116 | } 117 | QUEST_DIFFICULTY_OVERRIDES = { 118 | 'DRAGONBATTLE_ULT': { 119 | '1': '2', 120 | '4': '1', 121 | '5': '3', 122 | '6': '4', 123 | }, 124 | } 125 | MAIN_DIFFICULTY = { 126 | '01': 'Normal', 127 | '02': 'Hard', 128 | '03': 'Very Hard', 129 | } 130 | RARE_ENEMY_QUESTS = { 131 | '01': 'Light Campaign / Avenue / Ruins Quests: Level ', 132 | '02': 'Water Campaign / Ruins Quests: Level ', 133 | '03': 'Wind Campaign / Ruins Quests: Level ', 134 | '04': 'Shadow Campaign / Avenue / Ruins Quests: Level ', 135 | '05': 'Flame Campaign / Ruins Quests: Level ', 136 | } 137 | TRIBES = { 138 | '0': 'None', 139 | '1': 'Thaumian', 140 | '2': 'Physian', 141 | '3': 'Demihuman', 142 | '4': 'Therion', 143 | '5': 'Undead', 144 | '6': 'Demon', 145 | '7': 'Human', 146 | '8': 'Dragon', 147 | } 148 | ELEMENTAL_TYPES = { 149 | '1': 'Flame', 150 | '2': 'Water', 151 | '3': 'Wind', 152 | '4': 'Light', 153 | '5': 'Shadow', 154 | '99': '', # None 155 | } 156 | 157 | class Enemy: 158 | def __init__(self, enemy_param, enemy_data, enemy_list, weapon_data): 159 | ep = enemy_param 160 | ed = enemy_data[ep['_DataId']] 161 | el = enemy_list[ed['_BookId']] 162 | 163 | data = OrderedDict() 164 | data['Id'] = ep['_Id'] 165 | data['DataId'] = ep['_DataId'] 166 | data['Name'] = get_label(el['_Name']) 167 | data['ModelId'] = get_model_name(ed) 168 | data['RareStayTime'] = int(ep['_RareStayTime']) 169 | data['HP'] = int(ep['_HP']) 170 | data['Atk'] = int(ep['_Atk']) 171 | data['Def'] = int(ep['_Def']) 172 | data['Overwhelm'] = int(ep['_Overwhelm']) 173 | data['BaseOD'] = int(ep['_BaseOD']) 174 | data['BaseBreak'] = int(ep['_BaseBreak']) 175 | data['CounterRate'] = int(ep['_CounterRate']) 176 | data['BarrierRate'] = int(ep['_BarrierRate']) 177 | data['GetupActionRate'] = int(ep['_GetupActionRate']) 178 | # For the index order, match up against ActionCondition fields starting from 'RatePoison'. 179 | data['Poison'] = int(ep['_RegistAbnormalRate01']) 180 | data['Burn'] = int(ep['_RegistAbnormalRate02']) 181 | data['Freeze'] = int(ep['_RegistAbnormalRate03']) 182 | data['Paralysis'] = int(ep['_RegistAbnormalRate04']) 183 | data['Blind'] = int(ep['_RegistAbnormalRate05']) 184 | data['Stun'] = int(ep['_RegistAbnormalRate06']) 185 | data['Bog'] = int(ep['_RegistAbnormalRate07']) 186 | data['Sleep'] = int(ep['_RegistAbnormalRate08']) 187 | data['Curse'] = int(ep['_RegistAbnormalRate09']) 188 | data['Frostbite'] = int(ep['_RegistAbnormalRate10']) 189 | data['Flashburn'] = int(ep['_RegistAbnormalRate11']) 190 | data['Stormlash'] = int(ep['_RegistAbnormalRate12']) 191 | data['Shadowblight'] = int(ep['_RegistAbnormalRate13']) 192 | data['Scorchrend'] = int(ep['_RegistAbnormalRate14']) 193 | data['PartsA'] = ep['_PartsA'] 194 | data['PartsB'] = ep['_PartsB'] 195 | data['PartsC'] = ep['_PartsC'] 196 | data['PartsD'] = ep['_PartsD'] 197 | data['PartsNode'] = ep['_PartsNode'] 198 | # data['CrashedHPRate'] = ep['_CrashedHPRate'] # Currently unused 199 | data['ParamGroupName'] = ep['_ParamGroupName'] 200 | data['MissionType'] = get_enemy_quest_name(ep['_ParamGroupName']) 201 | data['MissionDifficulty'] = '' # Currently unused 202 | data['Tribe'] = TRIBES.get(el['_TribeType'], el['_TribeType']) 203 | data['Weapon'] = get_label(weapon_data.get(ed['_WeaponId'], '')) 204 | data['ElementalType'] = ELEMENTAL_TYPES.get(ed['_ElementalType']) 205 | data['BreakDuration'] = float(ed['_BreakDuration']) 206 | data['MoveSpeed'] = float(ed['_MoveSpeed']) 207 | data['TurnSpeed'] = float(ed['_TurnSpeed']) 208 | data['SuperArmor'] = float(ed['_SuperArmor']) 209 | data['BreakAtkRate'] = float(ed['_BreakAtkRate']) 210 | data['BreakDefRate'] = float(ed['_BreakDefRate']) 211 | data['ODAtkRate'] = float(ed['_ObAtkRate']) 212 | data['ODDefRate'] = float(ed['_ObDefRate']) 213 | data['Ability01'] = ep['_Ability01'] 214 | data['Ability02'] = ep['_Ability02'] 215 | data['Ability03'] = ep['_Ability03'] 216 | data['Ability04'] = ep['_Ability04'] 217 | self.data = data 218 | 219 | def summary(self): 220 | return ''.join([ 221 | 'Id: ', self.data['Id'], 222 | ', DataId: ', self.data['DataId'], 223 | ', ModelId: ', self.data['ModelId'], 224 | '\n\tName: ', self.data['Name'], 225 | '\n\tQuest: ', self.data['MissionType'], 226 | '\n\tTribe: ', self.data['Tribe'], 227 | '\n\tElement: ', self.data['ElementalType'], 228 | '\n\tHP: ', self.data['HP'], ', ATK: ', self.data['Atk'], 229 | ]) 230 | 231 | def __repr__(self): 232 | for item in self.data.items(): 233 | for i in item: 234 | if isinstance(i, dict): 235 | print(i) 236 | 237 | return ''.join([ 238 | '{{EnemyData|', 239 | '|'.join('='.join((k, str(v))) for k, v in self.data.items()), 240 | '}}', 241 | ]) 242 | 243 | def get_label(label, default=''): 244 | return TEXT_LABEL.get(label, default) or default 245 | 246 | def get_enemy_quest_name(group_name): 247 | for pattern in QUEST_NAME_REGEX: 248 | match = pattern.match(group_name) 249 | if match: 250 | name = QUEST_NAME_REGEX[pattern](*match.groups()) 251 | return name 252 | return '' 253 | 254 | def get_rare_enemy_quest_name(x, y): 255 | x = int(x) 256 | if x == 0: 257 | return 'Campaign / Avenue / Ruins Quests' 258 | else: 259 | return RARE_ENEMY_QUESTS[y] + str(x) 260 | 261 | # Categories whose 3D model resource IDs depend on their Base and Variation IDs 262 | BASE_VARIATION_PREFIXES = { 263 | '3': 'd', 264 | '5': 'c', 265 | } 266 | BOOK_GROUP_PREFIXES = { 267 | 'ENM': 'e', 268 | 'BOS': 'b', 269 | 'RID': 'r', 270 | 'HBS': 'h', 271 | 'EOJ': 'o', 272 | } 273 | def get_model_name(ed): 274 | category = ed['_Category'] 275 | group = ed['_EnemyGroupName'].split('_', 1)[0] 276 | if category == '0': 277 | return '' 278 | elif category in BASE_VARIATION_PREFIXES: 279 | return '{}{}_{}'.format(BASE_VARIATION_PREFIXES[category], 280 | ed['_BaseId'], ed['_VariationId'].zfill(2)) 281 | elif group in BOOK_GROUP_PREFIXES: 282 | return BOOK_GROUP_PREFIXES[group] + ed['_BookId'][-7:] 283 | else: 284 | return '' 285 | 286 | 287 | def csv_to_dict(path, index=None, value_key=None, tabs=False): 288 | with open(path, 'r', newline='', encoding='utf-8') as csvfile: 289 | if tabs: 290 | reader = csv.DictReader(csvfile, dialect='excel-tab') 291 | else: 292 | reader = csv.DictReader(csvfile) 293 | keys = reader.fieldnames 294 | 295 | if not index: 296 | index = keys[0] # get first key as index 297 | if not value_key and len(keys) == 2: 298 | # If not otherwise specified, load 2 column files as dict[string] = string 299 | value_key = keys[-1] # get second key 300 | if value_key: 301 | return {row[index]: row[value_key] for row in reader if row[index] != '0'} 302 | else: 303 | # load >2 column files as a dict[string] = OrderedDict 304 | return {row[index]: row for row in reader if row[index] != '0'} 305 | 306 | def parse(input_dir, output_dir='', text_label_dict=None): 307 | global TEXT_LABEL 308 | enemy_param = csv_to_dict(os.path.join(input_dir, 'EnemyParam.txt'), index='_Id') 309 | enemy_data = csv_to_dict(os.path.join(input_dir, 'EnemyData.txt'), index='_Id') 310 | enemy_list = csv_to_dict(os.path.join(input_dir, 'EnemyList.txt'), index='_Id') 311 | weapon_data = csv_to_dict(os.path.join(input_dir, 'WeaponData.txt'), index='_Id', value_key='_Name') 312 | if text_label_dict: 313 | TEXT_LABEL = text_label_dict 314 | else: 315 | TEXT_LABEL = csv_to_dict(os.path.join(input_dir, 'TextLabel.txt'), index='_Id', value_key='_Text', tabs=True) 316 | 317 | enemies_set = set() 318 | enemies_output = [] 319 | enemies_count = 0 320 | for enemy_param in enemy_param.values(): 321 | enemy = Enemy(enemy_param, enemy_data, enemy_list, weapon_data) 322 | 323 | if enemy_param['_ParamGroupName'].startswith('DEBUG'): 324 | # We think these were left in by accident, hide them from the output 325 | continue 326 | 327 | try: 328 | enemies_set.add(enemy.data['Name']) 329 | except Exception as e: 330 | print(e) 331 | print(enemy.data['Name']) 332 | 333 | enemies_output.append(enemy) 334 | enemies_count += 1 335 | 336 | os.makedirs(output_dir, exist_ok=True) 337 | os.makedirs(os.path.join(output_dir, 'EnemyData'), exist_ok=True) 338 | 339 | # Sort the enemies by Id 340 | enemies_output.sort(key=lambda x: x.data.get('Id', '')) 341 | 342 | # Break into subpages 343 | for i in range(0, enemies_count, 300): 344 | output_path = os.path.join(output_dir, 'EnemyData', f'{(i // 300) + 1}.txt') 345 | subset = enemies_output[i:i+300] 346 | with open(output_path, 'w', encoding='utf-8') as outfile: 347 | outfile.write('\n'.join((str(e) for e in subset))) 348 | 349 | # Lastly, print the summary 350 | with open(os.path.join(output_dir, 'EnemiesSummary.txt'), 'w', encoding='utf-8') as outfile: 351 | outfile.write('Total Enemy Entries: ' + str(enemies_count)) 352 | outfile.write('\nUnique Enemy Names: ' + str(len(enemies_set))) 353 | outfile.write('\nEnemy Names\n') 354 | outfile.write('===========\n') 355 | outfile.write('\n'.join(sorted(enemies_set))) 356 | 357 | 358 | if __name__ == "__main__": 359 | parser = argparse.ArgumentParser(description='Process Enemy CSV data into Wikitext.') 360 | parser.add_argument('-i', type=str, help='directory of input text files (default: ./)', default='./') 361 | parser.add_argument('-o', type=str, help='directory of output text files (default: ./EnemyData)', default='./EnemyData') 362 | 363 | args = parser.parse_args() 364 | input_dir = args.i 365 | output_dir = args.o 366 | parse(input_dir, output_dir) 367 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mushy mato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Process_DL_Data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import csv 6 | import glob 7 | import json 8 | import os 9 | import re 10 | import sqlite3 11 | import string 12 | import traceback 13 | 14 | from collections import OrderedDict, defaultdict 15 | from datetime import datetime, timedelta 16 | from shutil import copyfile, rmtree 17 | 18 | import pdb 19 | 20 | EXT = '.txt' 21 | DEFAULT_TEXT_LABEL = '' 22 | ENTRY_LINE_BREAK = '\n=============================\n' 23 | EDIT_THIS = '' 24 | 25 | ROW_INDEX = '_Id' 26 | EMBLEM_N = 'EMBLEM_NAME_' 27 | EMBLEM_P = 'EMBLEM_PHONETIC_' 28 | 29 | TEXT_LABEL = 'TextLabel' 30 | TEXT_LABEL_JP = 'TextLabelJP' 31 | TEXT_LABEL_TC = 'TextLabelTC' 32 | TEXT_LABEL_SC = 'TextLabelSC' 33 | TEXT_LABEL_DICT = {} 34 | 35 | CHAIN_COAB_SET = set() 36 | EPITHET_DATA_NAME = 'EmblemData' 37 | EPITHET_DATA_NAMES = None 38 | ITEM_NAMES = { 39 | 'AstralItem': {}, 40 | 'BuildEventItem': {}, 41 | 'CollectEventItem': {}, 42 | 'CombatEventItem': {}, 43 | 'Clb01EventItem': {}, 44 | 'EarnEventItem': {}, 45 | 'ExHunterEventItem': {}, 46 | 'ExRushEventItem': {}, 47 | 'GatherItem': {}, 48 | 'RaidEventItem': {}, 49 | 'SimpleEventItem': {}, 50 | } 51 | SKILL_DATA_NAME = 'SkillData' 52 | SKILL_DATA_NAMES = None 53 | 54 | ORDERING_DATA = {} 55 | 56 | ROMAN_NUMERALS = [None, 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'] 57 | ELEMENT_TYPE = { 58 | '0': '', 59 | '1': 'Flame', 60 | '2': 'Water', 61 | '3': 'Wind', 62 | '4': 'Light', 63 | '5': 'Shadow', 64 | '99': 'None' 65 | } 66 | CLASS_TYPE = [None, 'Attack', 'Defense', 'Support', 'Healing'] 67 | WEAPON_TYPE = [None, 'Sword', 'Blade', 'Dagger', 'Axe', 'Lance', 'Bow', 'Wand', 'Staff', 'Manacaster'] 68 | QUEST_TYPE_DICT = { 69 | 'Campaign': re.compile(r'MAIN_|PROLOGUE_'), 70 | 'Event': re.compile(r'AGITO_|ASTRAL_|DRAGONBATTLE_|WEEKLY_|EXP_|EMPIRE_|VOIDBATTLE_'), 71 | 'Facility': re.compile(r'BUILD_'), 72 | 'Raid': re.compile(r'RAID_'), 73 | 'Onslaught': re.compile(r'COMBAT_'), 74 | 'Defensive': re.compile(r'CLB_DEF_'), 75 | 'Collab': re.compile(r'CLB_\d|EX_'), 76 | 'Battle Royale': re.compile(r'BR_'), 77 | 'Simple': re.compile(r'SIMPLE_'), 78 | 'Invasion': re.compile(r'EARN_'), 79 | } 80 | CHAPTER_PARTS_PATTERN = re.compile(r'^Ch\. (\d+) / .*-(\d+)$') 81 | SKILL_DETAIL_PATTERN = re.compile(r'\[(\d+)\].*<\/size>\n') 82 | 83 | GROUP_TYPE_DICT = { 84 | '1' : 'Campaign', 85 | '2' : 'Event', 86 | } 87 | QUEST_MODE_PLAY_TYPE_DICT = { 88 | '1' : '', 89 | '2' : ' (Solo)', 90 | '3' : ' (Co-op)', 91 | '4' : ' (Quick Play Co-op)', 92 | } 93 | WEAPON_SERIES = { 94 | '1' : 'Core', 95 | '2' : 'Void', 96 | '3' : 'High Dragon', 97 | '4' : 'Agito', 98 | '5' : 'Chimeratech', 99 | '6' : 'Other', 100 | '7' : 'Primal Dragon', 101 | } 102 | 103 | FACILITY_EFFECT_TYPE_DICT = { 104 | '1': 'Adventurer', # weapon 105 | '2': 'Adventurer', # elemental 106 | '4': 'Dragon', # dracolith 107 | '6': 'Dragon' # fafnir 108 | } 109 | MANA_PIECE_DESC = { 110 | '10101': 'HP', 111 | '10102': 'Strength', 112 | '10103': 'HP/Str', 113 | '10201': 'Force Strike', 114 | '10301': 'Ability 1', 115 | '10302': 'Ability 2', 116 | '10303': 'Ability 3', 117 | '10401': 'Skill 1', 118 | '10402': 'Skill 2', 119 | '10501': 'Co-ability', 120 | '10601': 'Damascus Crystal', 121 | '10602': 'Damascus Crystal?', 122 | '10701': 'Standard Attack Level Increase', 123 | '10801': 'Max Level Increase' 124 | } 125 | DMODE_SERVITOR_TYPE = { # DmodeServitorType.cs 126 | '0': 'None', 127 | '1': 'Attack', 128 | '2': 'Defense', 129 | '3': 'Support', 130 | } 131 | DMODE_SERVITOR_PASSIVE_TYPE = { 132 | '0': 'None', 133 | '1': 'HP', 134 | '2': 'Strength', 135 | '3': 'Dawn Amber', 136 | '4': 'Dusk Amber', 137 | '5': 'EXP', 138 | '6': 'Skill Damage', 139 | '7': 'Critical Rate', 140 | '8': 'Critical Damage', 141 | '9': 'Force Strike', 142 | '10': 'Recovery Boost', 143 | '11': 'Thaumian Res', 144 | '12': 'Physian Res', 145 | '13': 'Demihuman Res', 146 | '14': 'Therion Res', 147 | '15': 'Undead Res', 148 | '16': 'Demon Res', 149 | '17': 'Dragon Res', 150 | } 151 | 152 | # (icon+text, text only, category) 153 | # GiftType.cs 154 | ENTITY_TYPE_DICT = { 155 | '1': (lambda id: '{{Icon|Adventurer|' + get_chara_name(id) + '|size=24px|text=1}}', 156 | lambda id: get_chara_name(id), 157 | lambda id: 'Adventurer'), 158 | '2': (lambda id: '{{' + get_label('USE_ITEM_NAME_' + id) + '-}}', 159 | lambda id: get_label('USE_ITEM_NAME_' + id), 160 | lambda id: 'Consumable'), 161 | '3': (lambda id: '{{Icon|Weapon|' + get_label('WEAPON_NAME_' + id) + '|size=24px|text=1}}', 162 | lambda id: get_label('WEAPON_NAME_' + id), 163 | lambda id: 'Weapon'), 164 | '4': (lambda _: '{{Rupies-}}', 165 | lambda _: 'Rupies', 166 | lambda id: 'Resource'), 167 | '7': (lambda id: '{{Icon|Dragon|' + get_label('DRAGON_NAME_' + id) + '|size=24px|text=1}}', 168 | lambda id: get_label('DRAGON_NAME_' + id), 169 | lambda id: 'Dragon'), 170 | '8': (lambda id: '{{' + get_label('MATERIAL_NAME_' + id) + '-}}', 171 | lambda id: get_label('MATERIAL_NAME_' + id), 172 | lambda id: 'Material'), 173 | '9': (lambda id: '{{Icon|Facility|' + get_label('FORT_PLANT_NAME_' + id) + '|size=24px|text=1}}', 174 | lambda id: get_label('FORT_PLANT_NAME_' + id), 175 | lambda id: 'Facility'), 176 | '10': (lambda id: '[[File:Icon Profile 0' + EPITHET_RANKS.get(id, '') + ' Frame.png|19px|Epithet|link=Epithets]] ' + get_label('EMBLEM_NAME_' + id), 177 | lambda id: get_label('EMBLEM_NAME_' + id), 178 | lambda id: 'Epithet'), 179 | '11': (lambda id: '[[File:' + id + ' en.png|24px|Sticker|link=Stickers]] ' + get_label('STAMP_NAME_' + id), 180 | lambda id: get_label('STAMP_NAME_' + id), 181 | lambda id: 'Sticker'), 182 | '12': (lambda id: '{{Icon|Wyrmprint|' + get_label('AMULET_NAME_' + id) + '|size=24px|text=1}}', 183 | lambda id: get_label('AMULET_NAME_' + id), 184 | lambda id: 'Wyrmprint'), 185 | '14': (lambda _: '{{Eldwater-}}', 186 | lambda _: 'Eldwater', 187 | lambda id: 'Resource'), 188 | '15': (lambda id: '{{' + get_label('DRAGON_GIFT_NAME_' + id) + '-}}', 189 | lambda id: get_label('DRAGON_GIFT_NAME_' + id), 190 | lambda id: 'Gift'), 191 | '16': (lambda _: '{{Skip Ticket-}}', 192 | lambda _: 'Skip Ticket', 193 | lambda id: 'Consumable'), 194 | '17': (lambda id: '{{' + get_label('SUMMON_TICKET_NAME_' + id) + '-}}', 195 | lambda id: get_label('SUMMON_TICKET_NAME_' + id), 196 | lambda id: 'Consumable'), 197 | '18': (lambda _: '{{Mana-}}', 198 | lambda _: 'Mana', 199 | lambda id: 'Resource'), 200 | '20': (lambda id: '{{' + get_item_label('RaidEventItem', id) + '-}}', 201 | lambda id: get_item_label('RaidEventItem', id), 202 | lambda id: 'Material'), 203 | '22': (lambda id: '{{' + get_item_label('BuildEventItem', id) + '-}}', 204 | lambda id: get_item_label('BuildEventItem', id), 205 | lambda id: 'Material'), 206 | '23': (lambda _: '{{Wyrmite-}}', 207 | lambda _: 'Wyrmite', 208 | lambda id: 'Currency'), 209 | '24': (lambda id: '{{' + get_item_label('CollectEventItem', id) + '-}}', 210 | lambda id: get_item_label('CollectEventItem', id), 211 | lambda id: 'Material'), 212 | '25': (lambda id: '{{' + get_item_label('Clb01EventItem', id) + '-}}', 213 | lambda id: get_item_label('Clb01EventItem', id), 214 | lambda id: 'Material'), 215 | '26': (lambda id: '{{' + get_item_label('AstralItem', id) + '-}}', 216 | lambda id: get_item_label('AstralItem', id), 217 | lambda id: 'Consumable'), 218 | '28': (lambda _: '{{Hustle Hammer-}}', 219 | lambda _: 'Hustle Hammer', 220 | lambda id: 'Consumable'), 221 | '29': (lambda id: '{{' + get_item_label('ExRushEventItem', id) + '-}}', 222 | lambda id: get_item_label('ExRushEventItem', id), 223 | lambda id: 'Material'), 224 | '30': (lambda id: '{{' + get_item_label('SimpleEventItem', id) + '-}}', 225 | lambda id: get_item_label('SimpleEventItem', id), 226 | lambda id: 'Material'), 227 | '31': (lambda id: '{{' + get_label('LOTTERY_TICKET_NAME_' + id) + '-}}', 228 | lambda id: get_label('LOTTERY_TICKET_NAME_' + id), 229 | lambda id: 'Consumable'), 230 | '32': (lambda id: '{{' + get_item_label('ExHunterEventItem', id) + '-}}', 231 | lambda id: get_item_label('ExHunterEventItem', id), 232 | lambda id: 'Material'), 233 | '33': (lambda id: '{{' + get_item_label('GatherItem', id) + '-}}', 234 | lambda id: get_item_label('GatherItem', id), 235 | lambda id: 'Material'), 236 | '34': (lambda id: '{{' + get_item_label('CombatEventItem', id) + '-}}', 237 | lambda id: get_item_label('CombatEventItem', id), 238 | lambda id: 'Material'), 239 | '37': (lambda id: '{{Icon|WeaponSkin|' + str(id) + '|size=24px|text=1}}', 240 | lambda id: get_label('WEAPON_SKIN_NAME_' + id), 241 | lambda id: 'WeaponSkin'), 242 | '38': (lambda id: '{{Icon|Weapon|' + get_label('WEAPON_NAME_' + id) + '|size=24px|text=1}}', 243 | lambda id: get_label('WEAPON_NAME_' + id), 244 | lambda id: 'Weapon'), 245 | '39': (lambda id: '{{Icon|Wyrmprint|' + get_label('AMULET_NAME_' + id) + '|size=24px|text=1}}', 246 | lambda id: get_label('AMULET_NAME_' + id), 247 | lambda id: 'Wyrmprint'), 248 | '40': (lambda id: '{{' + get_item_label('EarnEventItem', id) + '-}}', 249 | lambda id: get_item_label('EarnEventItem', id), 250 | lambda id: 'Material'), 251 | '41': (lambda id: '{{Icon|PortraitWyrmprint|' + str(id) + '|size=24px|text=1}}', 252 | lambda id: get_label_by_field(id, 'TalismanData'), 253 | lambda id: 'PortraitWyrmprint'), 254 | '42': (lambda id: '{{' + get_label_by_field(id, 'DmodePoint') + '-}}', 255 | lambda id: get_label_by_field(id, 'DmodePoint'), 256 | lambda id: 'KaleidoscapeItem'), 257 | '43': (lambda id: '{{Icon|' + '|'.join(get_dmode_item(id).items()) + '|size=24px|text=1}}', 258 | lambda id: get_dmode_item(id)['label'], 259 | lambda id: get_dmode_item(id)['type']), 260 | } 261 | MISSION_ENTITY_OVERRIDES_DICT = { 262 | '3' : lambda x: ["Override={}".format(get_entity_item('3', x, format=0))], 263 | '7' : lambda x: ["Override={}".format(get_entity_item('7', x, format=0))], 264 | '10': lambda x: ["Epithet: {}".format(get_label(EMBLEM_N + x)), "Rank=" + EPITHET_RANKS.get(x, '')], 265 | '11' : lambda x: ["Override={}".format(get_entity_item('11', x, format=0))], 266 | '12' : lambda x: ["Override={}".format(get_entity_item('12', x, format=0))], 267 | } 268 | EVENT_TYPES = { 269 | '1': 'Raid', 270 | '4': 'Facility', 271 | '5': 'Story', 272 | '6': 'Collab', # CLB01 / Fire Emblem 273 | '7': 'Collab', # EX_RUSH / Mega Man 274 | '8': 'Collab', # EX_HUNTER / MonHun 275 | '9': 'Simple', 276 | '10': 'Onslaught|Defensive', 277 | '11': 'Alberian Battle Royale', 278 | '12': 'Invasion', 279 | } 280 | 281 | MATERIAL_NAME_LABEL = 'MATERIAL_NAME_' 282 | PERCENTAGE_REGEX = re.compile(r' (\d+)%') 283 | 284 | class DataParser: 285 | def __init__(self, _data_name, _template, _formatter, _process_info): 286 | self.data_name = _data_name 287 | self.template = _template 288 | self.formatter = _formatter 289 | self.process_info = _process_info 290 | self.row_data = [] 291 | self.extra_data = {} 292 | 293 | def process_csv(self, file_name, func): 294 | with open(in_dir+file_name+EXT, 'r', newline='', encoding='utf-8') as in_file: 295 | reader = csv.DictReader(in_file) 296 | for row in reader: 297 | if row[ROW_INDEX] == '0': 298 | continue 299 | try: 300 | func(row, self.row_data) 301 | except TypeError: 302 | func(row, self.row_data, self.extra_data) 303 | # except Exception as e: 304 | # print('Error processing {}: {}'.format(file_name, str(e))) 305 | 306 | def process(self): 307 | try: # process_info is an iteratable of (file_name, process_function) 308 | for file_name, func in self.process_info: 309 | self.process_csv(file_name, func) 310 | except TypeError: # process_info is the process_function 311 | self.process_csv(self.data_name, self.process_info) 312 | 313 | def emit(self, out_dir): 314 | with open(out_dir+self.data_name+EXT, 'w', newline='', encoding='utf-8') as out_file: 315 | for display_name, row in self.row_data: 316 | out_file.write(self.formatter(row, self.template, display_name)) 317 | 318 | class CustomDataParser: 319 | def __init__(self, _data_name, _processor_params): 320 | self.data_name = _data_name 321 | self.process_func = _processor_params[0] 322 | self.extra_files = _processor_params[1:] 323 | 324 | def process(self, in_dir, out_dir): 325 | with open(in_dir+self.data_name+EXT, 'r', newline='', encoding='utf-8') as in_file: 326 | reader = csv.DictReader(in_file) 327 | with open(out_dir+self.data_name+EXT, 'w', newline='', encoding='utf-8') as out_file: 328 | self.process_func(reader, out_file, *[in_dir+f+EXT for f in self.extra_files]) 329 | 330 | class DatabaseBasedParser: 331 | def __init__(self, _data_name, _processor_params): 332 | self.data_name = _data_name 333 | self.process_func = _processor_params[0] 334 | 335 | def process(self, out_dir): 336 | with open(out_dir+self.data_name+EXT, 'w', newline='', encoding='utf-8') as out_file: 337 | self.process_func(out_file) 338 | 339 | def csv_as_index(path, index=None, value_key=None, tabs=False): 340 | with open(path, 'r', newline='', encoding='utf-8') as csvfile: 341 | if tabs: 342 | reader = csv.DictReader(csvfile, dialect='excel-tab') 343 | else: 344 | reader = csv.DictReader(csvfile) 345 | 346 | keys = reader.fieldnames 347 | 348 | if not index: 349 | index = keys[0] # get first key as index 350 | if not value_key and len(keys) == 2: 351 | # If not otherwise specified, load 2 column files as dict[string] = string 352 | value_key = keys[1] # get second key 353 | if value_key: 354 | return {row[index]: row[value_key] for row in reader if row[index] != '0'} 355 | else: 356 | # load >2 column files as a dict[string] = OrderedDict 357 | return {row[index]: row for row in reader if row[index] != '0'} 358 | 359 | def get_label(key, lang='en'): 360 | try: 361 | txt_label = TEXT_LABEL_DICT[lang] 362 | except KeyError: 363 | txt_label = TEXT_LABEL_DICT['en'] 364 | return (txt_label.get(key, DEFAULT_TEXT_LABEL) or DEFAULT_TEXT_LABEL).replace('\\n', ' ') 365 | 366 | def get_item_label(type, key): 367 | try: 368 | label_key = ITEM_NAMES[type][key] 369 | return get_label(label_key) 370 | except KeyError: 371 | return key 372 | 373 | def get_label_by_field(key, table, field='_Name'): 374 | try: 375 | return get_label( 376 | db_query_one( 377 | f"SELECT {field} FROM {table} WHERE _Id='{key}'")['_Name'] 378 | ) 379 | except: 380 | return key 381 | 382 | def get_dmode_item(key): 383 | result = db_query_one( 384 | f"SELECT * FROM DmodeDungeonItemData WHERE _Id='{key}'") 385 | itemtype = result['_DmodeDungeonItemType'] 386 | target_id = result['_DungeonItemTargetId'] 387 | 388 | # DmodeDungeonItemType.cs 389 | if itemtype == '1': 390 | dragon = db_query_one(f"SELECT _Name FROM DragonData WHERE _Id='{target_id}'") 391 | return {'type': 'Dragon', 'label': get_label(dragon['_Name'])} 392 | elif itemtype == '2': 393 | weapon = db_query_one( 394 | "SELECT ws._Name FROM DmodeWeapon dw " 395 | "JOIN WeaponSkin ws ON dw._WeaponSkinId = ws._Id " 396 | f"WHERE dw._Id='{target_id}'" 397 | ) 398 | return { 399 | 'type': 'Weapon', 400 | 'label': get_label(weapon['_Name']).replace(' (Skin)', ''), 401 | } 402 | elif itemtype == '3': 403 | wp = db_query_one( 404 | "SELECT ac._Name FROM DmodeAbilityCrest dac " 405 | "JOIN AbilityCrest ac ON dac._AbilityCrestId = ac._Id " 406 | f"WHERE dac._Id='{target_id}'" 407 | ) 408 | return { 409 | 'type': 'Wyrmprint', 410 | 'label': get_label(wp['_Name']), 411 | } 412 | else: 413 | print('UNKNOWN DmodeDungeonItemType') 414 | 415 | return {'type': 'KaleidoscapeItem', 'label': key} 416 | 417 | def get_chara_name(chara_id): 418 | return get_label('CHARA_NAME_COMMENT_' + chara_id) or get_label('CHARA_NAME_' + chara_id) 419 | 420 | def get_dragon_name(dragon_id): 421 | return get_label('DRAGON_NAME_COMMENT_' + dragon_id) or get_label('DRAGON_NAME_' + dragon_id) 422 | 423 | def get_epithet(emblem_id, lang='en'): 424 | return get_label(EMBLEM_N + emblem_id, lang=lang) 425 | 426 | def get_jp_epithet(emblem_id): 427 | if 'jp' in TEXT_LABEL_DICT: 428 | transliteration = get_label(EMBLEM_P + emblem_id, lang='sc') 429 | return f'({transliteration})
' + '{{' + 'Ruby|{}|{}'.format( 430 | get_label(EMBLEM_N + emblem_id, lang='jp'), 431 | get_label(EMBLEM_P + emblem_id, lang='jp')) + '}}' 432 | return '' 433 | 434 | def get_epithet_rarity(emblem_id): 435 | return db_query_one(f"SELECT _Rarity FROM EmblemData WHERE _Id='{emblem_id}'")['_Rarity'] 436 | 437 | # Formats= 0: icon + text, 1: text only, 2: category 438 | def get_entity_item(item_type, item_id, format=1): 439 | try: 440 | if item_type == '0': 441 | return '' 442 | return ENTITY_TYPE_DICT[item_type][format](item_id) 443 | except KeyError: 444 | return 'Entity type {}: {}'.format(item_type, item_id) 445 | 446 | def get_quest_title(quest_type, quest_id): 447 | if quest_type == '2': # Story 448 | return get_label('STORY_QUEST_NAME_' + quest_id) 449 | elif quest_type == '1': # Battle 450 | return get_label('QUEST_NAME_' + quest_id) 451 | elif quest_type == '3': # Treasure chest, don't handle this 452 | return '' 453 | else: 454 | print('Unknown next quest type: ' + quest_type) 455 | return '' 456 | 457 | # All process_* functions take in 1 parameter (OrderedDict row) and return 3 values (OrderedDict new_row, str template_name, str display_name) 458 | # Make sure the keys are added to the OrderedDict in the desired output order 459 | def process_AbilityLimitedGroup(row, existing_data): 460 | new_row = OrderedDict() 461 | copy_without_entriesKey(new_row, row) 462 | new_row['AbilityLimitedText'] = get_label(row['_AbilityLimitedText']).format(ability_limit0=row['_MaxLimitedValue']) 463 | existing_data.append((None, new_row)) 464 | 465 | def process_AbilityShiftGroup(row, existing_data, ability_shift_groups): 466 | ability_shift_groups[row[ROW_INDEX]] = row 467 | 468 | def process_AbilityData(row, existing_data, ability_shift_groups): 469 | if row[ROW_INDEX] in CHAIN_COAB_SET: 470 | # Process abilities known to be chain coabilities (from being 471 | # referenced in CharaData), separately. 472 | return 473 | new_row = OrderedDict() 474 | 475 | new_row['Id'] = row[ROW_INDEX] 476 | new_row['PartyPowerWeight'] = row['_PartyPowerWeight'] 477 | 478 | shift_value = 0 479 | try: 480 | shift_group = ability_shift_groups[row['_ShiftGroupId']] 481 | for i in range(1, int(shift_group['_AmuletEffectMaxLevel']) + 1): 482 | if shift_group['_Level{}'.format(i)] == row[ROW_INDEX]: 483 | shift_value = i 484 | break 485 | except KeyError: 486 | shift_value = int(row['_ShiftGroupId']) 487 | 488 | # TODO: figure out what actually goes to {ability_val0} 489 | ability_value = EDIT_THIS if row['_AbilityType1UpValue'] == '0' else row['_AbilityType1UpValue'] 490 | weapon_owner = '{weapon_owner}' 491 | weapon_type = int(row['_WeaponType']) 492 | if weapon_type < len(WEAPON_TYPE): 493 | weapon_owner = WEAPON_TYPE[weapon_type] or weapon_owner 494 | 495 | name = get_label(row['_Name']).format( 496 | ability_shift0 = ROMAN_NUMERALS[shift_value], # heck 497 | ability_val0 = ability_value, 498 | element_owner = ELEMENT_TYPE.get(row['_ElementalType'], '') or '{element_owner}', 499 | weapon_owner = weapon_owner 500 | ) 501 | # guess the generic name by chopping off the last word, which is usually +n% or V 502 | new_row['GenericName'] = name[:name.rfind(' ')].replace('%', '') 503 | new_row['Name'] = name 504 | 505 | # _ElementalType seems unreliable, use (element) in _Name for now 506 | detail_label = get_label(row['_Details']) 507 | if '{element_owner}' in detail_label and ')' in new_row['Name']: 508 | element = new_row['Name'][1:new_row['Name'].index(')')] 509 | else: 510 | element = ELEMENT_TYPE[row['_ElementalType']] 511 | if element == 'None': 512 | element = 'EDIT_THIS' 513 | new_row['Details'] = detail_label.format( 514 | ability_cond0 = row['_ConditionValue'], 515 | ability_val0 = ability_value, 516 | element_owner = element, 517 | weapon_owner = weapon_owner 518 | ) 519 | new_row['Details'] = PERCENTAGE_REGEX.sub(r" '''\1%'''", new_row['Details']) 520 | 521 | new_row['Effects'] = '' # TODO 522 | new_row['AbilityIconName'] = row['_AbilityIconName'] 523 | new_row['AbilityGroup'] = row['_ViewAbilityGroupId1'] 524 | new_row['AbilityLimitedGroupId1'] = row['_AbilityLimitedGroupId1'] 525 | new_row['AbilityLimitedGroupId2'] = row['_AbilityLimitedGroupId2'] 526 | new_row['AbilityLimitedGroupId3'] = row['_AbilityLimitedGroupId3'] 527 | existing_data.append((new_row['Name'], new_row)) 528 | 529 | def process_ChainCoAbility(row, existing_data): 530 | if not row[ROW_INDEX] in CHAIN_COAB_SET: 531 | return 532 | new_row = OrderedDict() 533 | new_row['Id'] = row[ROW_INDEX] 534 | 535 | ability_value = (EDIT_THIS if row['_AbilityType1UpValue'] == '0' 536 | else row['_AbilityType1UpValue']) 537 | new_row['Name'] = get_label(row['_Name']).format( 538 | ability_val0 = ability_value) 539 | # guess the generic name by chopping off the last word, which is usually +n% or V 540 | new_row['GenericName'] = new_row['Name'][:new_row['Name'].rfind(' ')].replace('%', '') 541 | 542 | # _ElementalType seems unreliable, use (element) in _Name for now 543 | detail_label = get_label(row['_Details']) 544 | if '{element_owner}' in detail_label and ')' in new_row['Name']: 545 | element = new_row['Name'][1:new_row['Name'].index(')')] 546 | else: 547 | element = ELEMENT_TYPE[row['_ElementalType']] 548 | new_row['Details'] = detail_label.format( 549 | ability_cond0 = row['_ConditionValue'], 550 | ability_val0 = ability_value, 551 | element_owner = element).replace(' ', ' ') 552 | new_row['Details'] = PERCENTAGE_REGEX.sub(r" '''\1%'''", new_row['Details']) 553 | new_row['Effects'] = '' # TODO 554 | new_row['AbilityIconName'] = row['_AbilityIconName'] 555 | 556 | existing_data.append((new_row['Name'], new_row)) 557 | 558 | def process_AbilityCrest(out_file): 559 | results = db_query_all( 560 | "SELECT * FROM AbilityCrest " 561 | "WHERE _Id!='0' " 562 | "ORDER BY CAST(_Id as INT)") 563 | 564 | for row in results: 565 | new_row = OrderedDict() 566 | 567 | new_row['Id'] = row['_Id'] 568 | new_row['BaseId'] = row['_BaseId'] 569 | new_row['Name'] = get_label(row['_Name']) 570 | new_row['NameJP'] = get_label(row['_Name'], lang='jp') 571 | new_row['NameSC'] = get_label(row['_Name'], lang='sc') 572 | new_row['NameTC'] = get_label(row['_Name'], lang='tc') 573 | new_row['IsHideChangeImage'] = row['_IsHideChangeImage'] 574 | new_row['Rarity'] = row['_Rarity'] 575 | new_row['AmuletType'] = row['_AbilityCrestType'] 576 | new_row['CrestSlotType'] = row['_CrestSlotType'] 577 | new_row['UnitType'] = row['_UnitType'] 578 | new_row['MinHp'] = row['_BaseHp'] 579 | new_row['MaxHp'] = row['_MaxHp'] 580 | new_row['MinAtk'] = row['_BaseAtk'] 581 | new_row['MaxAtk'] = row['_MaxAtk'] 582 | new_row['VariationId'] = row['_VariationId'] 583 | new_row['Abilities11'] = row['_Abilities11'] 584 | new_row['Abilities12'] = row['_Abilities12'] 585 | new_row['Abilities13'] = row['_Abilities13'] 586 | new_row['Abilities21'] = row['_Abilities21'] 587 | new_row['Abilities22'] = row['_Abilities22'] 588 | new_row['Abilities23'] = row['_Abilities23'] 589 | new_row['UnionAbilityGroupId'] = row['_UnionAbilityGroupId'] 590 | for i in range(1, 6): 591 | new_row[f'FlavorText{i}'] = get_label(row[f'_Text{i}']) 592 | new_row['IsPlayable'] = row['_IsPlayable'] 593 | new_row['DuplicateEntity'] = get_entity_item(row['_DuplicateEntityType'], row['_DuplicateEntityId']) 594 | new_row['DuplicateEntityQuantity'] = row['_DuplicateEntityQuantity'] 595 | new_row['AbilityCrestBuildupGroupId'] = row['_AbilityCrestBuildupGroupId'] 596 | new_row['UniqueBuildupMaterialId'] = row['_UniqueBuildupMaterialId'] 597 | new_row['AbilityCrestLevelRarityGroupId'] = row['_AbilityCrestLevelRarityGroupId'] 598 | 599 | # Trade/obtain info 600 | trade = db_query_one('SELECT * FROM AbilityCrestTrade ' 601 | f'WHERE _AbilityCrestId="{row["_Id"]}"') 602 | 603 | if trade: 604 | new_row['NeedDewPoint'] = trade['_NeedDewPoint'] 605 | new_row['Obtain'] = '[[Shop/Wyrmprints|Wyrmprints Shop]]' 606 | new_row['ReleaseDate'] = trade['_CommenceDate'] 607 | if trade['_MemoryPickupEventId'] != '0': 608 | new_row['Availability'] = 'Compendium' 609 | elif trade['_CompleteDate']: 610 | new_row['Availability'] = 'Limited' 611 | else: 612 | new_row['Availability'] = 'Permanent' 613 | 614 | else: 615 | new_row['NeedDewPoint'] = 0 616 | new_row['Obtain'] = '' 617 | new_row['ReleaseDate'] = '' 618 | new_row['Availability'] = '' 619 | 620 | new_row['ArtistCV'] = row['_CvInfo'] 621 | new_row['FeaturedCharacters'] = '' 622 | new_row['Notes'] = '' 623 | 624 | out_file.write(ENTRY_LINE_BREAK) 625 | out_file.write(new_row['Name']) 626 | out_file.write(ENTRY_LINE_BREAK) 627 | out_file.write(build_wikitext_row('Wyrmprint', new_row, delim='\n|')) 628 | out_file.write('\n') 629 | 630 | def process_ActionCondition(out_file): 631 | conditions_with_unique_icons = db_query_all( 632 | "SELECT ac.*,bid._IconName FROM ActionCondition ac " 633 | "JOIN BuffIconData bid ON (ac._BuffIconId = bid._Id) " 634 | "WHERE ac._Id!='0' AND (_Overwrite!='0' OR _OverwriteIdenticalOwner!='0' OR _OverwriteGroupId!='0') " 635 | "ORDER BY CAST(ac._Id as INT)") 636 | 637 | for cond in conditions_with_unique_icons: 638 | row = OrderedDict() 639 | row['Id'] = cond['_Id'] 640 | name = get_label(cond['_Text']).replace('{0:P0}', 'X%') 641 | nameEx = get_label(cond['_TextEx']) 642 | if name: 643 | row['Name'] = name 644 | if nameEx: 645 | row['NameEx'] = nameEx 646 | if cond['_IconName']: 647 | row['IconName'] = cond['_IconName'] 648 | 649 | for field in ('_Overwrite', '_OverwriteIdenticalOwner', '_OverwriteGroupId', 650 | '_MaxDuplicatedCount'): 651 | if cond[field] != '0': 652 | row[field[1:]] = cond[field] 653 | 654 | out_file.write(build_wikitext_row('ActionCondition', row)) 655 | out_file.write('\n') 656 | 657 | def process_AlbumStory(out_file): 658 | groups = db_query_all( 659 | "SELECT asg.* FROM AlbumStoryGroup asg " 660 | "WHERE asg._Id!='0' " 661 | "ORDER BY CAST(asg._SortId as INT)") 662 | 663 | for group in groups: 664 | row = OrderedDict() 665 | 666 | if group['_AlbumStoryId'] == '101': # Campaign 667 | albumId = group['_Id'] 668 | chapter_num = db_query_one( 669 | "SELECT _ChapterNum FROM QuestMainGroup " 670 | f"WHERE _Id='{albumId}'")['_ChapterNum'] 671 | row['Name'] = 'Chapter ' + chapter_num 672 | else: 673 | row['Name'] = get_label('EVENT_NAME_' + group['_Id']) 674 | 675 | row['Id'] = group['_Id'] 676 | row['AlbumStoryId'] = group['_AlbumStoryId'] 677 | row['SortId'] = group['_SortId'] 678 | row['ReleaseQuestStoryId'] = group['_ReleaseQuestStoryId'] 679 | row['StartDate'] = group['_ViewStartDate'] 680 | row['EndDate'] = group['_ViewEndDate'] 681 | 682 | character_ids = [] 683 | dragon_ids = [] 684 | npc_ids = [] 685 | for i in range(1,9): 686 | entity_type = group[f'_ViewEntityType{i}'] 687 | if entity_type == '1': 688 | cid = group[f'_ViewEntityId{i}'] 689 | character = db_query_one( 690 | "SELECT _IsPlayable FROM CharaData " 691 | f"WHERE _Id='{cid}'" 692 | ) 693 | if not character or character['_IsPlayable'] == '1': 694 | character_ids.append(cid) 695 | else: 696 | npc_ids.append(cid) 697 | elif entity_type == '7': 698 | dragon_ids.append(group[f'_ViewEntityId{i}']) 699 | elif entity_type == '0': 700 | continue 701 | else: 702 | print('UNKNOWN ViewEntityType: ' + entity_type) 703 | 704 | memory_ids = [] 705 | for i in range(1,13): 706 | image_id = group[f'_ArtworkImageId{i}'] 707 | if image_id != '0': 708 | memory_ids.append(group[f'_ArtworkImageId{i}']) 709 | 710 | row['CharacterIds'] = ', '.join(character_ids) 711 | row['DragonIds'] = ', '.join(dragon_ids) 712 | row['NpcIds'] = ', '.join(npc_ids) 713 | row['MemoryArtworkIds'] = ', '.join(memory_ids) 714 | 715 | out_file.write(ENTRY_LINE_BREAK) 716 | out_file.write(row['Name']) 717 | out_file.write(ENTRY_LINE_BREAK) 718 | out_file.write(build_wikitext_row('StoryGroup', row, delim='\n|')) 719 | out_file.write('\n') 720 | 721 | def process_Consumable(row, existing_data): 722 | new_row = OrderedDict() 723 | 724 | new_row['Id'] = 'Consumable_' + row[ROW_INDEX] 725 | new_row['Name'] = get_label(row['_Name']) 726 | new_row['Description'] = get_label(row['_Description']) 727 | new_row['SortId'] = row[ROW_INDEX] 728 | new_row['Obtain'] = '\n*' 729 | 730 | existing_data.append((new_row['Name'], new_row)) 731 | 732 | def process_DmodeAbilityCrest(out_file): 733 | results = db_query_all( 734 | "SELECT * FROM DmodeAbilityCrest " 735 | "WHERE _Id!='0' " 736 | "ORDER BY CAST(_Id as INT)") 737 | ability_crests = db_query_all( 738 | "SELECT * FROM AbilityCrest " 739 | "WHERE _Id!='0'") 740 | ability_crest_map = {x['_Id']: x for x in ability_crests} 741 | items = db_query_all( 742 | "SELECT * FROM DmodeDungeonItemData " 743 | "WHERE _DmodeDungeonItemType='3'") 744 | item_map = {x['_DungeonItemTargetId']: x for x in items} 745 | 746 | for row in results: 747 | new_row = OrderedDict() 748 | copy_without_entriesKey(new_row, row) 749 | 750 | ability_crest_row = ability_crest_map[new_row['AbilityCrestId']] 751 | new_row['BaseId'] = ability_crest_row['_BaseId'] 752 | new_row['Name'] = get_label(ability_crest_row['_Name']) 753 | new_row['NameJP'] = get_label(ability_crest_row['_Name'], lang='jp') 754 | new_row['NameSC'] = get_label(ability_crest_row['_Name'], lang='sc') 755 | new_row['NameTC'] = get_label(ability_crest_row['_Name'], lang='tc') 756 | 757 | item_row = item_map[new_row['Id']] 758 | new_row['ItemId'] = item_row['_Id'] 759 | new_row['ItemRarity'] = item_row['_Rarity'] 760 | new_row['ItemUseCount'] = item_row['_UseCount'] 761 | new_row['SellKaleidoscapePoint1'] = item_row['_SellDmodePoint1'] 762 | new_row['SellKaleidoscapePoint2'] = item_row['_SellDmodePoint2'] 763 | 764 | out_file.write(build_wikitext_row('KaleidoscapeWyrmprint', new_row, delim='\n|')) 765 | out_file.write('\n') 766 | 767 | def process_DmodeStory(out_file): 768 | results = db_query_all( 769 | "SELECT * FROM DmodeStory " 770 | "WHERE _Id!='0' " 771 | "ORDER BY CAST(_SortId as INT)") 772 | 773 | for row in results: 774 | new_row = OrderedDict() 775 | copy_without_entriesKey(new_row, row) 776 | new_row['Title'] = get_label(new_row['Title']) 777 | new_row['BodyText'] = get_label(new_row['BodyText']) 778 | 779 | out_file.write(build_wikitext_row('KaleidoscapeStory', new_row, delim='\n|')) 780 | out_file.write('\n') 781 | 782 | def process_DmodeServitorPassive(row, existing_data): 783 | new_row = OrderedDict() 784 | 785 | new_row['Id'] = row['_Id'] 786 | new_row['TypeId'] = row['_ServitorPassiveType'] 787 | new_row['Type'] = DMODE_SERVITOR_PASSIVE_TYPE.get(row['_ServitorPassiveType'], '') 788 | new_row['Name'] = get_label(row['_PassiveName']) 789 | new_row['Num'] = row['_PassiveNum'] 790 | new_row['SortId'] = row['_SortId'] 791 | new_row['IconImage'] = row['_IconImage'] 792 | 793 | existing_data.append((new_row['Name'], new_row)) 794 | 795 | def process_DmodeServitorPassiveLevel(row, existing_data): 796 | new_row = OrderedDict() 797 | 798 | new_row['Id'] = row['_Id'] 799 | new_row['Num'] = row['_PassiveNum'] 800 | new_row['Detail'] = get_label(row['_PassiveDetail']).format(ability_val0 = row['_UpValue']) 801 | new_row['Level'] = row['_Level'] 802 | new_row['UpValue'] = row['_UpValue'] 803 | 804 | for i in range(1,4): 805 | new_row[f'ReleaseEntityType{i}'] = get_entity_item(row[f'_ReleaseEntityType{i}'], row[f'_ReleaseEntityId{i}']) 806 | new_row[f'ReleaseEntityQuantity{i}'] = row[f'_ReleaseEntityQuantity{i}'] 807 | 808 | existing_data.append((None, new_row)) 809 | 810 | def process_DmodeWeapon(out_file): 811 | results = db_query_all("SELECT * FROM DmodeWeapon WHERE _Id!='0'") 812 | weapon_skins = db_query_all("SELECT * FROM WeaponSkin WHERE _Id!='0'") 813 | weapon_skin_map = {x['_Id']: x for x in weapon_skins} 814 | items = db_query_all( 815 | "SELECT * FROM DmodeDungeonItemData " 816 | "WHERE _DmodeDungeonItemType='2'") 817 | item_map = {x['_DungeonItemTargetId']: x for x in items} 818 | 819 | for row in results: 820 | skin_row = weapon_skin_map[row['_WeaponSkinId']] 821 | 822 | new_row = OrderedDict() 823 | new_row['Id'] = row['_Id'] 824 | new_row['BaseId'] = skin_row['_BaseId'] 825 | new_row['VariationId'] = skin_row['_VariationId'] 826 | new_row['FormId'] = skin_row['_FormId'] 827 | new_row['Name'] = get_label(skin_row['_Name']).replace(' (Skin)', '') 828 | new_row['WeaponType'] = skin_row['_WeaponType'] 829 | # new_row['NameJP'] = get_label(skin_row['_Name'], lang='jp').replace('[スキン]', '') 830 | # new_row['NameSC'] = get_label(skin_row['_Name'], lang='sc').replace('[皮肤]', '') 831 | # new_row['NameTC'] = get_label(skin_row['_Name'], lang='jp').replace('[造型]', '') 832 | new_row['StrengthParamGroupId'] = row['_StrengthParamGroupId'] 833 | new_row['StrengthAbilityGroupId'] = row['_StrengthAbilityGroupId'] 834 | new_row['StrengthSkillGroupId'] = row['_StrengthSkillGroupId'] 835 | new_row['IsDefaultWeapon'] = row['_IsDefaultWeapon'] 836 | new_row['WeaponSkinId'] = row['_WeaponSkinId'] 837 | 838 | if new_row['Id'] in item_map: 839 | item_row = item_map[new_row['Id']] 840 | new_row['ItemId'] = item_row['_Id'] 841 | new_row['ItemRarity'] = item_row['_Rarity'] 842 | new_row['ItemUseCount'] = item_row['_UseCount'] 843 | new_row['SellKaleidoscapePoint1'] = item_row['_SellDmodePoint1'] 844 | new_row['SellKaleidoscapePoint2'] = item_row['_SellDmodePoint2'] 845 | 846 | out_file.write(build_wikitext_row('KaleidoscapeWeapon', new_row, delim='\n|')) 847 | out_file.write('\n') 848 | 849 | def process_DmodeEnemies(out_file): 850 | enemies = db_query_all( 851 | "SELECT ep.*, el._Name, el._TribeType " 852 | "FROM EnemyParam ep " 853 | "JOIN EnemyData ed ON ed._Id=ep._DataId " 854 | "JOIN EnemyList el ON ed._BookId=el._id " 855 | "WHERE ep._DmodeEnemyParamGroupId!='0' " 856 | "ORDER BY ep._Id") 857 | for e in enemies: 858 | new_row = OrderedDict() 859 | new_row['Id'] = e['_Id'] 860 | new_row['DataId'] = e['_DataId'] 861 | new_row['Name'] = get_label(e['_Name']) 862 | new_row['TribeId'] = e['_TribeType'] 863 | new_row['DmodeEnemyParamGroupId'] = e['_DmodeEnemyParamGroupId'] 864 | new_row['ParamGroupName'] = e['_ParamGroupName'] 865 | 866 | out_file.write(build_wikitext_row('KaleidoscapeEnemy', new_row, delim='|')) 867 | out_file.write('\n') 868 | 869 | def process_DmodeEnemyParams(out_file): 870 | results = db_query_all( 871 | "SELECT * FROM DmodeEnemyParam " 872 | "WHERE _Id!='0' " 873 | "ORDER BY CAST(_Id as INT)") 874 | for row in results: 875 | new_row = OrderedDict() 876 | new_row['Id'] = row['_Id'] 877 | new_row['GroupId'] = row['_DmodeEnemyParamGroupId'] 878 | new_row['Level'] = row['_Level'] 879 | new_row['DropExp'] = row['_DropExp'] 880 | new_row['DropDmodePoint1'] = row['_DropDmodePoint1'] 881 | new_row['DropDmodePoint2'] = row['_DropDmodePoint2'] 882 | new_row['Score'] = row['_DmodeScore'] 883 | new_row['HP'] = row['_Hp'] 884 | new_row['Atk'] = row['_Atk'] 885 | new_row['Def'] = row['_Def'] 886 | new_row['Overwhelm'] = row['_Overwhelm'] 887 | new_row['BaseOD'] = row['_BaseOD'] 888 | new_row['BaseBreak'] = row['_BaseBreak'] 889 | # For the index order, match up against ActionCondition fields starting from 'RatePoison'. 890 | new_row['Poison'] = row['_RegistAbnormalRate01'] 891 | new_row['Burn'] = row['_RegistAbnormalRate02'] 892 | new_row['Freeze'] = row['_RegistAbnormalRate03'] 893 | new_row['Paralysis'] = row['_RegistAbnormalRate04'] 894 | new_row['Blind'] = row['_RegistAbnormalRate05'] 895 | new_row['Stun'] = row['_RegistAbnormalRate06'] 896 | new_row['Bog'] = row['_RegistAbnormalRate07'] 897 | new_row['Sleep'] = row['_RegistAbnormalRate08'] 898 | new_row['Curse'] = row['_RegistAbnormalRate09'] 899 | new_row['Frostbite'] = row['_RegistAbnormalRate10'] 900 | new_row['Flashburn'] = row['_RegistAbnormalRate11'] 901 | new_row['Stormlash'] = row['_RegistAbnormalRate12'] 902 | new_row['Shadowblight'] = row['_RegistAbnormalRate13'] 903 | new_row['Scorchrend'] = row['_RegistAbnormalRate14'] 904 | 905 | out_file.write(build_wikitext_row('KaleidoscapeEnemyParam', new_row, delim='|')) 906 | out_file.write('\n') 907 | 908 | def process_DmodeExpeditionFloor(out_file): 909 | out_file.write('{| class="wikitable"\n') 910 | out_file.write('! Target Floor || Duration || Reward') 911 | 912 | items = db_query_all( 913 | "SELECT * FROM DmodeExpeditionFloor " 914 | "WHERE _Id!='0' " 915 | "ORDER BY CAST(_FloorNum as INT)") 916 | totalTime = 0 917 | 918 | for i in items: 919 | totalTime += int(i['_NeedTime']) // 60 920 | hours = totalTime // 60 921 | minutes = totalTime % 60 922 | 923 | if minutes > 0: 924 | duration = '{}h {}m'.format(hours, minutes) 925 | else: 926 | duration = '{}h'.format(hours, minutes) 927 | 928 | rewards = [] 929 | if i['_RewardDmodePoint1'] != '0': 930 | rewards.append('{{Dawn Amber-}} x' + i['_RewardDmodePoint1']) 931 | if i['_RewardDmodePoint2'] != '0': 932 | rewards.append('{{Dusk Amber-}} x' + i['_RewardDmodePoint2']) 933 | 934 | out_file.write('\n|-\n| ' + ' || '.join([ 935 | i['_FloorNum'], 936 | duration, 937 | ', '.join(rewards), 938 | ])) 939 | out_file.write('\n|}') 940 | 941 | def process_DmodeTreasureTrade(out_file): 942 | out_file.write('{| class="wikitable"\n') 943 | out_file.write('! Item || Limit || Cost') 944 | 945 | items = db_query_all( 946 | "SELECT * FROM TreasureTrade " 947 | "WHERE _TradeGroupId='1012' " 948 | "ORDER BY CAST(_Priority as INT)") 949 | for i in items: 950 | out_file.write('\n|-\n| ' + ' || '.join([ 951 | '{} x{}'.format( 952 | get_entity_item( 953 | i['_DestinationEntityType'], 954 | i['_DestinationEntityId'], 955 | format=0), 956 | i['_DestinationEntityQuantity'] 957 | ), 958 | i['_Limit'], 959 | ', '.join([ 960 | '{} x{}'.format( 961 | get_entity_item(i[f'_NeedEntityType{j}'], 962 | i[f'_NeedEntityId{j}'], 963 | format=0), 964 | i[f'_NeedEntityQuantity{j}'] 965 | ) 966 | for j in range(1,6) 967 | if i[f'_NeedEntityId{j}'] != '0' 968 | ]), 969 | ])) 970 | out_file.write('\n|}') 971 | 972 | def process_DmodePoint(out_file): 973 | results = db_query_all( 974 | "SELECT * FROM DmodePoint " 975 | "WHERE _Id!='0' " 976 | "ORDER BY CAST(_Id as INT)") 977 | 978 | for row in results: 979 | new_row = OrderedDict() 980 | new_row['Id'] = row['_Id'] 981 | new_row['Name'] = get_label(row['_Name']) 982 | new_row['Description'] = get_label(row['_Description']) 983 | 984 | out_file.write(ENTRY_LINE_BREAK) 985 | out_file.write(new_row['Name']) 986 | out_file.write(ENTRY_LINE_BREAK) 987 | out_file.write(build_wikitext_row('KaleidoscapeItem', new_row, delim='\n|')) 988 | out_file.write('\n') 989 | 990 | def process_Material(row, existing_data): 991 | new_row = OrderedDict() 992 | 993 | new_row['Id'] = row[ROW_INDEX] 994 | new_row['Name'] = get_label(row['_Name']) 995 | new_row['Description'] = get_label(row['_Detail']) 996 | try: 997 | new_row['Rarity'] = row['_MaterialRarity'] 998 | except KeyError: 999 | new_row['Rarity'] = '' # EDIT_THIS 1000 | if '_EventId' in row: 1001 | new_row['QuestEventId'] = row['_EventId'] 1002 | new_row['SortId'] = row[ROW_INDEX] 1003 | 1004 | if 'EV_BATTLE_ROYAL' in row['_Name']: 1005 | new_row['Category'] = 'Battle Royale' 1006 | 1007 | elif '_RaidEventId' in row: 1008 | new_row['QuestEventId'] = row['_RaidEventId'] 1009 | new_row['Category'] = 'Raid' 1010 | new_row['SortId'] = row[ROW_INDEX] 1011 | elif '_QuestEventId' in row: 1012 | new_row['QuestEventId'] = row['_QuestEventId'] 1013 | new_row['Category'] = row['_Category'] 1014 | new_row['SortId'] = row['_SortId'] 1015 | new_row['Obtain'] = '\n*' + get_label(row['_Description']) 1016 | new_row['Usage'] = '' # EDIT_THIS 1017 | new_row['MoveQuest1'] = row['_MoveQuest1'] 1018 | new_row['MoveQuest2'] = row['_MoveQuest2'] 1019 | new_row['MoveQuest3'] = row['_MoveQuest3'] 1020 | new_row['MoveQuest4'] = row['_MoveQuest4'] 1021 | new_row['MoveQuest5'] = row['_MoveQuest5'] 1022 | new_row['PouchRarity'] = row['_PouchRarity'] 1023 | 1024 | try: 1025 | new_row['Exp'] = row['_Exp'] 1026 | # new_row['Plus'] = row['_Plus'] # augments 1027 | except KeyError: 1028 | pass 1029 | 1030 | existing_data.append((new_row['Name'], new_row)) 1031 | 1032 | def process_CharaModeData(row, existing_data, chara_mode_data): 1033 | chara_mode_data[row[ROW_INDEX]] = row 1034 | 1035 | def process_CharaData(row, existing_data, chara_mode_data): 1036 | # Process NPCs separately. 1037 | if row['_IsPlayable'] != '1': 1038 | return 1039 | 1040 | new_row = OrderedDict() 1041 | 1042 | new_row['IdLong'] = row[ROW_INDEX] 1043 | new_row['Id'] = row['_BaseId'] 1044 | new_row['Name'] = get_label(row['_Name']) 1045 | new_row['FullName'] = get_label(row['_SecondName']) or new_row['Name'] 1046 | new_row['NameJP'] = get_label(row['_Name'], lang='jp') 1047 | new_row['NameSC'] = get_label(row['_Name'], lang='sc') 1048 | new_row['NameTC'] = get_label(row['_Name'], lang='tc') 1049 | new_row['Title'] = get_epithet(row['_EmblemId']) 1050 | new_row['TitleJP'] = get_jp_epithet(row['_EmblemId']) 1051 | new_row['TitleSC'] = get_epithet(row['_EmblemId'], lang='sc') 1052 | new_row['TitleTC'] = get_epithet(row['_EmblemId'], lang='tc') 1053 | new_row['Obtain'] = '' # EDIT_THIS 1054 | new_row['ReleaseDate'] = row['_ReleaseStartDate'] 1055 | new_row['Availability'] = '' # EDIT_THIS 1056 | new_row['WeaponType'] = WEAPON_TYPE[int(row['_WeaponType'])] 1057 | new_row['Rarity'] = row['_Rarity'] 1058 | new_row['Gender'] = '' # EDIT_THIS 1059 | new_row['Race'] = '' # EDIT_THIS 1060 | new_row['ElementalType'] = ELEMENT_TYPE[row['_ElementalType']] 1061 | new_row['CharaType'] = CLASS_TYPE[int(row['_CharaType'])] 1062 | new_row['VariationId'] = row['_VariationId'] 1063 | for stat in ('Hp', 'Atk'): 1064 | for i in range(3, 6): 1065 | min_k = 'Min{}{}'.format(stat, i) 1066 | new_row[min_k] = row['_' + min_k] 1067 | max_k = 'Max{}'.format(stat) 1068 | new_row[max_k] = row['_' + max_k] 1069 | add_k = 'AddMax{}1'.format(stat) 1070 | new_row[add_k] = row['_' + add_k] 1071 | for i in range(0, 6): 1072 | plus_k = 'Plus{}{}'.format(stat, i) 1073 | new_row[plus_k] = row['_' + plus_k] 1074 | mfb_k = 'McFullBonus{}5'.format(stat) 1075 | new_row[mfb_k] = row['_' + mfb_k] 1076 | new_row['MinDef'] = row['_MinDef'] 1077 | new_row['DefCoef'] = row['_DefCoef'] 1078 | try: 1079 | new_row['Skill1ID'] = row['_Skill1'] 1080 | new_row['Skill2ID'] = row['_Skill2'] 1081 | new_row['Skill1Name'] = get_label(SKILL_DATA_NAMES[row['_Skill1']]) 1082 | new_row['Skill2Name'] = get_label(SKILL_DATA_NAMES[row['_Skill2']]) 1083 | except KeyError: 1084 | new_row['Skill1ID'] = '' 1085 | new_row['Skill2ID'] = '' 1086 | new_row['Skill1Name'] = '' 1087 | new_row['Skill2Name'] = '' 1088 | 1089 | new_row['HoldEditSkillCost'] = row['_HoldEditSkillCost'] 1090 | new_row['EditSkillId'] = row['_EditSkillId'] 1091 | 1092 | if (row['_EditSkillId'] != row['_Skill1'] and 1093 | row['_EditSkillId'] != row['_Skill2'] and 1094 | row['_EditSkillId'] != '0'): 1095 | new_row['EditSkillId'] += ' // Shared skill differs from actual skill. Double check shared skill entry!' 1096 | 1097 | new_row['EditSkillLevelNum'] = row['_EditSkillLevelNum'] 1098 | new_row['EditSkillCost'] = row['_EditSkillCost'] 1099 | new_row['EditSkillRelationId'] = row['_EditSkillRelationId'] 1100 | new_row['EditReleaseEntityType1'] = row['_EditReleaseEntityType1'] 1101 | new_row['EditReleaseEntityId1'] = row['_EditReleaseEntityId1'] 1102 | new_row['EditReleaseEntityQuantity1'] = row['_EditReleaseEntityQuantity1'] 1103 | 1104 | for i in range(1, 4): 1105 | for j in range(1, 5): 1106 | ab_k = 'Abilities{}{}'.format(i, j) 1107 | new_row[ab_k] = row['_' + ab_k] 1108 | for i in range(1, 6): 1109 | ex_k = 'ExAbilityData{}'.format(i) 1110 | new_row[ex_k] = row['_' + ex_k] 1111 | for i in range(1, 6): 1112 | ex_k = 'ExAbility2Data{}'.format(i) 1113 | new_row[ex_k] = row['_' + ex_k] 1114 | CHAIN_COAB_SET.add(new_row[ex_k]) 1115 | new_row['ManaCircleName'] = row['_ManaCircleName'] 1116 | 1117 | # new_row['EffNameCriticalHit'] = row['_EffNameCriticalHit'] 1118 | 1119 | new_row['JapaneseCV'] = get_label(row['_CvInfo']) 1120 | new_row['EnglishCV'] = get_label(row['_CvInfoEn']) 1121 | new_row['Description'] = get_label(row['_ProfileText']) 1122 | new_row['IsPlayable'] = row['_IsPlayable'] 1123 | new_row['MaxFriendshipPoint'] = row['_MaxFriendshipPoint'] 1124 | # MC Stuff 1125 | new_row['MaxLimitBreakCount'] = row['_MaxLimitBreakCount'] 1126 | new_row['CharaLimitBreakId'] = row['_CharaLimitBreak'] 1127 | new_row['PieceMaterialElementId'] = row['_PieceMaterialElementId'] 1128 | if row['_GrowMaterialId'] != '0': 1129 | new_row['LimitBreakMaterialId'] = row['_GrowMaterialId'] 1130 | new_row['UniqueGrowMaterialId1'] = row['_UniqueGrowMaterialId1'] 1131 | new_row['UniqueGrowMaterialId2'] = row['_UniqueGrowMaterialId2'] 1132 | for key in ('_DefaultAbility1Level', 1133 | '_DefaultAbility2Level', 1134 | '_DefaultAbility3Level', 1135 | '_DefaultBurstAttackLevel'): 1136 | if row[key] != '0': 1137 | new_row[key[1:]] = row[key] 1138 | 1139 | gunmodes = set() 1140 | for m in range(1, 5): 1141 | mode = row['_ModeId{}'.format(m)] 1142 | if mode in chara_mode_data and int(chara_mode_data[mode]['_GunMode']): 1143 | gunmodes.add(chara_mode_data[mode]['_GunMode']) 1144 | if gunmodes: 1145 | new_row['GunModes'] = ','.join(sorted(gunmodes)) 1146 | 1147 | if row['_UniqueWeaponSkinId'] != '0': 1148 | new_row['CannotUseWeaponSkin'] = 1 1149 | 1150 | existing_data.append((new_row['FullName'], new_row)) 1151 | 1152 | def process_NPC(out_file): 1153 | results = db_query_all( 1154 | "SELECT * FROM CharaData " 1155 | "WHERE _IsPlayable='0' AND _Id!='0' AND _Id<'99' " 1156 | "ORDER BY CAST(_Id as INT)") 1157 | results.extend( 1158 | db_query_all( 1159 | "SELECT * FROM DragonData " 1160 | "WHERE _IsPlayable='0' AND _Id!='0' AND _Id<'299' " 1161 | "ORDER BY CAST(_Id as INT)") 1162 | ) 1163 | 1164 | for row in results: 1165 | new_row = OrderedDict() 1166 | new_row['Id'] = row['_BaseId'] 1167 | new_row['IdLong'] = row[ROW_INDEX] 1168 | new_row['VariationId'] = row['_VariationId'] 1169 | new_row['VariationsList'] = row['_VariationId'] 1170 | new_row['Name'] = get_label(row['_SecondName']) or get_label(row['_Name']) 1171 | new_row['NameJP'] = get_label(row['_Name'], lang='jp') 1172 | new_row['NameSC'] = get_label(row['_Name'], lang='sc') 1173 | new_row['NameTC'] = get_label(row['_Name'], lang='tc') 1174 | new_row['Title'] = '' 1175 | new_row['JapaneseCV'] = '' 1176 | new_row['EnglishCV'] = '' 1177 | new_row['ReleaseDate'] = row['_ReleaseStartDate'] 1178 | new_row['Description'] = '' 1179 | new_row['Information'] = '' 1180 | new_row['HideDisplay'] = '' 1181 | 1182 | misc_row = OrderedDict() 1183 | misc_row['otherArt'] = ( 1184 | '\n' 1185 | 'File:{id} {variation} r05.png|Story Character Icon\n' 1186 | '' 1187 | ).format(id=new_row['Id'], variation=new_row['VariationId'].zfill(2)) 1188 | 1189 | out_file.write(ENTRY_LINE_BREAK) 1190 | out_file.write(new_row['Name']) 1191 | out_file.write(ENTRY_LINE_BREAK) 1192 | out_file.write(build_wikitext_row('NPC', new_row, delim='\n|')) 1193 | out_file.write('\n') 1194 | out_file.write(build_wikitext_row('NPCMisc', misc_row, delim='\n|')) 1195 | out_file.write('\n') 1196 | 1197 | def process_TalismanData(out_file): 1198 | results = db_query_all( 1199 | "SELECT * FROM TalismanData " 1200 | "WHERE _Id!='0' " 1201 | "ORDER BY CAST(_Id as INT)") 1202 | 1203 | for row in results: 1204 | new_row = OrderedDict() 1205 | new_row['Id'] = row['_Id'] 1206 | new_row['Name'] = get_label(row['_Name']) 1207 | new_row['BaseHp'] = row['_BaseHp'] 1208 | new_row['BaseAtk'] = row['_BaseAtk'] 1209 | new_row['CharaId'] = row['_TalismanCharaId'] 1210 | new_row['SellCoin'] = row['_SellCoin'] 1211 | 1212 | out_file.write(build_wikitext_row('PortraitWyrmprint', new_row, delim='\n|')) 1213 | out_file.write('\n') 1214 | 1215 | def process_SkillDataNames(row, existing_data): 1216 | for idx, (name, chara) in enumerate(existing_data): 1217 | for i in (1, 2): 1218 | sn_k = 'Skill{}Name'.format(i) 1219 | if chara[sn_k] == row[ROW_INDEX]: 1220 | chara[sn_k] = get_label(row['_Name']) 1221 | existing_data[idx] = (name, chara) 1222 | 1223 | def process_Dragon(out_file): 1224 | results = db_query_all( 1225 | "SELECT * FROM DragonData " 1226 | "WHERE _Id!='0' " 1227 | "ORDER BY CAST(_Id as INT)") 1228 | 1229 | for row in results: 1230 | new_row = OrderedDict() 1231 | 1232 | new_row['Id'] = row[ROW_INDEX] 1233 | new_row['BaseId'] = row['_BaseId'] 1234 | new_row['Name'] = get_label(row['_Name']) 1235 | new_row['FullName'] = get_label(row['_SecondName']) or new_row['Name'] 1236 | new_row['NameJP'] = get_label(row['_Name'], lang='jp') 1237 | new_row['NameSC'] = get_label(row['_Name'], lang='sc') 1238 | new_row['NameTC'] = get_label(row['_Name'], lang='tc') 1239 | new_row['Title'] = get_epithet(row['_EmblemId']) 1240 | new_row['TitleJP'] = get_jp_epithet(row['_EmblemId']) 1241 | new_row['TitleSC'] = get_epithet(row['_EmblemId'], lang='sc') 1242 | new_row['TitleTC'] = get_epithet(row['_EmblemId'], lang='tc') 1243 | if row['_CharaBaseId'] != '0': 1244 | new_row['CharaBaseId'] = row['_CharaBaseId'] 1245 | new_row['Obtain'] = '' # EDIT_THIS 1246 | new_row['ReleaseDate'] = row['_ReleaseStartDate'] 1247 | new_row['Availability'] = '' # EDIT_THIS 1248 | new_row['Rarity'] = row['_Rarity'] 1249 | new_row['Gender'] = '' # EDIT_THIS 1250 | new_row['ElementalType'] = ELEMENT_TYPE[row['_ElementalType']] 1251 | new_row['VariationId'] = row['_VariationId'] 1252 | new_row['IsPlayable'] = row['_IsPlayable'] 1253 | new_row['MinHp'] = row['_MinHp'] 1254 | new_row['MaxHp'] = row['_MaxHp'] 1255 | new_row['AddMaxHp1'] = row['_AddMaxHp1'] 1256 | new_row['MinAtk'] = row['_MinAtk'] 1257 | new_row['MaxAtk'] = row['_MaxAtk'] 1258 | new_row['AddMaxAtk1'] = row['_AddMaxAtk1'] 1259 | try: 1260 | new_row['SkillID'] = row['_Skill1'] 1261 | new_row['SkillName'] = get_label(SKILL_DATA_NAMES[row['_Skill1']]) 1262 | new_row['Skill2ID'] = row['_Skill2'] 1263 | new_row['Skill2Name'] = get_label(SKILL_DATA_NAMES[row['_Skill2']]) 1264 | except KeyError: 1265 | pass 1266 | for i in (1, 2): 1267 | for j in range(1, 7): 1268 | ab_k = 'Abilities{}{}'.format(i, j) 1269 | new_row[ab_k] = row['_' + ab_k] 1270 | 1271 | # Kaleido 1272 | if row['_DmodePassiveAbilityId'] != '0': 1273 | new_row['DmodePassiveAbilityId'] = row['_DmodePassiveAbilityId'] 1274 | 1275 | new_row['ProfileText'] = get_label(row['_Profile']) 1276 | new_row['MaxLimitBreakCount'] = row['_MaxLimitBreakCount'] 1277 | new_row['LimitBreakId'] = row['_LimitBreakId'] 1278 | new_row['LimitBreakMaterialId'] = row['_LimitBreakMaterialId'] 1279 | new_row['FavoriteType'] = row['_FavoriteType'] 1280 | new_row['JapaneseCV'] = get_label(row['_CvInfo']) 1281 | new_row['EnglishCV'] = get_label(row['_CvInfoEn']) 1282 | new_row['SellCoin'] = row['_SellCoin'] 1283 | new_row['SellDewPoint'] = row['_SellDewPoint'] 1284 | new_row['MoveSpeed'] = row['_MoveSpeed'] 1285 | new_row['DashSpeedRatio'] = row['_DashSpeedRatio'] 1286 | new_row['TurnSpeed'] = row['_TurnSpeed'] 1287 | new_row['IsTurnToDamageDir'] = row['_IsTurnToDamageDir'] 1288 | new_row['MoveType'] = row['_MoveType'] 1289 | new_row['IsLongRange'] = row['_IsLongLange'] 1290 | new_row['AttackModifiers'] = '\n{{DragonAttackModifierRow|Combo 1|%|}}\n{{DragonAttackModifierRow|Combo 2|%|}}\n{{DragonAttackModifierRow|Combo 3|%|}}' 1291 | 1292 | out_file.write(ENTRY_LINE_BREAK) 1293 | out_file.write(new_row['FullName']) 1294 | out_file.write(ENTRY_LINE_BREAK) 1295 | out_file.write(build_wikitext_row('Dragon', new_row, delim='\n|')) 1296 | out_file.write('\n') 1297 | 1298 | def process_DragonGiftData(row, existing_data): 1299 | new_row = OrderedDict() 1300 | 1301 | new_row['Id'] = row[ROW_INDEX] 1302 | new_row['IconName'] = 'Gift_{}.png'.format(new_row['Id']) 1303 | new_row['Name'] = get_label(row['_Name']) 1304 | new_row['Description'] = get_label(row['_Descripsion']) 1305 | new_row['SortId'] = row['_SortId'] 1306 | new_row['Availability'] = '' 1307 | new_row['Reliability'] = row['_Reliability'] 1308 | new_row['FavoriteReliability'] = row['_FavoriteReliability'] 1309 | new_row['FavoriteType'] = row['_FavoriteType'] 1310 | existing_data.append((new_row['Name'], new_row)) 1311 | 1312 | def process_ExAbilityData(row, existing_data): 1313 | new_row = OrderedDict() 1314 | 1315 | new_row['Id'] = row[ROW_INDEX] 1316 | new_row['Name'] = get_label(row['_Name']) 1317 | # guess the generic name by chopping off the last word, which is usually +n% or V 1318 | new_row['GenericName'] = new_row['Name'][0:new_row['Name'].rfind(' ')] 1319 | new_row['Details'] = get_label(row['_Details']).format( 1320 | value1=row['_AbilityType1UpValue0'] 1321 | ) 1322 | new_row['Details'] = PERCENTAGE_REGEX.sub(r" '''\1%'''", new_row['Details']) 1323 | new_row['Effects'] = '' # TODO 1324 | new_row['AbilityIconName'] = row['_AbilityIconName'] 1325 | new_row['Category'] = row['_Category'] 1326 | new_row['PartyPowerWeight'] = row['_PartyPowerWeight'] 1327 | 1328 | existing_data.append((new_row['Name'], new_row)) 1329 | 1330 | event_emblem_pattern = re.compile(r'^A reward from the ([A-Z].*?) event.$') 1331 | def process_EmblemData(row, existing_data): 1332 | new_row = OrderedDict() 1333 | 1334 | new_row['Title'] = get_label(row['_Title']) 1335 | new_row['TitleJP'] = get_jp_epithet(row['_Id']) 1336 | # new_row['TitleSC'] = get_label(row['_Title'], lang='sc') 1337 | # new_row['TitleTC'] = get_label(row['_Title'], lang='tc') 1338 | new_row['Icon'] = 'data-sort-value ="{0}" | [[File:Icon_Profile_0{0}_Frame.png|28px|center]]'.format(row['_Rarity']) 1339 | new_row['Text'] = get_label(row['_Gettext']) 1340 | res = event_emblem_pattern.match(new_row['Text']) 1341 | if res: 1342 | new_row['Text'] = 'A reward from the [[{}]] event.'.format(res.group(1)) 1343 | 1344 | existing_data.append((new_row['Title'], new_row)) 1345 | 1346 | def process_FortPlantDetail(row, existing_data, fort_plant_detail): 1347 | try: 1348 | fort_plant_detail[row['_AssetGroup']].append(row) 1349 | except KeyError: 1350 | fort_plant_detail[row['_AssetGroup']] = [row] 1351 | 1352 | def process_FortPlantData(row, existing_data, fort_plant_detail): 1353 | new_row = OrderedDict() 1354 | 1355 | new_row['Id'] = row[ROW_INDEX] 1356 | new_row['Name'] = get_label(row['_Name']) 1357 | new_row['Description'] = get_label(row['_Description']) 1358 | new_row['Type'] = '' 1359 | new_row['Size'] = '{w}x{w}'.format(w=row['_PlantSize']) 1360 | new_row['Available'] = '1' 1361 | new_row['Obtain'] = '' # EDIT_THIS 1362 | new_row['ReleaseDate'] = '' # EDIT_THIS 1363 | new_row['ShortSummary'] = '' # EDIT_THIS 1364 | 1365 | images = [] 1366 | upgrades = [] 1367 | upgrade_totals = {'Cost': 0, 'Build Time': 0, 'Materials': {}} 1368 | for detail in fort_plant_detail[row[ROW_INDEX]]: 1369 | if len(images) == 0 or images[-1][1] != detail['_ImageUiName']: 1370 | images.append((detail['_Level'], detail['_ImageUiName'])) 1371 | if detail['_Level'] == '0': 1372 | continue 1373 | upgrade_row = OrderedDict() 1374 | upgrade_row['Level'] = detail['_Level'] 1375 | # EffectId 1 for dojo, 2 for altars 1376 | if detail['_EffectId'] != '0': 1377 | # stat fac 1378 | try: 1379 | new_row['Type'] = FACILITY_EFFECT_TYPE_DICT[detail['_EffectId']] 1380 | except KeyError: 1381 | print(new_row['Name']) 1382 | if detail['_EffectId'] == '4': 1383 | upgrade_row['Bonus Dmg'] = detail['_EffArgs1'] 1384 | else: 1385 | upgrade_row['HP +%'] = detail['_EffArgs1'] 1386 | upgrade_row['Str +%'] = detail['_EffArgs2'] 1387 | if detail['_EventEffectType'] != '0': 1388 | # event fac 1389 | upgrade_row['Damage +% {{Tooltip|(Event)|This Damage boost will only be active during its associated event and will disappear after the event ends.}}'] = detail['_EventEffectArgs'] 1390 | if detail['_MaterialMaxTime'] != '0': 1391 | # dragontree 1392 | upgrade_row['Prod Time'] = detail['_MaterialMaxTime'] 1393 | upgrade_row['Limit'] = detail['_MaterialMax'] 1394 | upgrade_row['Output Lv.'] = detail['_Odds'].replace('FortFruitOdds_', '') 1395 | if detail['_CostMaxTime'] != '0': 1396 | # rupee mine 1397 | upgrade_row['Prod Time'] = detail['_CostMaxTime'] 1398 | upgrade_row['Limit'] = detail['_CostMax'] 1399 | upgrade_row['{{Rupies}}Cost'] = '{:,}'.format(int(detail['_Cost'])) 1400 | upgrade_totals['Cost'] += int(detail['_Cost']) 1401 | mats = {} 1402 | for i in range(1, 6): 1403 | if detail['_MaterialsId' + str(i)] != '0': 1404 | material_name = get_label(MATERIAL_NAME_LABEL + detail['_MaterialsId' + str(i)]) 1405 | mats[material_name] = int(detail['_MaterialsNum' + str(i)]) 1406 | try: 1407 | upgrade_totals['Materials'][material_name] += mats[material_name] 1408 | except: 1409 | upgrade_totals['Materials'][material_name] = mats[material_name] 1410 | upgrade_row['Materials Needed'] = mats 1411 | if int(detail['_NeedLevel']) > 1: 1412 | upgrade_row['Player Lv. Needed'] = detail['_NeedLevel'] 1413 | upgrade_row['Total Materials Left to Max Level'] = None 1414 | upgrade_row['Build Time'] = '{{BuildTime|' + detail['_Time'] + '}}' 1415 | upgrade_totals['Build Time'] += int(detail['_Time']) 1416 | 1417 | upgrades.append(upgrade_row) 1418 | 1419 | if len(images) > 1: 1420 | new_row['Images'] = '{{#tag:tabber|\nLv' + \ 1421 | '\n{{!}}-{{!}}\n'.join( 1422 | ['{}=\n[[File:{}.png|256px]]'.format(lvl, name) for lvl, name in images]) + \ 1423 | '}}' 1424 | elif len(images) == 1: 1425 | new_row['Images'] = '[[File:{}.png|256px]]'.format(images[0][1]) 1426 | else: 1427 | new_row['Images'] = '' 1428 | 1429 | if len(upgrades) > 0: 1430 | remaining = upgrade_totals['Materials'].copy() 1431 | mat_delim = ', ' 1432 | for u in upgrades: 1433 | if len(remaining) == 1: 1434 | remaing_mats = [] 1435 | for k in remaining: 1436 | try: 1437 | remaining[k] -= u['Materials Needed'][k] 1438 | except KeyError: 1439 | pass 1440 | if remaining[k] > 0: 1441 | remaing_mats.append('{{{{{}-}}}} x{:,}'.format(k, remaining[k])) 1442 | u['Total Materials Left to Max Level'] = 'style{{=}}"text-align:left" | ' + mat_delim.join(remaing_mats) if len(remaing_mats) > 0 else '—' 1443 | else: 1444 | del u['Total Materials Left to Max Level'] 1445 | 1446 | current_mats = [] 1447 | for k, v in u['Materials Needed'].items(): 1448 | current_mats.append('{{{{{}-}}}} x{:,}'.format(k, v)) 1449 | u['Materials Needed'] = 'style{{=}}"text-align:left" | ' + mat_delim.join(current_mats) if len(current_mats) > 0 else '—' 1450 | 1451 | colspan = list(upgrades[0].keys()).index('{{Rupies}}Cost') 1452 | total_mats = [] 1453 | for k, v in upgrade_totals['Materials'].items(): 1454 | total_mats.append('{{{{{}-}}}} x{:,}'.format(k, v)) 1455 | 1456 | totals_row = ('|-\n| style{{=}}"text-align:center" colspan{{=}}"' + str(colspan) + '" | Total || ' 1457 | + '{:,}'.format(upgrade_totals['Cost']) + ' || style{{=}}"text-align:left" | ' + mat_delim.join(total_mats) 1458 | + (' || style{{=}}"text-align:left" | —' if len(remaining) == 1 else '') 1459 | + ' || {{BuildTime|' + str(upgrade_totals['Build Time']) + '}}\n') 1460 | 1461 | new_row['UpgradeTable'] = ('\n{{Wikitable|class="wikitable right" style="width:100%"\n! ' + ' !! '.join(upgrades[0].keys()) 1462 | + '\n' + ''.join(map((lambda r: row_as_wikitable(r)), upgrades)) + totals_row + '}}') 1463 | else: 1464 | new_row['UpgradeTable'] = '' 1465 | existing_data.append((new_row['Name'], new_row)) 1466 | 1467 | def process_SkillData(row, existing_data): 1468 | new_row = OrderedDict() 1469 | max_skill_lv = 4 1470 | 1471 | new_row['SkillId']= row[ROW_INDEX] 1472 | new_row['Name']= get_label(row['_Name']) 1473 | new_row['SkillType']= row['_SkillType'] 1474 | for i in range(1, max_skill_lv + 1): 1475 | si_k = 'SkillLv{}IconName'.format(i) 1476 | new_row[si_k]= row['_'+si_k] 1477 | for i in range(1, max_skill_lv + 1): 1478 | des_k = 'Description{}'.format(i) 1479 | new_row[des_k]= get_label(row['_'+des_k]) 1480 | new_row[des_k] = PERCENTAGE_REGEX.sub(r" '''\1%'''", new_row[des_k]) 1481 | new_row['MaxSkillLevel']= '' # EDIT_THIS 1482 | 1483 | # For Sp, SpLv2, SpLv3, SpEdit, SpDragon, etc fields 1484 | for suffix in ('', 'Edit', 'Dragon'): 1485 | for prefix in ['Sp'] + ['SpLv'+str(i) for i in range(2, max_skill_lv + 1)]: 1486 | sp_key = prefix + suffix 1487 | new_row[sp_key]= row['_' + sp_key] 1488 | 1489 | new_row['SpRegen']= '' # EDIT_THIS 1490 | new_row['IsAffectedByTension']= row['_IsAffectedByTension'] 1491 | new_row['ZoominTime']= '{:.1f}'.format(float(row['_ZoominTime'])) 1492 | new_row['Zoom2Time']= '{:.1f}'.format(float(row['_Zoom2Time'])) 1493 | new_row['ZoomWaitTime']= '{:.1f}'.format(float(row['_ZoomWaitTime'])) 1494 | new_row['SpRecoveryRule']= row['_SpRecoveryRule'] 1495 | if row['_AutoRecoverySpForDmode'] != '0': 1496 | new_row['AutoRecoverySpForDmode']= row['_AutoRecoverySpForDmode'] 1497 | if row['_AutoRecoverySpForDmodeWeaponSkill'] != '0': 1498 | new_row['AutoRecoverySpForDmodeWeaponSkill'] = row['_AutoRecoverySpForDmodeWeaponSkill'] 1499 | if row['_OverChargeSkillId'] != '0': 1500 | new_row['OverChargeSkillId']= row['_OverChargeSkillId'] 1501 | new_row['Effects'] = '' # TODO 1502 | new_row['SSEffects'] = '' # TODO 1503 | 1504 | existing_data.append((new_row['Name'], new_row)) 1505 | 1506 | def process_MissionData(row, existing_data): 1507 | new_row = [get_label(row['_Text'])] 1508 | try: 1509 | entity_type = MISSION_ENTITY_OVERRIDES_DICT[row['_EntityType']](row['_EntityId']) 1510 | new_row.extend(entity_type + [row['_EntityQuantity']]) 1511 | except KeyError: 1512 | entity_type = get_entity_item(row['_EntityType'], row['_EntityId']) 1513 | new_row.extend([entity_type, row['_EntityQuantity']]) 1514 | pass 1515 | 1516 | existing_data.append((new_row[0], new_row)) 1517 | 1518 | def process_QuestData(row, existing_data): 1519 | new_row = {} 1520 | new_row['nocargo'] = '{{{nocargo|}}}' 1521 | for quest_type, quest_type_pattern in QUEST_TYPE_DICT.items(): 1522 | if quest_type_pattern.match(row['_AreaName01']): 1523 | new_row['QuestType'] = quest_type 1524 | break 1525 | new_row['Id'] = row[ROW_INDEX] 1526 | # new_row['_Gid'] = row['_Gid'] 1527 | new_row['QuestGroupName'] = get_label(row['_QuestViewName']).partition(':') 1528 | if not new_row['QuestGroupName'][1]: 1529 | new_row['QuestGroupName'] = '' 1530 | else: 1531 | new_row['QuestGroupName'] = new_row['QuestGroupName'][0] 1532 | try: 1533 | new_row['GroupType'] = GROUP_TYPE_DICT[row['_GroupType']] 1534 | except KeyError: 1535 | pass 1536 | new_row['EventName'] = get_label('EVENT_NAME_{}'.format(row['_Gid'])) 1537 | new_row['SectionName'] = get_label(row['_SectionName']) 1538 | new_row['QuestViewName'] = get_label(row['_QuestViewName']) 1539 | new_row['Elemental'] = ELEMENT_TYPE[row['_Elemental']] 1540 | new_row['Elemental2'] = ELEMENT_TYPE[row['_Elemental2']] 1541 | new_row['LimitedElementalType'] = ELEMENT_TYPE[row['_LimitedElementalType']] 1542 | new_row['LimitedElementalType2'] = ELEMENT_TYPE[row['_LimitedElementalType2']] 1543 | new_row['LimitedWeaponTypePatternId'] = row['_LimitedWeaponTypePatternId'] 1544 | if row['_QuestOverwriteId'] != '0': 1545 | new_row['FixedStats'] = '1' 1546 | if row['_QuestOrderPartyGroupId'] != '0': 1547 | new_row['FixedPartyGroupId'] = row['_QuestOrderPartyGroupId'] 1548 | # new_row['ElementalId'] = int(row['_Elemental']) 1549 | # process_QuestMight 1550 | if row['_DifficultyLimit'] == '0': 1551 | new_row['SuggestedMight'] = row['_Difficulty'] 1552 | else: 1553 | new_row['MightRequirement'] = row['_DifficultyLimit'] 1554 | 1555 | # process_QuestSkip 1556 | if row['_SkipTicketCount'] == '1': 1557 | new_row['SkipTicket'] = 'Yes' 1558 | elif row['_SkipTicketCount'] == '-1': 1559 | new_row['SkipTicket'] = '' 1560 | 1561 | if row['_PayStaminaSingle'] != '0': 1562 | new_row['NormalStaminaCost'] = row['_PayStaminaSingle'] 1563 | new_row['CampaignStaminaCost'] = row['_CampaignStaminaSingle'] 1564 | if row['_PayStaminaMulti'] != '0': 1565 | new_row['GetherwingCost'] = row['_PayStaminaMulti'] 1566 | new_row['CampaignGetherwingCost'] = row['_CampaignStaminaMulti'] 1567 | 1568 | if row['_PayEntityType'] != '0': 1569 | new_row['OtherCostType'] = get_entity_item(row['_PayEntityType'], row['_PayEntityId']) 1570 | new_row['OtherCostQuantity'] = row['_PayEntityQuantity'] 1571 | 1572 | new_row['ClearTermsType'] = get_label('QUEST_CLEAR_CONDITION_{}'.format(row['_ClearTermsType'])) 1573 | 1574 | row_failed_terms_type = row['_FailedTermsType'] 1575 | row_failed_terms_type = "0" if row_failed_terms_type == "6" else row_failed_terms_type 1576 | new_row['FailedTermsType'] = get_label('QUEST_FAILURE_CONDITON_{}'.format(row_failed_terms_type)) 1577 | if row['_FailedTermsTimeElapsed'] != '0': 1578 | new_row['TimeLimit'] = row['_FailedTermsTimeElapsed'] 1579 | 1580 | new_row['ContinueLimit'] = row['_ContinueLimit'] 1581 | new_row['RebornLimit'] = row['_RebornLimit'] 1582 | new_row['ThumbnailImage'] = row['_ThumbnailImage'] 1583 | new_row['ShowEnemies'] = 1 1584 | new_row['AutoPlayType'] = row['_AutoPlayType'] 1585 | 1586 | page_name = new_row['QuestViewName'] 1587 | if new_row.get('GroupType', '') == 'Campaign': 1588 | if row['_VariationType'] == '1': 1589 | page_name += '/Normal' 1590 | elif row['_VariationType'] == '2': 1591 | page_name += '/Hard' 1592 | elif row['_VariationType'] == '3': 1593 | page_name += '/Very Hard' 1594 | elif new_row.get('GroupType', '') == 'Event': 1595 | if new_row.get('QuestType', '') in ('Onslaught', 'Defensive'): 1596 | quest_mode_suffix = f" ({new_row['EventName']})" 1597 | else: 1598 | quest_mode_suffix = QUEST_MODE_PLAY_TYPE_DICT.get(row['_QuestPlayModeType'], '') 1599 | new_row['QuestViewName'] += quest_mode_suffix 1600 | page_name += quest_mode_suffix 1601 | 1602 | new_row['DropRewards'] = '' 1603 | new_row['WeaponRewards'] = '' 1604 | new_row['WyrmprintRewards'] = '' 1605 | 1606 | existing_data.append((page_name, new_row)) 1607 | 1608 | def process_QuestRewardData(row, existing_data): 1609 | QUEST_FIRST_CLEAR_COUNT = 5 1610 | QUEST_COMPLETE_COUNT = 3 1611 | reward_template = '\n{{{{DropReward|droptype=First|itemtype={}|item={}|exact={}}}}}' 1612 | 1613 | found = False 1614 | for index,existing_row in enumerate(existing_data): 1615 | if existing_row[1]['Id'] == row[ROW_INDEX]: 1616 | found = True 1617 | break 1618 | assert(found) 1619 | 1620 | curr_row = existing_row[1] 1621 | complete_type_dict = { 1622 | '1' : (lambda x: 'Don\'t allow any of your team to fall in battle' if x == '0' else 'Allow no more than {} of your team to fall in battle'.format(x)), 1623 | '15': (lambda x: 'Don\'t use any continues'), 1624 | '18': (lambda x: 'Finish in {} seconds or less'.format(x)), 1625 | '32': (lambda x: 'Don\'t use any revives'), 1626 | } 1627 | 1628 | curr_row['FirstClearRewards'] = '' 1629 | for i in range(1,QUEST_FIRST_CLEAR_COUNT+1): 1630 | entity_type = row['_FirstClearSetEntityType{}'.format(i)] 1631 | entity_id = row['_FirstClearSetEntityId{}'.format(i)] 1632 | if entity_type != '0': 1633 | curr_row['FirstClearRewards'] += reward_template.format( 1634 | get_entity_item(entity_type, entity_id, format=2), 1635 | get_entity_item(entity_type, entity_id), 1636 | row['_FirstClearSetEntityQuantity{}'.format(i)]) 1637 | 1638 | for i in range(1,QUEST_COMPLETE_COUNT+1): 1639 | complete_type = row['_MissionCompleteType{}'.format(i)] 1640 | complete_value = row['_MissionCompleteValues{}'.format(i)] 1641 | clear_reward_type = row['_MissionsClearSetEntityType{}'.format(i)] 1642 | 1643 | entity_type = row['_MissionsClearSetEntityType{}'.format(i)] 1644 | if entity_type != '0': 1645 | curr_row['MissionCompleteType{}'.format(i)] = complete_type_dict[complete_type](complete_value) 1646 | curr_row['MissionsClearSetEntityType{}'.format(i)] = get_entity_item(clear_reward_type, entity_type) 1647 | curr_row['MissionsClearSetEntityQuantity{}'.format(i)] = row['_MissionsClearSetEntityQuantity{}'.format(i)] 1648 | 1649 | first_clear1_type = row['_FirstClearSetEntityType1'] 1650 | if first_clear1_type != '0': 1651 | curr_row['MissionCompleteEntityType'] = get_entity_item(first_clear1_type, row['_MissionCompleteEntityType']) 1652 | curr_row['MissionCompleteEntityQuantity'] = row['_MissionCompleteEntityQuantity'] 1653 | 1654 | limit_break_material_id = row['_DropLimitBreakMaterialId'] 1655 | if limit_break_material_id != '0': 1656 | curr_row['DropLimitBreakMaterial'] = get_entity_item('8', limit_break_material_id) 1657 | curr_row['DropLimitBreakMaterialQuantity'] = row['_DropLimitBreakMaterialQuantity'] 1658 | curr_row['LimitBreakMaterialDailyDrop'] = row['_LimitBreakMaterialDailyDrop'] 1659 | 1660 | if (quest_scoring_enemy_group_id := row['_QuestScoringEnemyGroupId']) != '0': 1661 | curr_row['QuestScoringEnemyGroupId'] = quest_scoring_enemy_group_id 1662 | 1663 | existing_data[index] = (existing_row[0], curr_row) 1664 | 1665 | def process_QuestBonusData(row, existing_data): 1666 | 1667 | found = False 1668 | for index, existing_row in enumerate(existing_data): 1669 | if '_Gid' not in existing_row[1]: 1670 | found = False 1671 | break 1672 | if existing_row[1]['_Gid'] == row['_Id']: 1673 | found = True 1674 | break 1675 | if not found: 1676 | return 1677 | 1678 | curr_row = existing_row[1] 1679 | if row['_QuestBonusType'] == '1': 1680 | curr_row['DailyDropQuantity'] = row['_QuestBonusCount'] 1681 | curr_row['DailyDropReward'] = '' 1682 | elif row['_QuestBonusType'] == '2': 1683 | curr_row['WeeklyDropQuantity'] = row['_QuestBonusCount'] 1684 | curr_row['WeeklyDropReward'] = '' 1685 | 1686 | existing_data[index] = (existing_row[0], curr_row) 1687 | 1688 | def process_QuestMainMenu(row, existing_data): 1689 | new_row = OrderedDict() 1690 | new_row['Id'] = row[ROW_INDEX] 1691 | quest_id = row['_EntryQuestId1'] 1692 | 1693 | if row['_EntryQuestType1'] == '1': 1694 | new_row['Type'] = 'Battle' 1695 | chapter_part_match = CHAPTER_PARTS_PATTERN.match(get_label('QUEST_TITLE_' + quest_id)) 1696 | chapter_num = chapter_part_match.group(1) 1697 | part = chapter_part_match.group(2) 1698 | elif row['_EntryQuestType1'] == '2': 1699 | new_row['Type'] = 'Story ' 1700 | chapter_part_match = CHAPTER_PARTS_PATTERN.match(get_label('STORY_QUEST_TITLE_' + quest_id)) 1701 | if chapter_part_match: 1702 | chapter_num = chapter_part_match.group(1) 1703 | part = chapter_part_match.group(2) 1704 | else: 1705 | chapter_num = row['_GroupId'][-2:].lstrip('0') 1706 | part = '' 1707 | elif row['_EntryQuestType1'] == '3': 1708 | # these are treasure chests, which we don't particularly care to process here 1709 | return 1710 | else: 1711 | print('unknown main quest type') 1712 | 1713 | new_row['Chapter'] = chapter_num 1714 | new_row['ChapterName'] = get_label('QUEST_GROUP_NAME_' + row['_GroupId']) 1715 | new_row['Location'], new_row['LocationName'] = get_label( 1716 | 'QUEST_LANDMARK_NAME_' + row['_LocationId']).split('. ') 1717 | new_row['Part'] = part 1718 | 1719 | prev_quest_id = row['_ReleaseQuestId1'] 1720 | if prev_quest_id != '0': 1721 | new_row['PreviousQuest'] = get_quest_title(row['_ReleaseQuestType1'], prev_quest_id) or prev_quest_id 1722 | 1723 | next_quests = db_query_all( 1724 | "SELECT _EntryQuestId1,_EntryQuestType1 FROM QuestMainMenu " 1725 | f"WHERE '{quest_id}' IN (_ReleaseQuestId1, _ReleaseQuestId2, _ReleaseQuestId3) " 1726 | "AND _EntryQuestType1 != '3'") 1727 | for i in range(len(next_quests)): 1728 | next_quest_id = next_quests[i]['_EntryQuestId1'] 1729 | key = 'NextQuest' + (str(i + 1) if i > 0 else '') 1730 | new_row[key] = get_quest_title(next_quests[i]['_EntryQuestType1'], next_quest_id) or next_quest_id 1731 | 1732 | title = get_quest_title(row['_EntryQuestType1'], quest_id) or quest_id 1733 | existing_data.append((title, new_row)) 1734 | 1735 | def process_QuestWeaponTypePattern(row, existing_data): 1736 | new_row = OrderedDict() 1737 | copy_without_entriesKey(new_row, row) 1738 | allowed_types = [] 1739 | for i in range(1, len(WEAPON_TYPE)): 1740 | if row['_IsPatternWeaponType{}'.format(i)] == '1': 1741 | allowed_types.append( 1742 | '[[File:Icon Weapon {0}.png|24px|link=]] {0}'.format(WEAPON_TYPE[i])) 1743 | new_row['WeaponTypes'] = ', '.join(allowed_types) 1744 | existing_data.append((None, new_row)) 1745 | 1746 | def process_UnionAbility(row, existing_data): 1747 | new_row = OrderedDict() 1748 | copy_without_entriesKey(new_row, row) 1749 | new_row['Name'] = get_label(row['_Name']) 1750 | existing_data.append((None, new_row)) 1751 | 1752 | def process_WeaponBody(row): 1753 | new_row = OrderedDict() 1754 | 1755 | new_row['Id'] = row['_Id'] 1756 | new_row['Name'] = get_label(row['_Name']) 1757 | new_row['NameJP'] = get_label(row['_Name'], lang='jp') 1758 | new_row['NameSC'] = get_label(row['_Name'], lang='sc') 1759 | new_row['NameTC'] = get_label(row['_Name'], lang='tc') 1760 | new_row['WeaponSeriesId'] = row['_WeaponSeriesId'] 1761 | new_row['WeaponSkinId'] = row['_WeaponSkinId'] 1762 | new_row['WeaponType'] = row['_WeaponType'] 1763 | new_row['Rarity'] = row['_Rarity'] 1764 | new_row['ElementalType'] = row['_ElementalType'] 1765 | new_row['Obtain'] = 'Crafting' if row['_CreateCoin'] != '0' else '' # EDIT_THIS 1766 | new_row['MaxLimitOverCount'] = row['_MaxLimitOverCount'] 1767 | new_row['BaseHp'] = row['_BaseHp'] 1768 | new_row['MaxHp1'] = row['_MaxHp1'] 1769 | new_row['MaxHp2'] = row['_MaxHp2'] 1770 | new_row['MaxHp3'] = row['_MaxHp3'] 1771 | new_row['BaseAtk'] = row['_BaseAtk'] 1772 | new_row['MaxAtk1'] = row['_MaxAtk1'] 1773 | new_row['MaxAtk2'] = row['_MaxAtk2'] 1774 | new_row['MaxAtk3'] = row['_MaxAtk3'] 1775 | new_row['LimitOverCountPartyPower1'] = row['_LimitOverCountPartyPower1'] 1776 | new_row['LimitOverCountPartyPower2'] = row['_LimitOverCountPartyPower2'] 1777 | new_row['CrestSlotType1BaseCount'] = row['_CrestSlotType1BaseCount'] 1778 | new_row['CrestSlotType1MaxCount'] = row['_CrestSlotType1MaxCount'] 1779 | new_row['CrestSlotType2BaseCount'] = row['_CrestSlotType2BaseCount'] 1780 | new_row['CrestSlotType2MaxCount'] = row['_CrestSlotType2MaxCount'] 1781 | new_row['CrestSlotType3BaseCount'] = row['_CrestSlotType3BaseCount'] 1782 | new_row['CrestSlotType3MaxCount'] = row['_CrestSlotType3MaxCount'] 1783 | new_row['ChangeSkillId1'] = row['_ChangeSkillId1'] 1784 | new_row['ChangeSkillId2'] = row['_ChangeSkillId2'] 1785 | new_row['ChangeSkillId3'] = row['_ChangeSkillId3'] 1786 | new_row['Abilities11'] = row['_Abilities11'] 1787 | new_row['Abilities12'] = row['_Abilities12'] 1788 | new_row['Abilities13'] = row['_Abilities13'] 1789 | new_row['Abilities21'] = row['_Abilities21'] 1790 | new_row['Abilities22'] = row['_Abilities22'] 1791 | new_row['Abilities23'] = row['_Abilities23'] 1792 | new_row['IsPlayable'] = row['_IsPlayable'] 1793 | new_row['Text'] = get_label(row['_Text']) 1794 | new_row['CreateStartDate'] = row['_CreateStartDate'] 1795 | new_row['NeedFortCraftLevel'] = row['_NeedFortCraftLevel'] 1796 | new_row['NeedCreateWeaponBodyId1'] = row['_NeedCreateWeaponBodyId1'] 1797 | new_row['NeedCreateWeaponBodyId2'] = row['_NeedCreateWeaponBodyId2'] 1798 | new_row['NeedAllUnlockWeaponBodyId1'] = row['_NeedAllUnlockWeaponBodyId1'] 1799 | new_row['CreateCoin'] = row['_CreateCoin'] 1800 | for i in range(1, 6): 1801 | new_row[f'CreateEntity{i}'] = get_entity_item(row[f'_CreateEntityType{i}'], row[f'_CreateEntityId{i}']) 1802 | new_row[f'CreateEntityQuantity{i}'] = row[f'_CreateEntityQuantity{i}'] 1803 | new_row['DuplicateEntity'] = get_entity_item(row['_DuplicateEntityType'], row['_DuplicateEntityId']) 1804 | new_row['DuplicateEntityQuantity'] = row['_DuplicateEntityQuantity'] 1805 | new_row['WeaponPassiveAbilityGroupId'] = row['_WeaponPassiveAbilityGroupId'] 1806 | new_row['WeaponBodyBuildupGroupId'] = row['_WeaponBodyBuildupGroupId'] 1807 | new_row['MaxWeaponPassiveCharaCount'] = row['_MaxWeaponPassiveCharaCount'] 1808 | new_row['WeaponPassiveEffHp'] = row['_WeaponPassiveEffHp'] 1809 | new_row['WeaponPassiveEffAtk'] = row['_WeaponPassiveEffAtk'] 1810 | for i in range(1, 6): 1811 | new_row[f'RewardWeaponSkinId{i}'] = row[f'_RewardWeaponSkinId{i}'] 1812 | 1813 | return new_row 1814 | 1815 | def process_WeaponSkin(row): 1816 | new_row = OrderedDict() 1817 | # set some initial ordering 1818 | for x in ('Id', 'Name', 'NameJP', 'NameSC', 'NameTC', 'Text'): 1819 | new_row[x] = '' 1820 | copy_without_entriesKey(new_row, row) 1821 | del new_row['DuplicateEntityType'] 1822 | del new_row['DuplicateEntityId'] 1823 | new_row['Name'] = get_label(row['_Name']) 1824 | new_row['NameJP'] = get_label(row['_Name'], lang='jp') 1825 | new_row['NameSC'] = get_label(row['_Name'], lang='sc') 1826 | new_row['NameTC'] = get_label(row['_Name'], lang='tc') 1827 | new_row['Text'] = get_label(row['_Text']) 1828 | new_row['Obtain'] = '' 1829 | new_row['Availability'] = '' 1830 | new_row['ReleaseDate'] = '' 1831 | new_row['DuplicateEntity'] = get_entity_item(row['_DuplicateEntityType'], row['_DuplicateEntityId']) 1832 | return new_row 1833 | 1834 | def process_Weapons(out_file): 1835 | weapon_skins = db_query_all("SELECT * FROM WeaponSkin WHERE _Id!='0'") 1836 | for skin in weapon_skins: 1837 | skin_row = process_WeaponSkin(skin) 1838 | skin_id = skin_row['Id'] 1839 | weapon = db_query_one(f"SELECT * FROM WeaponBody WHERE _WeaponSkinId='{skin_id}'") 1840 | if weapon: 1841 | weapon_row = process_WeaponBody(weapon) 1842 | skin_row['HideDisplay'] = 1 1843 | skin_row['Obtain'] = weapon_row['Obtain'] 1844 | skin_row['Availability'] = WEAPON_SERIES.get(weapon_row['WeaponSeriesId'], '') 1845 | skin_row['ReleaseDate'] = weapon_row['CreateStartDate'] 1846 | 1847 | out_file.write(weapon_row['Name']) 1848 | out_file.write(ENTRY_LINE_BREAK) 1849 | out_file.write(build_wikitext_row('Weapon', weapon_row, delim='\n|')) 1850 | else: 1851 | out_file.write(skin_row['Name'].replace(' (Skin)', '')) 1852 | out_file.write(ENTRY_LINE_BREAK) 1853 | 1854 | out_file.write(build_wikitext_row('WeaponSkin', skin_row, delim='\n|')) 1855 | out_file.write(ENTRY_LINE_BREAK) 1856 | 1857 | def prcoess_QuestWallMonthlyReward(row, existing_data, reward_sum): 1858 | new_row = OrderedDict() 1859 | reward_entity_dict = { 1860 | ('18', '0'): 'Mana', 1861 | ('4', '0') : 'Rupies', 1862 | ('14', '0'): 'Eldwater', 1863 | ('8', '202004004') : 'Twinkling Sand', 1864 | } 1865 | reward_fmt = (lambda key, amount: '{{' + reward_entity_dict[key] + '-}}' + ' x {:,}'.format(amount)) 1866 | 1867 | lvl = int(row['_TotalWallLevel']) 1868 | try: 1869 | reward_sum[lvl] = reward_sum[lvl - 1].copy() 1870 | except: 1871 | reward_sum[1] = {k: 0 for k in reward_entity_dict} 1872 | reward_key = (row['_RewardEntityType'], row['_RewardEntityId']) 1873 | reward_amount = int(row['_RewardEntityQuantity']) 1874 | reward_sum[lvl][reward_key] += reward_amount 1875 | new_row['Combined'] = row['_TotalWallLevel'] 1876 | new_row['Reward'] = 'data-sort-value="{}" | '.format(reward_key[0]) + reward_fmt(reward_key, reward_amount) 1877 | new_row['Total Reward'] = ' '.join( 1878 | [reward_fmt(k, v) for k, v in reward_sum[lvl].items() if v > 0] 1879 | ) 1880 | 1881 | existing_data.append((lvl, new_row)) 1882 | 1883 | def process_BuildEventReward(reader, outfile): 1884 | table_header = ('|CollectionRewards=
\n' 1885 | '{{Wikitable|class="wikitable darkpurple sortable right" style="width:100%"\n' 1886 | '|-\n' 1887 | '! Item !! Qty !! {} Req.') 1888 | row_divider = '\n|-\n| style{{=}}"text-align:left" | ' 1889 | events = defaultdict(list) 1890 | 1891 | for row in reader: 1892 | if row['_Id'] == '0': 1893 | continue 1894 | event_item_qty = int(row['_EventItemQuantity']) 1895 | reward_item = get_entity_item(row['_RewardEntityType'], row['_RewardEntityId'], format=0) 1896 | events[row['_EntriesKey']].append({ 1897 | 'evt_item_qty': event_item_qty, 1898 | 'row': ' || '.join(( 1899 | reward_item, 1900 | f'{int(row["_RewardEntityQuantity"]):,}', 1901 | f'{event_item_qty:,}', 1902 | )) 1903 | }) 1904 | 1905 | for event_id in events: 1906 | reward_list = sorted(events[event_id], key = lambda x: x['evt_item_qty']) 1907 | 1908 | outfile.write('{} - {}'.format(get_label('EVENT_NAME_' + event_id), event_id)) 1909 | outfile.write(ENTRY_LINE_BREAK) 1910 | outfile.write(table_header) 1911 | outfile.write(row_divider) 1912 | outfile.write(row_divider.join((x['row'] for x in reward_list))) 1913 | outfile.write('\n}}\n
') 1914 | outfile.write(ENTRY_LINE_BREAK) 1915 | 1916 | def process_RaidEventReward(reader, outfile): 1917 | table_header = ('|-| {emblem_type}=\n' 1918 | '
\n' 1919 | '{{{{Wikitable|class="wikitable darkred sortable right" style="width:100%"\n' 1920 | '|-\n' 1921 | '! Item !! Qty !! Emblems Req.') 1922 | row_divider = '\n|-\n| style{{=}}"text-align:left" | ' 1923 | events = defaultdict(lambda: defaultdict(list)) 1924 | 1925 | for row in reader: 1926 | if row['_Id'] == '0': 1927 | continue 1928 | event_item_qty = int(row['_RaidEventItemQuantity']) 1929 | event_item_type = get_item_label('RaidEventItem', row['_RaidEventItemId']).replace(' Emblem', '') 1930 | reward_item = get_entity_item(row['_RewardEntityType'], row['_RewardEntityId'], format=0) 1931 | events[row['_EntriesKey']][event_item_type].append({ 1932 | 'evt_item_qty': event_item_qty, 1933 | 'row': ' || '.join(( 1934 | reward_item, 1935 | f'{int(row["_RewardEntityQuantity"]):,}', 1936 | f'{event_item_qty:,}', 1937 | )) 1938 | }) 1939 | 1940 | for event_id in events: 1941 | outfile.write('{} - {}'.format(get_label('EVENT_NAME_' + event_id), event_id)) 1942 | outfile.write(ENTRY_LINE_BREAK) 1943 | outfile.write('|CollectionRewards=
\n' 1944 | '\n') 1945 | 1946 | for emblem_type in ('Bronze', 'Silver', 'Gold'): 1947 | reward_list = sorted(events[event_id][emblem_type], key = lambda x: x['evt_item_qty']) 1948 | outfile.write(table_header.format(emblem_type=emblem_type)) 1949 | outfile.write(row_divider) 1950 | outfile.write(row_divider.join((x['row'] for x in reward_list))) 1951 | outfile.write('\n}}\n
\n') 1952 | 1953 | outfile.write('\n
') 1954 | outfile.write(ENTRY_LINE_BREAK) 1955 | 1956 | def process_CombatEventLocation(reader, outfile, reward_filename): 1957 | events = defaultdict(dict) 1958 | 1959 | for row in reader: 1960 | if row['_Id'] == '0': 1961 | continue 1962 | events[row['_EventId']][row['_LocationRewardId']] = { 1963 | 'Name': get_label(row['_LocationName']), 1964 | 'Rewards': [], 1965 | } 1966 | 1967 | with open(reward_filename, 'r', newline='', encoding='utf-8') as in_file: 1968 | rewards_reader = csv.DictReader(in_file) 1969 | for row in rewards_reader: 1970 | if row['_Id'] == '0': 1971 | continue 1972 | events[row['_EventId']][row['_LocationRewardId']]['Rewards'].append( 1973 | '* {{{{{}-}}}} x{:,}'.format(get_entity_item(row['_EntityType'], row['_EntityId']), int(row['_EntityQuantity'])) 1974 | ) 1975 | 1976 | for event_id, locations in events.items(): 1977 | outfile.write('{} - {}'.format(get_label('EVENT_NAME_' + event_id), event_id)) 1978 | outfile.write(ENTRY_LINE_BREAK) 1979 | 1980 | for location in locations.values(): 1981 | outfile.write('{}:\n'.format(location['Name'])) 1982 | outfile.write('\n'.join(location['Rewards'])) 1983 | outfile.write('\n') 1984 | 1985 | outfile.write(ENTRY_LINE_BREAK) 1986 | 1987 | def process_BattleRoyale(out_file): 1988 | out_file.write('===Adventurer skins that cannot be unlocked===\n') 1989 | unavail_skins = db_query_all( 1990 | "SELECT cd._Id FROM CharaData cd " 1991 | "LEFT JOIN BattleRoyalCharaSkin brcs ON cd._Id=brcs._BaseCharaId " 1992 | "WHERE cd._Id!='0' AND cd._IsPlayable AND brcs._Id IS NULL " 1993 | "ORDER BY cd._Id") 1994 | out_file.write(', '.join([get_chara_name(x['_Id']) for x in unavail_skins])) 1995 | 1996 | out_file.write('\n\n==Adventurers==\n') 1997 | units = db_query_all( 1998 | "SELECT bru.*,cd._BaseId,cd._VariationId,cd._WeaponType " 1999 | "FROM BattleRoyalUnit bru JOIN CharaData cd ON cd._Id=bru._BaseCharaDataId " 2000 | "WHERE bru._Id!='0' " 2001 | "ORDER BY bru._Id") 2002 | for unit in units: 2003 | weapon_name = WEAPON_TYPE[int(unit['_WeaponType'])] 2004 | data = { 2005 | 'Id': unit['_BaseCharaDataId'], 2006 | 'Portrait': '{}_{}_r05_portrait.png'.format(unit['_BaseId'], unit['_VariationId'].zfill(2)), 2007 | 'Name': weapon_name + ' Character', 2008 | 'WeaponType': weapon_name, 2009 | 'Skill': unit['_SkillId'], 2010 | 'Hp': unit['_Hp'], 2011 | 'Atk': unit['_Atk'], 2012 | } 2013 | max_level = 10 2014 | for i in range(2,max_level): 2015 | data['NeedsNumWeaponLv' + str(i)] = unit['_NeedsNumWeaponLv' + str(i)] 2016 | for i in range(2,max_level): 2017 | data['AtkRatioWeaponLv' + str(i)] = unit['_AtkRatioWeaponLv' + str(i)] 2018 | for i in range(2,max_level): 2019 | data['HpLv' + str(i)] = unit['_HpLv' + str(i)] 2020 | data['Combo'] = '\n{{ABRComboRow|Combo 1|x%|y|z}}' 2021 | data['ForceStrike'] = '' 2022 | out_file.write(build_wikitext_row('ABRCharacter', data, delim='\n|')) 2023 | out_file.write('\n') 2024 | 2025 | out_file.write('\n\n===Special Skins===\n') 2026 | special_skins = db_query_all( 2027 | "SELECT brcs.*,cd._BaseId,cd._VariationId,cd._WeaponType FROM BattleRoyalCharaSkin brcs " 2028 | "JOIN CharaData cd ON brcs._BaseCharaId=cd._Id " 2029 | "WHERE _SpecialSkillId!='0' " 2030 | "ORDER BY _Id") 2031 | for skin in special_skins: 2032 | out_file.write(build_wikitext_row('ABRCharacter', { 2033 | 'Id': skin['_Id'], 2034 | 'Portrait': '{} {} r05 portrait.png'.format(skin['_BaseId'], skin['_VariationId'].zfill(2)), 2035 | 'Name': get_chara_name(skin['_BaseCharaId']) + ' (Skin)', 2036 | 'WeaponType': WEAPON_TYPE[int(skin['_WeaponType'])], 2037 | 'Skill': skin['_SpecialSkillId'], 2038 | }, delim='\n|')) 2039 | out_file.write('\n') 2040 | 2041 | out_file.write('\n\n==Dragons==\n') 2042 | dragons = db_query_all( 2043 | "SELECT brds.*,dd._Skill1,dd._Name,dd._SecondName,dd._BaseId " 2044 | "FROM BattleRoyalDragonSchedule brds " 2045 | "JOIN DragonData dd ON dd._Id=brds._DragonId " 2046 | "WHERE brds._Id!='0' " 2047 | "ORDER BY _StartDate") 2048 | for dragon in dragons: 2049 | out_file.write(build_wikitext_row('ABRDragon', { 2050 | 'Id': dragon['_DragonId'], 2051 | 'Portrait': dragon['_BaseId'] + '_01_portrait.png', 2052 | 'Name': get_label(dragon['_SecondName']) or get_label(dragon['_Name']), 2053 | 'Skill': dragon['_Skill1'], 2054 | 'StartDate': dragon['_StartDate'], 2055 | 'EndDate': dragon['_EndDate'], 2056 | 'Combo': '', 2057 | }, delim='\n|')) 2058 | out_file.write('\n') 2059 | 2060 | out_file.write('\n\n## Misc additional data\n') 2061 | skins = db_query_all( 2062 | "SELECT * FROM BattleRoyalCharaSkin " 2063 | "WHERE _Id!='0' " 2064 | "ORDER BY _Id") 2065 | for skin in skins: 2066 | out_file.write(ENTRY_LINE_BREAK) 2067 | out_file.write(get_chara_name(skin['_BaseCharaId'])) 2068 | out_file.write(ENTRY_LINE_BREAK) 2069 | 2070 | skin['_UnlockMaterialId'] = get_label('MATERIAL_NAME_' + skin['_UnlockMaterialId']) 2071 | for key in skin.keys(): 2072 | if key in ('_EntriesKey', '_Id', '_BaseCharaId', '_AnimController'): 2073 | continue 2074 | out_file.write(key + ': ' + skin[key] + '\n') 2075 | 2076 | def process_BattleRoyalCharaSkinPickup(row, existing_data): 2077 | new_row = OrderedDict() 2078 | chara_name = get_chara_name(row['_BattleRoyalCharaId']) 2079 | new_row['CharaSkin'] = '{{Icon|Adventurer|' + chara_name + '|size=32px|text=1}}' 2080 | 2081 | start_date = datetime.strptime(row['_PickupStartDate'], '%Y/%m/%d %H:%M:%S').strftime('%B %d, %Y') 2082 | end_date = datetime.strptime(row['_PickupEndDate'], '%Y/%m/%d %H:%M:%S').strftime('%B %d, %Y') 2083 | new_row['Period'] = start_date + ' - ' + end_date 2084 | 2085 | existing_data.append((None, new_row)) 2086 | 2087 | def process_BattleRoyalEnemy(out_file): 2088 | out_file.write('{| class="wikitable"\n') 2089 | out_file.write('! Enemy || Can Enter Bush || HP || Atk || Def') 2090 | 2091 | enemies = db_query_all( 2092 | "SELECT bre.*,el._Name,ep._HP,ep._Atk,ep._Def " 2093 | "FROM BattleRoyalEnemy bre " 2094 | "JOIN EnemyParam ep ON bre._BaseEnemyParamId=ep._Id " 2095 | "JOIN EnemyData ed ON ed._Id=ep._DataId " 2096 | "JOIN EnemyList el ON ed._BookId=el._id " 2097 | "WHERE bre._Id!='0' " 2098 | "ORDER BY bre._Id") 2099 | for e in enemies: 2100 | out_file.write('\n|-\n| ' + ' || '.join([ 2101 | get_label(e['_Name']), 2102 | '{{Yes}}' if e['_CanEnterBush'] == '1' else '{{No}}', 2103 | e['_HP'], 2104 | e['_Atk'], 2105 | e['_Def'], 2106 | ])) 2107 | out_file.write('\n|}') 2108 | 2109 | def process_BattleRoyalEventCyclePointReward(out_file): 2110 | cycles = db_query_all( 2111 | "SELECT * FROM BattleRoyalEventCycle " 2112 | "WHERE _Id!='0' ORDER BY _EndDate DESC") 2113 | 2114 | for cycle in cycles: 2115 | out_file.write(ENTRY_LINE_BREAK) 2116 | out_file.write(cycle['_StartDate']) 2117 | out_file.write(' - ') 2118 | out_file.write(cycle['_EndDate']) 2119 | out_file.write(ENTRY_LINE_BREAK) 2120 | out_file.write('{{Wikitable|class="wikitable darkpurple sortable rCol2 rCol3" style="width:100%"\n') 2121 | out_file.write('! Item !! Qty !! Battle Point Req.') 2122 | 2123 | cycle_id = cycle['_Id'] 2124 | rewards = db_query_all( 2125 | "SELECT * FROM EventCyclePointReward " 2126 | f"WHERE _EventCycleId='{cycle_id}' " 2127 | "ORDER BY _Id") 2128 | for r in rewards: 2129 | out_file.write('\n|-\n| ' + ' || '.join([ 2130 | get_entity_item(r['_RewardEntityType'], r['_RewardEntityId'], format=0), 2131 | '{:,}'.format(int(r['_RewardEntityQuantity'])), 2132 | '{:,}'.format(int(r['_EventItemQuantity'])), 2133 | ])) 2134 | out_file.write('\n}}') 2135 | 2136 | def process_LoginBonusData(out_file): 2137 | bonuses = db_query_all( 2138 | "SELECT _Id,_LoginBonusName,_StartTime,_EndTime,_EachDayEntityType,_EachDayEntityQuantity " 2139 | "FROM LoginBonusData WHERE _Id != '0' ORDER BY _EndTime DESC") 2140 | 2141 | for bonus in bonuses: 2142 | bonus_id = bonus['_Id'] 2143 | name = get_label(bonus['_LoginBonusName']) 2144 | # Output format: September 27, 2019 06:00:00 UTC 2145 | start_date = datetime.strptime(bonus['_StartTime'] + ' UTC', '%Y/%m/%d %H:%M:%S %Z').strftime('%B %d, %Y %X %Z').strip() 2146 | end_date = datetime.strptime(bonus['_EndTime'] + ' UTC', '%Y/%m/%d %H:%M:%S %Z').strftime('%B %d, %Y %X %Z').strip() 2147 | 2148 | rewards = db_query_all( 2149 | "SELECT _Id,_Gid,_Day,_EntityType,_EntityId,_EntityQuantity " 2150 | f"FROM LoginBonusReward WHERE _Gid='{bonus_id}'") 2151 | rewards = '\n'.join(login_bonus_reward_string(r) for r in rewards) 2152 | 2153 | out_file.write('===' + name + '===\n') 2154 | out_file.write('[[File:Banner ' + name + '.png|300px|right]]\n') 2155 | out_file.write('[[File:Background ' + name + '.png|300px|right]]\n') 2156 | out_file.write('This login bonus was active from ' + start_date + ' to ' + end_date + '.\n') 2157 | out_file.write('{| class="wikitable"\n') 2158 | out_file.write('! Day || Bonus\n') 2159 | out_file.write(rewards) 2160 | out_file.write('\n|}\n\n') 2161 | 2162 | def login_bonus_reward_string(reward): 2163 | return ''.join([ 2164 | '|-\n| Day ', reward['_Day'], 2165 | ' || ', get_entity_item(reward['_EntityType'], reward['_EntityId'], format=0), 2166 | ' x', reward['_EntityQuantity'], 2167 | ]) 2168 | 2169 | def process_ManaCircle(out_file): 2170 | allowed_pairs = db_query_all( 2171 | "SELECT _Rarity,_ManaCircleName,_PieceMaterialElementId " 2172 | "FROM CharaData " 2173 | "WHERE _ManaCircleName != '' " 2174 | "GROUP BY _ManaCircleName,_PieceMaterialElementId") 2175 | nodes = db_query_all("SELECT * FROM MC WHERE _Id != '0' ORDER BY _Hierarchy,CAST(_No AS int)") 2176 | pieces = db_query_all("SELECT * FROM ManaPieceMaterial WHERE _Id != '0'") 2177 | 2178 | nodes_by_mc = defaultdict(list) 2179 | for n in nodes: 2180 | nodes_by_mc['MC_0' + n['_EntriesKey']].append(n) 2181 | 2182 | pieces_dict = defaultdict(lambda: defaultdict(dict)) 2183 | for p in pieces: 2184 | pieces_dict[p['_ElementId']][p['_ManaPieceType']][p['_Step']] = p 2185 | 2186 | material_fields = { 2187 | 'Mana': '_NecessaryManaPoint', 2188 | 'UniqueGrowMaterial1': '_UniqueGrowMaterialCount1', 2189 | 'UniqueGrowMaterial2': '_UniqueGrowMaterialCount2', 2190 | } 2191 | cost_rows = defaultdict(list) 2192 | display_rows = [] 2193 | 2194 | for mc_id in nodes_by_mc: 2195 | stepCounters = defaultdict(int) 2196 | for n in nodes_by_mc[mc_id]: 2197 | nodeType = n['_ManaPieceType'] 2198 | stepCounters[nodeType] += 1 2199 | display_rows.append(build_wikitext_row('MCNodeInfo', { 2200 | 'MC': mc_id, 2201 | 'Floor': n['_Hierarchy'], 2202 | 'No': n['_No'], 2203 | 'ManaPieceType': nodeType, 2204 | 'ManaPieceDesc': MANA_PIECE_DESC[nodeType], 2205 | 'IsReleaseStory': n['_IsReleaseStory'], 2206 | 'Step': stepCounters[nodeType] 2207 | })) 2208 | 2209 | for combo in allowed_pairs: 2210 | mc_id = combo['_ManaCircleName'] 2211 | piece_element_id = combo['_PieceMaterialElementId'] 2212 | pieces = pieces_dict.get(piece_element_id, {}) 2213 | stepCounters = defaultdict(int) 2214 | rarity_cost_rows = cost_rows[combo['_Rarity']] 2215 | 2216 | for n in nodes_by_mc[mc_id]: 2217 | nodeType = n['_ManaPieceType'] 2218 | stepCounters[nodeType] += 1 2219 | step = stepCounters[nodeType] 2220 | piece = pieces.get(nodeType, {}).get(str(step), None) 2221 | 2222 | for mat_type in material_fields: 2223 | quantity = n[material_fields[mat_type]] 2224 | if quantity > '0': 2225 | rarity_cost_rows.append(build_wikitext_row('MCNodeCost', { 2226 | 'MC': mc_id, 2227 | 'MCElementId': piece_element_id, 2228 | 'Floor': n['_Hierarchy'], 2229 | 'No': n['_No'], 2230 | 'Material': mat_type, 2231 | 'MaterialQuantity': n[material_fields[mat_type]], 2232 | })) 2233 | if piece: 2234 | if piece['_DewPoint'] > '0': 2235 | rarity_cost_rows.append(build_wikitext_row('MCNodeCost', { 2236 | 'MC': mc_id, 2237 | 'MCElementId': piece_element_id, 2238 | 'Floor': n['_Hierarchy'], 2239 | 'No': n['_No'], 2240 | 'Material': 'Eldwater', 2241 | 'MaterialQuantity': piece['_DewPoint'], 2242 | })) 2243 | for i in range(1,4): 2244 | matQuant = piece['_MaterialQuantity' + str(i)] 2245 | if matQuant > '0': 2246 | rarity_cost_rows.append(build_wikitext_row('MCNodeCost', { 2247 | 'MC': mc_id, 2248 | 'MCElementId': piece_element_id, 2249 | 'Floor': n['_Hierarchy'], 2250 | 'No': n['_No'], 2251 | 'Material': piece['_MaterialId' + str(i)], 2252 | 'MaterialQuantity': matQuant, 2253 | })) 2254 | 2255 | out_file.write('Template:MCNodeInfo/Data') 2256 | out_file.write(ENTRY_LINE_BREAK) 2257 | out_file.write('\n'.join(display_rows)) 2258 | for i in range(3,6): 2259 | out_file.write(ENTRY_LINE_BREAK) 2260 | out_file.write('Template:MCNodeCost/Data/{}'.format(i)) 2261 | out_file.write(ENTRY_LINE_BREAK) 2262 | out_file.write('\n'.join(cost_rows[str(i)])) 2263 | 2264 | def process_MCNodeCostUnbinds(out_file): 2265 | chara_unique_grow_mats = db_query_all( 2266 | "SELECT _UniqueGrowMaterialId1,_UniqueGrowMaterialId2 " 2267 | "FROM CharaData " 2268 | "WHERE _UniqueGrowMaterialId1 != '0' " 2269 | "GROUP BY _UniqueGrowMaterialId1,_UniqueGrowMaterialId2") 2270 | unique_grow_mats = {} 2271 | for chara_mats in chara_unique_grow_mats: 2272 | unique_grow_mats[chara_mats['_UniqueGrowMaterialId1']] = 'UniqueGrowMaterial1' 2273 | if chara_mats['_UniqueGrowMaterialId2'] != '0': 2274 | unique_grow_mats[chara_mats['_UniqueGrowMaterialId2']] = 'UniqueGrowMaterial2' 2275 | 2276 | unbinds = db_query_all("SELECT * FROM CharaLimitBreak WHERE _Id != '0'") 2277 | unbind_rows = [] 2278 | 2279 | for u in unbinds: 2280 | for floor in range(1, 6): 2281 | for growMatNum in range(1,3): 2282 | quantity = u[f'_UniqueGrowMaterial{growMatNum}Num{floor}'] 2283 | if quantity > '0': 2284 | unbind_rows.append(build_wikitext_row('MCNodeCost', { 2285 | 'MCElementId': u['_Id'], 2286 | 'Floor': floor + 1, 2287 | 'No': '0', 2288 | 'Material': f'UniqueGrowMaterial{growMatNum}', 2289 | 'MaterialQuantity': quantity, 2290 | })) 2291 | 2292 | for orbNum in range(1, 6): 2293 | quantity = u[f'_OrbData{orbNum}Num{floor}'] 2294 | if quantity > '0': 2295 | materialId = u[f'_OrbData{orbNum}Id{floor}'] 2296 | if materialId in unique_grow_mats: 2297 | materialId = unique_grow_mats[materialId] 2298 | unbind_rows.append(build_wikitext_row('MCNodeCost', { 2299 | 'MCElementId': u['_Id'], 2300 | 'Floor': floor + 1, 2301 | 'No': '0', 2302 | 'Material': materialId, 2303 | 'MaterialQuantity': quantity, 2304 | })) 2305 | out_file.write('\n'.join(unbind_rows)) 2306 | 2307 | def process_QuestScoringEnemy(out_file): 2308 | rows = db_query_all( 2309 | "SELECT qse.*, el._Name " 2310 | "FROM QuestScoringEnemy qse " 2311 | "JOIN EnemyList el ON(qse._EnemyListId=el._Id) " 2312 | "WHERE qse._Id != '0' " 2313 | "ORDER BY _ScoringEnemyGroupId, qse._Id") 2314 | for row in rows: 2315 | new_row = {k[1:]: v for k,v in row.items() if k not in ('_EntriesKey', '_EnemyListId')} 2316 | new_row['Name'] = get_label(new_row['Name']) 2317 | 2318 | out_file.write(build_wikitext_row('QuestScoringEnemy', new_row)) 2319 | out_file.write('\n') 2320 | 2321 | def process_StampData(out_file): 2322 | event_group_title = '
\n
[[{}]] Event Rewards
\n' 2323 | event_title_pattern = re.compile(r'A reward from the "?([^"]+)"? event.') 2324 | sticker_labels = { 2325 | 'en': 'Sticker: ', 2326 | 'jp': 'スタンプ(', 2327 | } 2328 | quotations = { 2329 | 'en': '""', 2330 | 'jp': '『』', 2331 | } 2332 | stickers = db_query_all( 2333 | "SELECT _Id,_Title,_InfoMsg,_VoiceId FROM StampData " 2334 | "WHERE _Id != '0' " 2335 | "ORDER BY CAST(_SortId AS int)") 2336 | groups = defaultdict(list) 2337 | 2338 | for sticker in stickers: 2339 | detail = get_label(sticker['_InfoMsg']) 2340 | if ' event' in detail: 2341 | groups[detail].append(sticker) 2342 | else: 2343 | groups['standard'].append(sticker) 2344 | 2345 | for group in groups: 2346 | if 'event' in group: 2347 | match = event_title_pattern.search(group) 2348 | group_name = event_group_title.format(match.group(1)) 2349 | else: 2350 | group_name = '== Standard ==\n' 2351 | 2352 | out_file.write(group_name) 2353 | 2354 | for lang in ('en', 'jp'): 2355 | sticker_label = sticker_labels[lang] 2356 | label_repl = '[[?]] ' + quotations[lang][0] 2357 | quote_end = quotations[lang][1] + '\n' 2358 | out_file.write('\n') 2359 | 2360 | for sticker in groups[group]: 2361 | out_file.write( 2362 | 'File:{0} {lang}.png|{{{{SimpleAudioPlayButton|{1} {lang}.wav}}}} '.format( 2363 | sticker['_Id'], sticker['_VoiceId'], lang=lang)) 2364 | sticker_quote = get_label(sticker['_Title'], lang=lang) 2365 | if lang != 'en': 2366 | sticker_quote = sticker_quote[:-1] # Chop off last parentheses 2367 | out_file.write(sticker_quote.replace(sticker_label, label_repl) + quote_end) 2368 | 2369 | out_file.write('\n') 2370 | 2371 | def process_EndeavorSets(out_file): 2372 | campaigns = db_query_all( 2373 | "SELECT _Id,_CampaignName,_CampaignType,_StartDate,_EndDate " 2374 | "FROM CampaignData WHERE _CampaignType='9' AND _Id != '0' ORDER BY _StartDate") 2375 | 2376 | for c in campaigns: 2377 | campaign_id = c['_Id'] 2378 | start_date = c['_StartDate'] + ' UTC' 2379 | end_date = c['_EndDate'] + ' UTC' 2380 | month = datetime.strptime(start_date, '%Y/%m/%d %H:%M:%S %Z').strftime(' (%b %Y)') 2381 | name = get_label(c['_CampaignName']) + month 2382 | 2383 | daily_endeavors = db_query_all( 2384 | "SELECT _Id,_Text,_SortId,_EntityType,_EntityId,_EntityQuantity " 2385 | f"FROM MissionDailyData WHERE _CampaignId='{campaign_id}' ORDER BY _SortId") 2386 | limited_endeavors = db_query_all( 2387 | "SELECT _Id,_Text,_SortId,_EntityType,_EntityId,_EntityQuantity " 2388 | f"FROM MissionPeriodData WHERE _CampaignId='{campaign_id}' ORDER BY _SortId") 2389 | 2390 | if len(daily_endeavors): 2391 | daily_set = { 2392 | 'Name': name, 2393 | 'Type': 'Daily', 2394 | 'Description': '', 2395 | 'StartDate': start_date, 2396 | 'EndDate': end_date, 2397 | 'Endeavors': '\n' + '\n'.join(endeavor_string(e, '(Daily) ') for e in daily_endeavors), 2398 | } 2399 | out_file.write(build_wikitext_row('EndeavorSet', daily_set, delim='\n|')) 2400 | out_file.write('\n') 2401 | if len(limited_endeavors): 2402 | limited_set = { 2403 | 'Name': name, 2404 | 'Type': 'Limited', 2405 | 'Description': '', 2406 | 'StartDate': start_date, 2407 | 'EndDate': end_date, 2408 | 'Endeavors': '\n' + '\n'.join(endeavor_string(e) for e in limited_endeavors), 2409 | } 2410 | out_file.write(build_wikitext_row('EndeavorSet', limited_set, delim='\n|')) 2411 | out_file.write('\n\n') 2412 | 2413 | def process_EndeavorSetsEvents(out_file): 2414 | events = db_query_all( 2415 | "SELECT _Id,_Name,_StartDate,_EndDate FROM EventData " 2416 | "WHERE _Id != '0' ORDER BY _StartDate") 2417 | 2418 | for e in events: 2419 | event_id = e['_Id'] 2420 | start_date = e['_StartDate'] + ' UTC' 2421 | end_date = e['_EndDate'] + ' UTC' 2422 | month = datetime.strptime(start_date, '%Y/%m/%d %H:%M:%S %Z').strftime('%B %Y') 2423 | name = get_label(e['_Name']) 2424 | 2425 | daily_endeavors = db_query_all( 2426 | "SELECT _Id,_Text,_SortId,_EntityType,_EntityId,_EntityQuantity " 2427 | f"FROM MissionDailyData WHERE _QuestGroupId='{event_id}' ORDER BY _SortId") 2428 | limited_endeavors = db_query_all( 2429 | "SELECT _Id,_Text,_SortId,_EntityType,_EntityId,_EntityQuantity " 2430 | f"FROM MissionPeriodData WHERE _QuestGroupId='{event_id}' ORDER BY _SortId") 2431 | memory_endeavors = db_query_all( 2432 | "SELECT _Id,_Text,_SortId,_EntityType,_EntityId,_EntityQuantity " 2433 | f"FROM MissionMemoryEventData WHERE _EventId='{event_id}' ORDER BY _SortId") 2434 | 2435 | if len(daily_endeavors): 2436 | daily_set = { 2437 | 'Name': f'{name} ({month}) Daily Endeavors', 2438 | 'Event': f'{name}/{month}', 2439 | 'Type': 'EventDaily', 2440 | 'Description': '', 2441 | 'StartDate': start_date, 2442 | 'EndDate': end_date, 2443 | 'Endeavors': '\n' + '\n'.join(endeavor_string(e) for e in daily_endeavors), 2444 | } 2445 | out_file.write(build_wikitext_row('EndeavorSet', daily_set, delim='\n|')) 2446 | out_file.write('\n') 2447 | if len(limited_endeavors): 2448 | limited_set = { 2449 | 'Name': f'{name} ({month}) Limited Endeavors', 2450 | 'Event': f'{name}/{month}', 2451 | 'Type': 'Event', 2452 | 'Description': '', 2453 | 'StartDate': start_date, 2454 | 'EndDate': end_date, 2455 | 'Endeavors': '\n' + '\n'.join(endeavor_string(e) for e in limited_endeavors), 2456 | } 2457 | out_file.write(build_wikitext_row('EndeavorSet', limited_set, delim='\n|')) 2458 | out_file.write('\n') 2459 | if len(memory_endeavors): 2460 | limited_set = { 2461 | 'Name': f'{name} Compendium Endeavors', 2462 | 'Event': f'{name}/Event Compendium', 2463 | 'Type': 'Event', 2464 | 'Description': '', 2465 | 'StartDate': start_date, 2466 | 'Endeavors': '\n' + '\n'.join(endeavor_string(e) for e in memory_endeavors), 2467 | } 2468 | out_file.write(build_wikitext_row('EndeavorSet', limited_set, delim='\n|')) 2469 | out_file.write('\n\n') 2470 | 2471 | def endeavor_string(e, prefix=''): 2472 | desc = get_label(e['_Text']).strip() 2473 | quantity = e['_EntityQuantity'] 2474 | item_type = get_entity_item(e['_EntityType'], e['_EntityId'], format=2) 2475 | item = get_entity_item(e['_EntityType'], e['_EntityId'], format=1) 2476 | extras = '' 2477 | 2478 | if item_type == 'Epithet': 2479 | rarity = get_epithet_rarity(e['_EntityId']) 2480 | item_type += 'Rank' + rarity 2481 | elif item_type == 'Sticker': 2482 | extras = '|StickerID=' + e['_EntityId'] + '_en' 2483 | 2484 | return '|'.join([ 2485 | '{{EndeavorSetRow', 2486 | prefix + desc, 2487 | item_type, 2488 | item, 2489 | quantity + extras + '}}', 2490 | ]) 2491 | 2492 | def process_HonorData(out_file): 2493 | medals = db_query_all( 2494 | "SELECT * FROM HonorData " 2495 | "WHERE _Id!='0' AND _EndDate='' ORDER BY CAST(_SortId AS int)") 2496 | for medal in medals: 2497 | name = get_label(medal['_HonorName']) 2498 | if '(' in name: 2499 | name, m_class = name.split(' (') 2500 | m_class = m_class.replace(' Class)', '') 2501 | else: 2502 | m_class = '' 2503 | 2504 | out_file.write( 2505 | '|-\n| data-sort-value="{sortId}" | [[File:Icon Medal {id}.png|60px]] ' 2506 | '|| {name} || {m_class} || {desc} || {start}\n'.format( 2507 | sortId=medal['_SortId'], 2508 | id=medal['_Id'], 2509 | name=name.replace(' Medal', ''), 2510 | m_class=m_class, 2511 | desc=get_label(medal['_Description']), 2512 | start=medal['_StartDate'].split()[0] 2513 | )) 2514 | 2515 | limited_medals = db_query_all( 2516 | "SELECT * FROM HonorData " 2517 | "WHERE _Id!='0' AND _EndDate!='' ORDER BY CAST(_SortId AS int)") 2518 | if len(limited_medals): 2519 | out_file.write('\n\n==Limited Time Medals==\n') 2520 | for medal in limited_medals: 2521 | name = get_label(medal['_HonorName']) 2522 | if '(' in name: 2523 | name, m_class = name.split(' (') 2524 | m_class = m_class.replace(' Class)', '') 2525 | else: 2526 | m_class = '' 2527 | out_file.write( 2528 | '|-\n| data-sort-value="{sortId}" | [[File:Icon Medal {id}.png|60px]] ' 2529 | '|| {name} || {m_class} || {desc} || {start} - {end}\n'.format( 2530 | sortId=medal['_SortId'], 2531 | id=medal['_Id'], 2532 | name=name.replace(' Medal', ''), 2533 | m_class=m_class, 2534 | desc=get_label(medal['_Description']), 2535 | start=medal['_StartDate'].split()[0], 2536 | end=medal['_EndDate'].split()[0] 2537 | )) 2538 | 2539 | def process_RankingGroupData(out_file): 2540 | groups = db_query_all( 2541 | "SELECT * FROM RankingGroupData " 2542 | "WHERE _Id!='0' ORDER BY _RankingEndDate DESC") 2543 | ranking_tier_rewards = db_query_all( 2544 | "SELECT * FROM RankingTierReward " 2545 | "WHERE _Id!='0' ORDER BY _Id") 2546 | reward_lists = defaultdict(lambda: defaultdict(list)) 2547 | for reward in ranking_tier_rewards: 2548 | reward_lists[reward['_GroupId']][reward['_QuestId']].append(reward) 2549 | 2550 | for group in groups: 2551 | out_file.write('===') 2552 | out_file.write(group['_RankingStartDate']) 2553 | out_file.write(' - ') 2554 | out_file.write(group['_RankingEndDate']) 2555 | out_file.write('===\n') 2556 | data = { 2557 | 'Name': 'Time Attack Challenges', 2558 | 'Date': datetime.strptime(group['_RankingStartDate'], '%Y/%m/%d %H:%M:%S').strftime('%b %Y'), 2559 | 'Type': 'Special', 2560 | 'Description': '', 2561 | 'EnemyElement1': '', 2562 | 'StartDate': group['_RankingStartDate'], 2563 | 'EndDate': group['_RankingEndDate'], 2564 | 'ViewEndDate': group['_RankingViewEndDate'], 2565 | } 2566 | battles = ( 2567 | '\n
' 2568 | '\n{{Wikitable|class="wikitable darkred" style="width:100%"' 2569 | '\n! Difficulty !! Class !! Requirements !! Reward') 2570 | battle_num = 1 2571 | 2572 | rewards = reward_lists.get(group['_RankingTierGroupId'], {}) 2573 | for quest_id in rewards: 2574 | tiers = len(rewards[quest_id]) 2575 | quest_name = get_label('QUEST_NAME_' + quest_id) 2576 | quest_link = quest_name.split(':')[0] + ' (Time Attack Challenge)' 2577 | data['BattleQuestName' + str(battle_num)] = quest_link 2578 | battle_num += 1 2579 | 2580 | battles += '\n|-\n| rowspan{{{{=}}}}"{}" | [[{}|{}]] ||'.format( 2581 | tiers, quest_link, quest_name 2582 | ) 2583 | battles += '\n|-\n|'.join([' {} || Clear in {} - {} || {} x{:,}'.format( 2584 | get_label(tier['_RankingDifficultyText']).split()[0], 2585 | str(timedelta(seconds=int(round(float(tier['_ClearTimeLower'])))))[3:], 2586 | str(timedelta(seconds=int(round(float(tier['_ClearTimeUpper'])))))[3:], 2587 | get_entity_item(tier['_RankingRewardEntityType'], tier['_RankingRewardEntityId'], format=0), 2588 | int(round(float(tier['_RankingRewardEntityQuantity']))) 2589 | ) for tier in rewards[quest_id]]) 2590 | 2591 | data['Battles'] = battles 2592 | out_file.write(build_wikitext_row('Event', data, delim='\n|')) 2593 | out_file.write('\n') 2594 | 2595 | def process_GenericTemplateWithEntriesKey(row, existing_data): 2596 | new_row = OrderedDict({k[1:]: v for k, v in row.items()}) 2597 | if 'EntriesKey1' in new_row: 2598 | del new_row['EntriesKey1'] 2599 | existing_data.append((None, new_row)) 2600 | 2601 | def process_GenericTemplate(row, existing_data): 2602 | new_row = OrderedDict() 2603 | copy_without_entriesKey(new_row, row) 2604 | existing_data.append((None, new_row)) 2605 | 2606 | def process_KeyValues(row, existing_data): 2607 | new_row = OrderedDict() 2608 | for k, v in row.items(): 2609 | if k == ROW_INDEX: 2610 | new_row['Id'] = v 2611 | elif 'Text' in k: 2612 | label = get_label(v) 2613 | if label != '': 2614 | new_row[k[1:]] = v 2615 | new_row[k[1:]+'Label'] = get_label(v) 2616 | elif v != '0' and v != '': 2617 | new_row[k[1:]] = v 2618 | existing_data.append((None, new_row)) 2619 | 2620 | def build_wikitext_row(template_name, row, delim='|'): 2621 | row_str = '{{' + template_name + delim 2622 | if template_name in ORDERING_DATA: 2623 | key_source = ORDERING_DATA[template_name] 2624 | else: 2625 | key_source = row.keys() 2626 | row_str += delim.join(['{}={}'.format(k, row[k]) for k in key_source if k in row]) 2627 | if delim[0] == '\n': 2628 | row_str += '\n' 2629 | row_str += '}}' 2630 | return row_str 2631 | 2632 | def row_as_wikitext(row, template_name, display_name = None): 2633 | text = "" 2634 | if display_name is not None: 2635 | text += display_name 2636 | text += ENTRY_LINE_BREAK 2637 | text += build_wikitext_row(template_name, row, delim='\n|') 2638 | text += ENTRY_LINE_BREAK 2639 | else: 2640 | text += build_wikitext_row(template_name, row) 2641 | text += '\n' 2642 | return text 2643 | 2644 | def row_as_wikitable(row, template_name=None, display_name=None, delim='|'): 2645 | return '{0}-\n{0} {1}\n'.format(delim, ' {} '.format(delim*2).join([v for v in row.values()])) 2646 | 2647 | def row_as_wikirow(row, template_name=None, display_name=None, delim='|'): 2648 | return '{{' + template_name + '|' + delim.join(row) + '}}\n' 2649 | 2650 | def row_as_kv_pairs(row, template_name=None, display_name=None, delim=': '): 2651 | return '\n\t'.join([str(k)+delim+str(v) for k, v in row.items()]) + '\n' 2652 | 2653 | def copy_without_entriesKey(new_row, row): 2654 | for k, v in row.items(): 2655 | if 'EntriesKey' in k: 2656 | continue 2657 | new_row[k[1:]] = v 2658 | 2659 | def db_query_one(query): 2660 | db.execute(query) 2661 | return db.fetchone() 2662 | 2663 | def db_query_all(query): 2664 | db.execute(query) 2665 | return db.fetchall() 2666 | 2667 | def row_factory(cursor, row): 2668 | return {col[0]: row[idx] for idx, col in enumerate(cursor.description)} 2669 | 2670 | DATA_PARSER_PROCESSING = { 2671 | 'AbilityLimitedGroup': ('AbilityLimitedGroup', row_as_wikitext, process_AbilityLimitedGroup), 2672 | 'CharaData': ('Adventurer', row_as_wikitext, 2673 | [('CharaModeData', process_CharaModeData), 2674 | ('CharaData', process_CharaData)]), 2675 | # Must come after CharaData processing 2676 | 'AbilityData': ('Ability', row_as_wikitext, 2677 | [('AbilityShiftGroup', process_AbilityShiftGroup), 2678 | ('AbilityData', process_AbilityData)]), 2679 | 'AbilityCrestBuildupGroup': ('WyrmprintBuildupGroup', row_as_wikitext, process_GenericTemplate), 2680 | 'AbilityCrestBuildupLevel': ('WyrmprintBuildupLevel', row_as_wikitext, process_GenericTemplate), 2681 | 'AbilityCrestRarity': ('WyrmprintRarity', row_as_wikitext, process_GenericTemplate), 2682 | 'BattleRoyalCharaSkinPickup': ('BRSkinDiscount', row_as_wikitable, process_BattleRoyalCharaSkinPickup), 2683 | 'BattleRoyalEventItem': ('Material', row_as_wikitext, process_Material), 2684 | 'BuildEventItem': ('Material', row_as_wikitext, process_Material), 2685 | 'ChainCoAbility': ('ChainCoAbility', row_as_wikitext, [('AbilityData', process_ChainCoAbility)]), 2686 | 'Clb01EventItem': ('Material', row_as_wikitext, process_Material), 2687 | 'CollectEventItem': ('Material', row_as_wikitext, process_Material), 2688 | 'CombatEventItem': ('Material', row_as_wikitext, process_Material), 2689 | 'DmodeCharaLevel': ('KaleidoscapeCharaLevel', row_as_wikitext, process_GenericTemplate), 2690 | 'DmodeServitorPassive': ('KaleidoscapePassive', row_as_wikitext, process_DmodeServitorPassive), 2691 | 'DmodeServitorDungeonLevel': ('KaleidoscapeServitorLevel', row_as_wikitext, process_GenericTemplate), 2692 | 'DmodeServitorPassiveLevel': ('KaleidoscapeServitorPassiveLevel', row_as_wikitext, process_DmodeServitorPassiveLevel), 2693 | 'DmodeStrengthAbility': ('KaleidoscapeStrengthAbility', row_as_wikitext, process_GenericTemplate), 2694 | 'DmodeStrengthParam': ('KaleidoscapeStrengthParam', row_as_wikitext, process_GenericTemplate), 2695 | 'DmodeStrengthSkill': ('KaleidoscapeStrengthSkill', row_as_wikitext, process_GenericTemplate), 2696 | 'EarnEventItem': ('Material', row_as_wikitext, process_Material), 2697 | 'SkillData': ('Skill', row_as_wikitext, process_SkillData), 2698 | 'DragonGiftData': ('Gift', row_as_wikitext, process_DragonGiftData), 2699 | 'DragonLimitBreak': ('DragonLimitBreak', row_as_wikitext, process_GenericTemplate), 2700 | 'ExAbilityData': ('CoAbility', row_as_wikitext, process_ExAbilityData), 2701 | 'EmblemData': ('Epithet', row_as_wikitable, process_EmblemData), 2702 | 'FortPlantData': ('Facility', row_as_wikitext, 2703 | [('FortPlantDetail', process_FortPlantDetail), 2704 | ('FortPlantData', process_FortPlantData)]), 2705 | 'MaterialData': ('Material', row_as_wikitext, process_Material), 2706 | 'RaidEventItem': ('Material', row_as_wikitext, process_Material), 2707 | 'SimpleEventItem': ('Material', row_as_wikitext, process_Material), 2708 | 'MissionAlbumData': ('EndeavorRow', row_as_wikirow, process_MissionData), 2709 | 'MissionDailyData': ('EndeavorRow', row_as_wikirow, process_MissionData), 2710 | 'MissionPeriodData': ('EndeavorRow', row_as_wikirow, process_MissionData), 2711 | 'MissionMainStoryData': ('EndeavorRow', row_as_wikirow, process_MissionData), 2712 | 'MissionNormalData': ('EndeavorRow', row_as_wikirow, process_MissionData), 2713 | 'QuestData': ('QuestDisplay', row_as_wikitext, 2714 | [('QuestData', process_QuestData), 2715 | ('QuestRewardData', process_QuestRewardData), 2716 | ('QuestEvent', process_QuestBonusData), 2717 | ]), 2718 | 'QuestMainMenu': ('CampaignQuestHeader', row_as_wikitext, process_QuestMainMenu), 2719 | 'QuestWallMonthlyReward': ('Mercurial', row_as_wikitable, prcoess_QuestWallMonthlyReward), 2720 | 'QuestWeaponTypePattern': ('QuestWeaponTypePattern', row_as_wikitext, process_QuestWeaponTypePattern), 2721 | 'UnionAbility': ('AffinityBonus', row_as_wikitext, process_UnionAbility), 2722 | 'UseItem': ('Consumable', row_as_wikitext, process_Consumable), 2723 | 'WeaponBodyBuildupGroup': ('WeaponBodyBuildupGroup', row_as_wikitext, process_GenericTemplate), 2724 | 'WeaponBodyBuildupLevel': ('WeaponBodyBuildupLevel', row_as_wikitext, process_GenericTemplate), 2725 | 'WeaponBodyRarity': ('WeaponBodyRarity', row_as_wikitext, process_GenericTemplate), 2726 | 'WeaponPassiveAbility': ('WeaponPassiveAbility', row_as_wikitext, process_GenericTemplate), 2727 | } 2728 | 2729 | # Data that cannot be structured into a simple row->template relationship, and 2730 | # will be parsed into a custom output format determined by each specific function. 2731 | NON_TEMPLATE_PROCESSING = { 2732 | 'BuildEventReward': (process_BuildEventReward,), 2733 | 'CombatEventLocation': (process_CombatEventLocation, 'CombatEventLocationReward'), 2734 | 'RaidEventReward': (process_RaidEventReward,), 2735 | } 2736 | 2737 | # Data that cannot be structured into a simple row->template relationship, and 2738 | # will be parsed into a custom output format determined by each specific function. 2739 | DATABASE_BASED_PROCESSING = { 2740 | 'AbilityCrest': (process_AbilityCrest,), 2741 | 'ActionCondition': (process_ActionCondition,), 2742 | 'BattleRoyale': (process_BattleRoyale,), 2743 | 'BattleRoyalEnemy': (process_BattleRoyalEnemy,), 2744 | 'BattleRoyaleMonthlyRewards': (process_BattleRoyalEventCyclePointReward,), 2745 | 'DmodeAbilityCrest': (process_DmodeAbilityCrest,), 2746 | 'DmodeEnemies': (process_DmodeEnemies,), 2747 | 'DmodeEnemyParams': (process_DmodeEnemyParams,), 2748 | 'DmodeExpeditionFloor': (process_DmodeExpeditionFloor,), 2749 | 'DmodePoint': (process_DmodePoint,), 2750 | 'DmodeStory': (process_DmodeStory,), 2751 | 'DmodeTreasureTrade': (process_DmodeTreasureTrade,), 2752 | 'DmodeWeapon': (process_DmodeWeapon,), 2753 | 'DragonData': (process_Dragon,), 2754 | 'Endeavor_Sets': (process_EndeavorSets,), 2755 | 'Endeavor_Sets-Events': (process_EndeavorSetsEvents,), 2756 | 'LoginBonus': (process_LoginBonusData,), 2757 | 'ManaCircle': (process_ManaCircle,), 2758 | 'MCNodeCost|Data|Unbind': (process_MCNodeCostUnbinds,), 2759 | 'Medals': (process_HonorData,), 2760 | 'NPC': (process_NPC,), 2761 | 'PortraitWyrmprints': (process_TalismanData,), 2762 | 'QuestScoringEnemy': (process_QuestScoringEnemy,), 2763 | 'Stickers': (process_StampData,), 2764 | 'StoryGroup': (process_AlbumStory,), 2765 | 'TimeAttackChallenges': (process_RankingGroupData,), 2766 | 'Weapons': (process_Weapons,), 2767 | } 2768 | 2769 | KV_PROCESSING = { 2770 | 'AbilityData': ('AbilityData', row_as_kv_pairs, process_KeyValues), 2771 | 'CampaignData': ('CampaignData', row_as_kv_pairs, process_KeyValues), 2772 | 'CharaModeData': ('CharaModeData', row_as_kv_pairs, process_KeyValues), 2773 | 'CharaUniqueCombo': ('CharaUniqueCombo', row_as_kv_pairs, process_KeyValues), 2774 | 'CommonActionHitAttribute': ('CommonActionHitAttribute', row_as_kv_pairs, process_KeyValues), 2775 | 'EnemyAbility': ('EnemyAbility', row_as_kv_pairs, process_KeyValues), 2776 | 'EnemyActionHitAttribute': ('EnemyActionHitAttribute', row_as_kv_pairs, process_KeyValues), 2777 | 'EnemyParam': ('EnemyParam', row_as_kv_pairs, process_KeyValues), 2778 | 'EventData': ('EventData', row_as_kv_pairs, process_KeyValues), 2779 | 'EventPassive': ('EventPassive', row_as_kv_pairs, process_KeyValues), 2780 | 'LoginBonusReward': ('LoginBonusReward', row_as_kv_pairs, process_KeyValues), 2781 | 'PlayerAction': ('PlayerAction', row_as_kv_pairs, process_KeyValues), 2782 | 'PlayerActionHitAttribute': ('PlayerActionHitAttribute', row_as_kv_pairs, process_KeyValues), 2783 | 'QuestData': ('QuestData', row_as_kv_pairs, process_KeyValues), 2784 | } 2785 | 2786 | def process(input_dir='./', output_dir='./output-data', ordering_data_path=None, delete_old=False): 2787 | global db, in_dir, ORDERING_DATA, SKILL_DATA_NAMES, EPITHET_RANKS 2788 | if delete_old: 2789 | if os.path.exists(output_dir): 2790 | try: 2791 | rmtree(output_dir) 2792 | print('Deleted old {}'.format(output_dir)) 2793 | except Exception: 2794 | print('Could not delete old {}'.format(output_dir)) 2795 | if ordering_data_path: 2796 | with open(ordering_data_path, 'r') as json_ordering_fp: 2797 | ORDERING_DATA = json.load(json_ordering_fp) 2798 | if not os.path.exists(output_dir): 2799 | os.makedirs(output_dir) 2800 | 2801 | in_dir = input_dir if input_dir[-1] == '/' else input_dir+'/' 2802 | out_dir = output_dir if output_dir[-1] == '/' else output_dir+'/' 2803 | 2804 | # Set up in-memory sql database for all monos 2805 | con = sqlite3.connect(':memory:') 2806 | con.row_factory = row_factory 2807 | db = con.cursor() 2808 | 2809 | for mono in glob.glob(f'{in_dir}*{EXT}'): 2810 | with open(mono, encoding='utf-8', newline='') as file: 2811 | table_name = os.path.basename(mono).replace(EXT, '') 2812 | dialect = 'excel-tab' if 'TextLabel' in table_name else 'excel' 2813 | reader = csv.reader(file, dialect=dialect) 2814 | columns = next(reader) 2815 | rows = [tuple(row) for row in reader] 2816 | placeholder = ','.join(["?" for i in columns]) 2817 | 2818 | db.execute(f'CREATE TABLE {table_name} ({",".join(columns)})') 2819 | try: 2820 | db.executemany(f'INSERT INTO {table_name} VALUES ({placeholder})', rows) 2821 | except: 2822 | print('Error in input csv: {}'.format(table_name)) 2823 | traceback.print_exc() 2824 | con.commit() 2825 | 2826 | TEXT_LABEL_DICT['en'] = csv_as_index(in_dir+TEXT_LABEL+EXT, index='_Id', value_key='_Text', tabs=True) 2827 | try: 2828 | TEXT_LABEL_DICT['jp'] = csv_as_index(in_dir+TEXT_LABEL_JP+EXT, index='_Id', value_key='_Text', tabs=True) 2829 | TEXT_LABEL_DICT['sc'] = csv_as_index(in_dir+TEXT_LABEL_SC+EXT, index='_Id', value_key='_Text', tabs=True) 2830 | TEXT_LABEL_DICT['tc'] = csv_as_index(in_dir+TEXT_LABEL_TC+EXT, index='_Id', value_key='_Text', tabs=True) 2831 | except: 2832 | pass 2833 | SKILL_DATA_NAMES = csv_as_index(in_dir+SKILL_DATA_NAME+EXT, index='_Id', value_key='_Name') 2834 | EPITHET_RANKS = csv_as_index(in_dir+EPITHET_DATA_NAME+EXT, index='_Id', value_key='_Rarity') 2835 | for item_type in ITEM_NAMES: 2836 | ITEM_NAMES[item_type] = csv_as_index(in_dir+item_type+EXT, index='_Id', value_key='_Name') 2837 | # find_fmt_params(in_dir, out_dir) 2838 | 2839 | for data_name, process_params in DATA_PARSER_PROCESSING.items(): 2840 | template, formatter, process_info = process_params 2841 | parser = DataParser(data_name, template, formatter, process_info) 2842 | parser.process() 2843 | parser.emit(out_dir) 2844 | print('Saved {}{}'.format(data_name, EXT)) 2845 | 2846 | for data_name, process_params in DATABASE_BASED_PROCESSING.items(): 2847 | parser = DatabaseBasedParser(data_name, process_params) 2848 | parser.process(out_dir) 2849 | print('Saved {}{}'.format(data_name, EXT)) 2850 | 2851 | for data_name, process_params in NON_TEMPLATE_PROCESSING.items(): 2852 | parser = CustomDataParser(data_name, process_params) 2853 | parser.process(in_dir, out_dir) 2854 | print('Saved {}{}'.format(data_name, EXT)) 2855 | 2856 | kv_out = out_dir+'/kv/' 2857 | if not os.path.exists(kv_out): 2858 | os.makedirs(kv_out) 2859 | for data_name, process_params in KV_PROCESSING.items(): 2860 | template, formatter, process_info = process_params 2861 | parser = DataParser(data_name, template, formatter, process_info) 2862 | parser.process() 2863 | parser.emit(kv_out) 2864 | print('Saved kv/{}{}'.format(data_name, EXT)) 2865 | 2866 | 2867 | if __name__ == '__main__': 2868 | parser = argparse.ArgumentParser(description='Process CSV data into Wikitext.') 2869 | parser.add_argument('-i', type=str, help='directory of input text files', default='./') 2870 | parser.add_argument('-o', type=str, help='directory of output text files (default: ./output-data)', default='./output-data') 2871 | parser.add_argument('-j', type=str, help='path to json file with ordering', default='') 2872 | parser.add_argument('--delete_old', help='delete older output files', dest='delete_old', action='store_true') 2873 | 2874 | args = parser.parse_args() 2875 | process(input_dir=args.i, output_dir=args.o, ordering_data_path=args.j, delete_old=args.delete_old) 2876 | -------------------------------------------------------------------------------- /Process_DL_Images.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import argparse 4 | import os 5 | import re 6 | import copy 7 | from PIL import Image 8 | from shutil import copyfile, rmtree 9 | 10 | ALPHA_TYPES = ('A', 'alpha', 'alphaA8') 11 | YCbCr_TYPES = ('Y', 'Cb', 'Cr') 12 | EXT = '.png' 13 | PORTRAIT_CATEGORIES = ( 14 | '_Full-Screen_Art', 15 | 'Wyrmprint_Images', 16 | 'Dragon_Images', 17 | 'NPC_Portrait_Images', 18 | 'Story_Art', 19 | 'Character_Portrait_Images', 20 | ) 21 | PORTRAIT_SUFFIX = '_portrait' 22 | WYRMPRINT_ALPHA = 'Wyrmprint_Alpha.png' 23 | CATEGORY_REGEX = { 24 | re.compile(r'background/story'): 'Background_Art', 25 | re.compile(r'images/icon/ability/l'): 'Ability_Icons', 26 | re.compile(r'images/icon/status/'): 'Affliction_Icons', 27 | re.compile(r'images/icon/guildemblem/m/(\d+)'): 'Alliance_Crest_Icons', 28 | re.compile(r'images/icon/union/m/'): 'Affinity_Bonus_Icons', 29 | re.compile(r'images/icon/brcharaskin/l/'): 'Alberian_Battle_Skin_Icons', 30 | re.compile(r'images/icon/brcharaskin/m/.*/(.+)$'): 'Small_Alberian_Battle_Skin_Icons', 31 | re.compile(r'images/icon/skill/ingamel'): 'Borderless_Skill_Icons', 32 | re.compile(r'images/story/bookpage/'): 'Book_Background_Art', 33 | re.compile(r'images/icon/queststorycellbg/1'): 'Campaign_Story_Banners', 34 | re.compile(r'images/icon/castlestorycellbg'): 'Castle_Story_Banners', 35 | re.compile(r'images/icon/chara/l'): 'Character_Icon_Images', 36 | re.compile(r'images/icon/chara/m/.+/(.+)$'): 'Small_Character_Icon_Images', 37 | re.compile(r'images/outgame/unitdetail/chara'): 'Character_Portrait_Images', 38 | re.compile(r'images/icon/item/useitem/l/(\d+)'): 'Consumables_Images', 39 | re.compile(r'images/icon/crafttopcellbg'): 'Crafting_Banner_Images', 40 | re.compile(r'images/outgame/eventlocalized/(\d+)/questdetail_defensemap_(\d+)'): 'Defensive_Battle_Map_Images', 41 | re.compile(r'images/icon/item/dragongift/l/(\d+)'): 'Dragon_Gift_Icons', 42 | re.compile(r'images/icon/dragon/l'): 'Dragon_Icons', 43 | re.compile(r'images/icon/dragon/m/.+/(.+)$'): 'Small_Dragon_Icons', 44 | re.compile(r'images/outgame/unitdetail/dragon'): 'Dragon_Images', 45 | re.compile(r'images/outgame/shop/top/dreamselect'): '_Dream_Summon_Banner_Images', 46 | re.compile(r'images/outgame/shop/top/shopspecialpacknotice'): '_Shop_Banner_Images (May be Dream_Summon_Banner_Images)', 47 | re.compile(r'images/icon/element/m/'): 'Element_Icons', 48 | re.compile(r'images/icon/enemyability'): 'Enemy_Ability_Icons', 49 | re.compile(r'images/outgame/eventlocalized/21601'): 'Mercurial_Gauntlet_Images', 50 | re.compile(r'images/outgame/eventlocalized/(\d+)/eventquestmenulist', re.IGNORECASE): 'Event_Banners', 51 | re.compile(r'images/outgame/eventlocalized/(\d+)/eventquestmenutop', re.IGNORECASE): 'Event_Top_Banners', 52 | re.compile(r'images/outgame/eventlocalized/(\d+)/event_prologue_(\d+)', re.IGNORECASE): 'Event_Guide_Images', 53 | re.compile(r'images/outgame/eventlocalized/(\d+)/event_(jikai|defense)_preview_(\d+)', re.IGNORECASE): 'Event_Preview_Images', 54 | re.compile(r'images/outgame/eventlocalized/(\d+)/event_banner_cell_(\d+)', re.IGNORECASE): 'Event_Quest_Banners', 55 | re.compile(r'images/outgame/eventlocalized/(\d+)/event_banner_01', re.IGNORECASE): 'Event_Treasure_Trade_Shop_Banners', 56 | re.compile(r'images/outgame/eventlocalized/(\d+)/event_banner_02', re.IGNORECASE): 'Event_Collection_Rewards_Banners', 57 | re.compile(r'images/outgame/eventlocalized/mainstory/mainstorymenulist_(\d+)', re.IGNORECASE): 'Campaign_Story_Banners', 58 | re.compile(r'images/icon/queststorycellbg/2'): 'Event_Story_Headers', 59 | re.compile(r'images/fort/tw'): 'Facility_Images', 60 | re.compile(r'emotion/eventcg'): '_Full-Screen_Art', 61 | re.compile(r'images/icon/chargegauge'): 'Gauge_Icons', 62 | re.compile(r'images/loading/tips/'): 'Loading_Tips_Images', 63 | re.compile(r'images/outgame/loginbonus'): 'Login_Bonus_Banners', 64 | re.compile(r'images/icon/item/(?:event|materialdata|gather)/l'): 'Material_Icons', 65 | re.compile(r'images/icon/item/(?:event|materialdata|gather)/m/.*/(.+)$'): 'Small_Material_Icons', 66 | re.compile(r'images/icon/manacircle/'): 'MC_Icons', 67 | re.compile(r'images/icon/eventthumbnail/(\d+)'): '_Memory_Icons', 68 | re.compile(r'images/story/tutorial'): 'Misc_Guide_Images', 69 | re.compile(r'images/icon/modechange'): 'Mode_Icons', 70 | re.compile(r'emotion/story/chara/120'): 'NPC_Portrait_Images', 71 | re.compile(r'images/icon/enemy/.*/(.*)$'): 'Enemy_Score_Icons', 72 | re.compile(r'images/icon/item/other/l'): 'Other_Icons', 73 | re.compile(r'images/icon/medal/l/nomal'): 'Medal_Icons', 74 | re.compile(r'images/icon/profile/l/'): 'Profile_Icons', 75 | re.compile(r'images/icon/questthumbnail'): 'Quest_List_Thumbnails', 76 | re.compile(r'images/icon/rarity/'): 'Rarity_Icons', 77 | re.compile(r'images/icon/item/event/s/(\d+)'): 'Resource_Icons', 78 | re.compile(r'images/icon/shoppack/specialshop/wide'): 'Shop_Pack_Icons', 79 | re.compile(r'images/icon/skill/l'): 'Skill_Icons', 80 | re.compile(r'images/icon/form/m/'): 'Slot_Icons', 81 | re.compile(r'images/icon/form/s/.*/(.+)$'): 'Small_Slot_Icons', 82 | re.compile(r'images/icon/skill/m/.*/(.+)$'): 'Small_Skill_Icons', 83 | re.compile(r'images/icon/campaign/'): 'Special_Campaign_Icons', 84 | re.compile(r'images/icon/stamp/l/framed'): 'Sticker_Images', 85 | re.compile(r'emotion/story'): 'Story_Art', 86 | re.compile(r'images/outgame/summon/top/(\d+)/localized/Summon_(Switch|Top)_Banner'): 'Summon_Showcase_Banner_Images', 87 | re.compile(r'images/icon/caption/'): 'UI_Icons', 88 | re.compile(r'images/icon/dmode/l/(\d+)'): 'Kaleidoscape_Item_Icons', 89 | re.compile(r'images/icon/dmodestorycellbg/(\d+)'): 'Kaleidoscape_Story_Headers', 90 | re.compile(r'images/icon/talisman/l/(\d+)'): 'Portrait_Wyrmprint_Icons', 91 | re.compile(r'images/icon/uniquetransform/'): 'Unique_Transformation_Icons', 92 | re.compile(r'images/icon/unitenhanceicon/l/'): 'Upgrade_Icons', 93 | re.compile(r'images/icon/weapon/l'): 'Weapon_Icons', 94 | re.compile(r'images/icon/weapon/m/.+/(.+)$'): 'Small_Weapon_Icons', 95 | re.compile(r'images/icon/amulet/l'): 'Wyrmprint Icons', 96 | re.compile(r'images/icon/amulet/m/.+/(.+)$'): 'Small_Wyrmprint_Icons', 97 | re.compile(r'images/icon/mapicon/'): 'Main_Campaign_Map_Icons', 98 | re.compile(r'images/outgame/unitdetail/amulet'): 'Wyrmprint_Images', 99 | re.compile(r'images/icon/amulet/artistname/'): '_Wyrmprint_Artists', 100 | re.compile(r'images/outgame/quest/areamap/map_area'): '_Map Images', 101 | re.compile(r'images/outgame/quest/areamap/'): '_Map Icons', 102 | 103 | re.compile(r'Btn_'): '__Unused/Button', 104 | re.compile(r'images/ingame/(?:chara|dragon)/(?:bustup|face)'): '__Unused/Closeups', 105 | re.compile(r'emotion/ingame/skillcutin'): '__Unused/Cutin', 106 | re.compile(r'images/outgame/eventlocalized/\d+'): '__Unused/Event', 107 | re.compile(r'emotion/event'): '__Unused/Event/Screen', 108 | re.compile(r'images/icon/(?:chara/wide|dragon/button)'): '__Unused/Icons', 109 | re.compile(r'_Mypage'): '__Unused/Mypage', 110 | re.compile(r'(?:/s/|/m/|/ingamem/)'): '__Unused/Small', 111 | re.compile(r'images/outgame/summon/top/\d+'): '__Unused/Summon', 112 | } 113 | CATEGORY_NAME_FORMATS = { 114 | 'Alliance_Crest_Icons': (lambda number: 'Icon Alliance {}'.format(number)), 115 | 'Consumables_Images': (lambda x: 'Consumable_{}'.format(x)), 116 | 'Defensive_Battle_Map_Images': 117 | (lambda event_id, number: 118 | 'DefenseMap EVENT_NAME_{} {}'.format(event_id, number)), 119 | 'Dragon_Gift_Icons': (lambda x: 'Gift_{}'.format(x)), 120 | 'Enemy_Score_Icons': (lambda x: 'EnemyIcon_{}'.format(x)), 121 | 'Kaleidoscape_Item_Icons': (lambda x: 'KaleidoscapeItem_{}'.format(x)), 122 | 'Kaleidoscape_Story_Headers': (lambda x: 'KaleidoscapeStoryHeader_{}'.format(x)), 123 | 'Portrait_Wyrmprint_Icons': (lambda x: 'PortraitWyrmprintIcon_{}'.format(x)), 124 | '_Memory_Icons': (lambda x: 'Memory_Icon_{}'.format(x)), 125 | 126 | 'Event_Banners': 127 | (lambda number: 'Banner EVENT_NAME_{}'.format(number)), 128 | 'Event_Top_Banners': 129 | (lambda number: 'Banner Top EVENT_NAME_{}'.format(number)), 130 | 'Event_Guide_Images': 131 | (lambda event_id, number: 132 | 'EVENT_NAME_{} Prologue {}'.format(event_id, number)), 133 | 'Event_Preview_Images': 134 | (lambda event_id, type, number: 135 | 'EVENT_NAME_{} {} {}'.format(event_id, 'Jikai Preview' if type == 'jikai' else 'Additional', number)), 136 | 'Event_Quest_Banners': 137 | (lambda event_id, number: 'Banner {}_{}'.format(event_id, number)), 138 | 'Event_Treasure_Trade_Shop_Banners': 139 | (lambda event_id: 'EVENT_NAME_{} Banner 01'.format(event_id)), 140 | 'Event_Collection_Rewards_Banners': 141 | (lambda event_id: 'EVENT_NAME_{} Banner 02'.format(event_id)), 142 | 'Resource_Icons': 143 | (lambda number: 'ITEM_NAME_{} Icon'.format(number)), 144 | 'Summon_Showcase_Banner_Images': 145 | (lambda number, type: 146 | 'Banner_Summon_Showcase {}'.format(number) if type=='Switch' else 147 | '{} Summon_Top_Banner'.format(number)), 148 | 'Campaign_Story_Banners': 149 | (lambda number: 'Banner Top Campaign Chapter ' + number), 150 | 151 | # Small suffix 152 | 'Small_Alberian_Battle_Skin_Icons': (lambda x: '{} (Small)'.format(x)), 153 | 'Small_Character_Icon_Images': (lambda x: '{} (Small)'.format(x)), 154 | 'Small_Dragon_Icons': (lambda x: '{} (Small)'.format(x)), 155 | 'Small_Material_Icons': (lambda x: '{} (Small)'.format(x)), 156 | 'Small_Slot_Icons': (lambda x: '{} (Small)'.format(x)), 157 | 'Small_Skill_Icons': (lambda x: '{} (Small)'.format(x)), 158 | 'Small_Weapon_Icons': (lambda x: '{} (Small)'.format(x)), 159 | 'Small_Wyrmprint_Icons': (lambda x: '{} (Small)'.format(x)), 160 | } 161 | 162 | 163 | image_name_pattern_alpha = re.compile(r'(.*?)(_(A|alpha|alphaA8))?$') 164 | image_name_pattern_YCbCr = re.compile(r'(.*?)_(Y|Cb|Cr)$') 165 | def split_image_name(file_name): 166 | # format basename_channel(YCbCr) 167 | matches = image_name_pattern_YCbCr.match(file_name) 168 | if matches: 169 | base_name, _ = matches.groups() 170 | return base_name, 'YCbCr' 171 | # format basename_channel(Alpha) 172 | matches = image_name_pattern_alpha.match(file_name) 173 | if matches: 174 | base_name, _, channel = matches.groups() 175 | channel = 'base' if channel is None else channel 176 | return base_name, channel 177 | else: 178 | return file_name, 'base' 179 | 180 | def merge_image_name(base_name, channel): 181 | image_name = base_name 182 | if channel != 'base': 183 | image_name += '_' + channel 184 | image_name += EXT 185 | return image_name 186 | 187 | def build_image_dict(files): 188 | images = {} 189 | for f in files: 190 | file_name, file_ext = os.path.splitext(f) 191 | if file_ext != EXT or 'parts' in file_name: 192 | continue 193 | base_name, channel = split_image_name(file_name) 194 | if base_name not in images: 195 | images[base_name] = {} 196 | images[base_name][channel] = True 197 | return images 198 | 199 | def filter_image_dict(images): 200 | no_merge = {} 201 | 202 | ref = copy.deepcopy(images) 203 | for base_name in ref: 204 | if len(ref[base_name]) == 1: 205 | no_merge[base_name] = images[base_name] 206 | del images[base_name] 207 | return images, no_merge 208 | 209 | def print_image_dict(images, paths=True): 210 | for d in images: 211 | print(d) 212 | for i in images[d]: 213 | if paths: 214 | for c in images[d][i]: 215 | print('\t', merge_image_name(i, c)) 216 | else: 217 | print(i, '\n\t', images[d][i]) 218 | 219 | def merge_alpha(directory, base_name, alpha_type): 220 | base_img = Image.open('{}/{}'.format(directory, merge_image_name(base_name, 'base'))) 221 | alph_img = Image.open('{}/{}'.format(directory, merge_image_name(base_name, alpha_type))) 222 | 223 | if base_img.size != alph_img.size: 224 | return 225 | try: 226 | r, g, b, _ = base_img.split() 227 | except: 228 | r, g, b = base_img.split() 229 | if alpha_type == 'alphaA8': 230 | _, _, _, a = alph_img.split() 231 | else: 232 | a = alph_img.convert('L') 233 | 234 | return Image.merge("RGBA", (r,g,b,a)) 235 | 236 | def merge_YCbCr(directory, base_name, unique_alpha=False): 237 | Y_img = Image.open('{}/{}'.format(directory, merge_image_name(base_name, 'Y'))) 238 | _, _, _, Y = Y_img.convert('RGBA').split() 239 | Cb = Image.open('{}/{}'.format(directory, merge_image_name(base_name, 'Cb'))).convert('L').resize(Y_img.size, Image.ANTIALIAS) 240 | Cr = Image.open('{}/{}'.format(directory, merge_image_name(base_name, 'Cr'))).convert('L').resize(Y_img.size, Image.ANTIALIAS) 241 | if unique_alpha: 242 | a = Image.open('{}/{}'.format(directory, merge_image_name(base_name, 'alpha'))).convert('L') 243 | elif 'unitdetail/amulet' in directory: 244 | a = Image.open(WYRMPRINT_ALPHA).convert('L') 245 | else: 246 | a = None 247 | if a is not None: 248 | r, g, b = Image.merge("YCbCr", (Y, Cb, Cr)).convert('RGB').split() 249 | return Image.merge("RGBA", (r, g, b, a)) 250 | else: 251 | return Image.merge("YCbCr", (Y, Cb, Cr)).convert('RGB') 252 | 253 | def merge_all_images(current_dir, images): 254 | merged_images = {} 255 | 256 | for i in images: 257 | m = {} 258 | if 'base' in images[i]: 259 | for alpha in ALPHA_TYPES: 260 | if alpha in images[i]: 261 | m['alpha'] = merge_alpha(current_dir, i, alpha) 262 | break 263 | elif 'YCbCr' in images[i]: 264 | try: 265 | m['YCbCr'] = merge_YCbCr(current_dir, i, unique_alpha=('alpha' in images[i])) 266 | except Exception as e: 267 | print(e) 268 | if len(m) > 0: 269 | merged_images[i] = m 270 | 271 | return merged_images 272 | 273 | def match_category(directory, file_name): 274 | file_path = os.path.join(directory, file_name) 275 | for pattern, category in CATEGORY_REGEX.items(): 276 | res = pattern.search(file_path) 277 | if res: 278 | if len(res.groups()) > 0: 279 | return category, CATEGORY_NAME_FORMATS[category](*res.groups()) 280 | return category, None 281 | return '', None 282 | 283 | def create_output_dir(out_dir, make_categories=False): 284 | os.makedirs(out_dir, exist_ok=True) 285 | if make_categories: 286 | for category in CATEGORY_REGEX.values(): 287 | os.makedirs(out_dir + '/' + category, exist_ok=True) 288 | 289 | def delete_empty_subdirectories(directory): 290 | for root, _, files in os.walk(directory, topdown=False): 291 | if not files: 292 | try: 293 | os.rmdir(root) 294 | except: 295 | pass 296 | 297 | def save_merged_images(merged_images, current_dir, out_dir): 298 | for i in merged_images: 299 | for t in merged_images[i]: 300 | img = merged_images[i][t] 301 | category, name = match_category(current_dir, i) 302 | img_name = i 303 | if name is not None: 304 | img_name = name 305 | 306 | if category: 307 | if category in PORTRAIT_CATEGORIES: 308 | img_name += PORTRAIT_SUFFIX 309 | save_path = '{}/{}/{}'.format(out_dir, category, img_name) 310 | else: 311 | save_path = os.path.join(out_dir, '_uncategorized', current_dir, img_name) 312 | os.makedirs(os.path.dirname(save_path), exist_ok=True) 313 | 314 | while os.path.exists(save_path): 315 | save_path += '#' 316 | img.save(save_path + EXT) 317 | 318 | 319 | def copy_not_merged_images(not_merged, current_dir, input_dir, out_dir): 320 | relative_dir = current_dir.replace(input_dir, '') 321 | if os.path.isabs(relative_dir): 322 | relative_dir = relative_dir[1:] 323 | for i in not_merged: 324 | for c in not_merged[i]: 325 | category, name = match_category(current_dir, i) 326 | if name is not None: 327 | img_name = name + EXT 328 | else: 329 | img_name = merge_image_name(i, c) 330 | 331 | if category: 332 | if category in PORTRAIT_CATEGORIES: 333 | img_name = img_name.replace(EXT, PORTRAIT_SUFFIX + EXT) 334 | elif 'Borderless' in category: 335 | img_name = img_name.replace(EXT, ' Borderless' + EXT) 336 | target_path = os.path.join(out_dir, category, img_name) 337 | else: 338 | target_path = os.path.join(out_dir, '_uncategorized', relative_dir, img_name) 339 | os.makedirs(os.path.dirname(target_path), exist_ok=True) 340 | 341 | while os.path.exists(target_path): 342 | target_path = '{}#{}'.format(target_path.replace(EXT, ''), EXT) 343 | copyfile(os.path.join(current_dir, merge_image_name(i, c)), target_path) 344 | 345 | 346 | def process(input_dir='./', output_dir='./output-img', wyrmprint_alpha_path='Wyrmprint_Alpha.png', delete_old=False): 347 | global WYRMPRINT_ALPHA 348 | if delete_old: 349 | if os.path.exists(output_dir): 350 | try: 351 | rmtree(output_dir) 352 | print('Deleted old {}\n'.format(output_dir)) 353 | except Exception: 354 | print('Could not delete old {}\n'.format(output_dir)) 355 | 356 | create_output_dir(output_dir, make_categories=True) 357 | WYRMPRINT_ALPHA = wyrmprint_alpha_path 358 | overall_not_merged = {} 359 | 360 | for root, unused_subdirs, files, in os.walk(input_dir): 361 | if not len(files): 362 | continue 363 | images = build_image_dict(files) 364 | images, not_merged = filter_image_dict(images) 365 | 366 | merged = merge_all_images(root, images) 367 | 368 | current_dir = root.replace(input_dir, '') 369 | if os.path.isabs(current_dir): 370 | current_dir = current_dir[1:] 371 | save_merged_images(merged, current_dir, output_dir) 372 | copy_not_merged_images(not_merged, root, input_dir, output_dir) 373 | overall_not_merged[root] = not_merged 374 | 375 | delete_empty_subdirectories(output_dir) 376 | print('\nThe following images were copied to {} without merging:'.format(output_dir)) 377 | print_image_dict(overall_not_merged) 378 | 379 | 380 | if __name__ == '__main__': 381 | parser = argparse.ArgumentParser(description='Merge alpha and YCbCr images.') 382 | parser.add_argument('-i', type=str, help='directory of input images', default='./') 383 | parser.add_argument('-o', type=str, help='directory of output images (default: ./output-img)', default='./output-img') 384 | parser.add_argument('--delete_old', help='delete older output files', dest='delete_old', action='store_true') 385 | parser.add_argument('-wpa', type=str, help='path to Wyrmprint_Alpha.png.', default='Wyrmprint_Alpha.png') 386 | 387 | args = parser.parse_args() 388 | process(input_dir=args.i, output_dir=args.o, wyrmprint_alpha_path=args.wpa, delete_old=args.delete_old) 389 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dragalia-wiki-scripts 2 | 3 | ## Requirements 4 | * Python 3 5 | * Pillow (https://pypi.org/project/Pillow/) 6 | 7 | ## Example usage 8 | ``` 9 | Process_DL_Images.py -i -o 10 | ``` 11 | The input_folder is expected to have images in the same folder structure described by the game manifest, in order to properly auto-categorize and alpha-merge images. 12 | 13 | To remove the old output folder before processing: 14 | ``` 15 | Process_DL_Images.py -i -o --delete_old 16 | ``` 17 | 18 | ### Enemy Data parsing 19 | The input_folder is expected to contain the CSV-form master Monos from the game dump. 20 | ``` 21 | Enemy_Parser.py -i -o 22 | ``` 23 | -------------------------------------------------------------------------------- /Wyrmprint_Alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dl-stuff/dl-wiki-scripts/56ffab0a971ae0b3651baf42ce9ab1dff47fb644/Wyrmprint_Alpha.png -------------------------------------------------------------------------------- /duplicate_page.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | duplicate_page.py 5 | MediaWiki API 6 | """ 7 | 8 | import requests 9 | import time 10 | import io 11 | 12 | S = requests.Session() 13 | URL = "https://dragalialost.gamepedia.com/api.php" 14 | 15 | # Retrieve login token first 16 | PARAMS_0 = { 17 | 'action': "query", 18 | 'meta': "tokens", 19 | 'type': "login", 20 | 'format': "json" 21 | } 22 | 23 | R = S.get(url=URL, params=PARAMS_0) 24 | DATA = R.json() 25 | 26 | LOGIN_TOKEN = DATA['query']['tokens']['logintoken'] 27 | 28 | print(LOGIN_TOKEN) 29 | 30 | PARAMS_1 = { 31 | 'action': "login", 32 | 'lgname': "BOT NAME", 33 | 'lgpassword': "BOT PASSWORD", 34 | 'lgtoken': LOGIN_TOKEN, 35 | 'format': "json" 36 | } 37 | 38 | R = S.post(URL, data=PARAMS_1) 39 | DATA = R.json() 40 | 41 | print(DATA) 42 | 43 | PARAMS_2 = { 44 | "action": "query", 45 | "meta": "tokens", 46 | "format": "json" 47 | } 48 | 49 | R = S.get(url=URL, params=PARAMS_2) 50 | DATA = R.json() 51 | 52 | CSRF_TOKEN = DATA['query']['tokens']['csrftoken'] 53 | 54 | queryPARAMS = { 55 | 'action': "query", 56 | 'prop': "revisions", 57 | 'rvslots': "*", 58 | 'rvprop': "content", 59 | 'formatversion': "2", 60 | 'format': "json" 61 | } 62 | 63 | editPARAMS = { 64 | 'action': "edit", 65 | 'summary': "Creating archived page", 66 | 'token': CSRF_TOKEN, 67 | 'bot': "1", 68 | 'format': "json", 69 | 'createonly': "1" 70 | } 71 | 72 | 73 | def archive_group(filepath, wikipath): 74 | with io.open(filepath, 'rt', encoding='utf8') as file: 75 | for line in file: 76 | old_title = line.replace("\n", "") 77 | queryPARAMS['titles'] = old_title 78 | print("Fetching ", old_title) 79 | json = S.get(url=URL, params=queryPARAMS) 80 | archive_data = json.json() 81 | pages = archive_data['query']["pages"] 82 | content = pages[0]['revisions'][0]['slots']['main']['content'] 83 | new_title = wikipath.format(old_title) 84 | editPARAMS['title'] = new_title 85 | editPARAMS['text'] = content 86 | print("Posting ", new_title) 87 | json = S.post(URL, data=editPARAMS) 88 | archive_data = json.json() 89 | print(archive_data) 90 | time.sleep(5) 91 | 92 | 93 | archive_group("C:/Users/Canim/Documents/Weapon List.txt", "Weapons/Archive/Version 1.23.1/{}") 94 | archive_group("C:/Users/Canim/Documents/Wyrmprint List.txt", "Wyrmprints/Archive/Version 1.23.1/{}") 95 | 96 | # Step 4: Send a POST request to logout 97 | PARAMS_3 = { 98 | "action": "logout", 99 | "token": CSRF_TOKEN, 100 | "format": "json" 101 | } 102 | 103 | R = S.post(URL, data=PARAMS_3) 104 | DATA = R.json() 105 | 106 | print(DATA) 107 | --------------------------------------------------------------------------------