371 lines
14 KiB
Python
371 lines
14 KiB
Python
import copy
|
||
|
||
import pefile
|
||
|
||
from lxml.etree import parse as etree_parse
|
||
from lxml.builder import E
|
||
|
||
CHART_MASKS = [0x00080000, 0, 0x01000000, 0x02000000, 0, 0x04000000, None]
|
||
|
||
|
||
def is_placeholder_song(c):
|
||
return c['fw_genre'] == c['fw_title'] == c['fw_artist'] == c['genre'] == c['title'] == c['artist'] == '‐'
|
||
|
||
|
||
def is_placeholder_chara(c):
|
||
return c['flags'] & 3 != 0
|
||
|
||
|
||
def translate_konami_string(data):
|
||
replace_str = [
|
||
# ["鶉", "ó"],
|
||
# ["鶇", "ö"],
|
||
# ["圈", "é"],
|
||
# ["鶫", "²"],
|
||
# ["鵝", "7"],
|
||
# ["囿", "♡"],
|
||
# ["囂", "♡"],
|
||
# ["鵑", ""],
|
||
# ["鶚", "㊙"],
|
||
# ["鵺", "Ü"],
|
||
# ["圄", "à"],
|
||
# ["圖", "ţ"],
|
||
# ["鵤", "Ä"],
|
||
# ["塔e", "∮テ"],
|
||
# ["囎", ":"],
|
||
# ["鵙", "ǝ"],
|
||
# ["圉", "ä"],
|
||
]
|
||
|
||
strdata = data.decode('cp932', errors="ignore").strip('\0')
|
||
|
||
for c in replace_str:
|
||
strdata = strdata.replace(c[0], c[1])
|
||
|
||
return strdata
|
||
|
||
|
||
def calculate_struct_len(data_struct):
|
||
return sum([data_struct[k][0] * data_struct[k][1] for k in data_struct])
|
||
|
||
|
||
def read_struct_data(pe, data_struct, data, index):
|
||
data_struct_len = calculate_struct_len(data_struct)
|
||
|
||
offset = index * data_struct_len
|
||
|
||
output = {
|
||
'_id': index,
|
||
'_type': data_struct,
|
||
}
|
||
|
||
idx = 0
|
||
for k in data_struct:
|
||
dsize, dcount, is_ptr = data_struct[k][:3]
|
||
|
||
if dcount > 1:
|
||
output[k] = []
|
||
|
||
for i in range(dcount):
|
||
if 'string' in data_struct[k]:
|
||
cur_data = translate_konami_string(data[offset+idx:offset+idx+dsize])
|
||
else:
|
||
cur_data = int.from_bytes(data[offset+idx:offset+idx+dsize], 'little', signed='signed' in data_struct[k])
|
||
|
||
if 'ignore' in data_struct[k] and cur_data != 0:
|
||
print(index)
|
||
print("Field set to be ignored, but it has non-zero data")
|
||
|
||
import hexdump
|
||
hexdump.hexdump(data[offset:offset+data_struct_len])
|
||
|
||
exit(1)
|
||
|
||
if is_ptr:
|
||
# Remove image base (0x10000000) from pointer address and get string data
|
||
cur_data = translate_konami_string(pe.get_string_at_rva(rva=cur_data - 0x10000000))
|
||
|
||
if dcount == 1:
|
||
output[k] = cur_data
|
||
|
||
else:
|
||
output[k].append(cur_data)
|
||
|
||
idx += dsize
|
||
|
||
if 'ignore' in data_struct[k] or 'ignore_silent' in data_struct[k]:
|
||
del output[k]
|
||
|
||
return output
|
||
|
||
|
||
def get_type(struct, k):
|
||
if 'charts' in struct[k]:
|
||
return "charts"
|
||
|
||
if 'string' in struct[k] or struct[k][2] == True:
|
||
return "str"
|
||
|
||
size = struct[k][0] * 8
|
||
sign = "s" if 'signed' in struct[k] else "u"
|
||
return "%s%d" % (sign, size)
|
||
|
||
|
||
def serialize_data_charts(x):
|
||
output = []
|
||
|
||
for chart_idx, chart in enumerate(x):
|
||
if chart == 0:
|
||
continue
|
||
|
||
idx = chart.get('_idx', str(chart_idx))
|
||
|
||
if '_idx' in chart:
|
||
del chart['_idx']
|
||
|
||
output.append(E('chart', *serialize_data(chart), idx=idx))
|
||
|
||
return output
|
||
|
||
def serialize_data(x):
|
||
ret = []
|
||
|
||
for k in x:
|
||
if k.startswith("_") or x[k] is None:
|
||
continue
|
||
|
||
if get_type(x['_type'], k) in ['charts']:
|
||
ret.append(E(k, *serialize_data_charts(x[k])))
|
||
|
||
elif type(x[k]) in [list, dict] and '_type' in x[k]:
|
||
ret.append(E(k, *serialize_data(x[k])))
|
||
|
||
elif type(x[k]) in [list]:
|
||
ret.append(E(
|
||
k,
|
||
" ".join([str(v) for v in x[k]]) if type(x[k]) in [list] else str(x[k]),
|
||
__type=get_type(x['_type'], k),
|
||
__count=str(len(x[k]) if type(x[k]) in [list] else 1),
|
||
))
|
||
|
||
else:
|
||
ret.append(E(
|
||
k,
|
||
" ".join([str(v) for v in x[k]]) if type(x[k]) in [list] else str(x[k]),
|
||
__type=get_type(x['_type'], k),
|
||
))
|
||
|
||
return ret
|
||
|
||
|
||
def parse_database_from_dll(input_dll_filename, input_patch_xml_filename):
|
||
# Format: [size, num, ptr_flag]
|
||
data_struct_song = {
|
||
'fw_genre': [4, 1, True],
|
||
'fw_title': [4, 1, True],
|
||
'fw_artist': [4, 1, True],
|
||
'genre': [4, 1, True],
|
||
'title': [4, 1, True],
|
||
'artist': [4, 1, True],
|
||
'chara1': [2, 1, False],
|
||
'chara2': [2, 1, False],
|
||
'mask': [4, 1, False],
|
||
'folder': [4, 1, False],
|
||
'cs_version': [4, 1, False],
|
||
'categories': [4, 1, False],
|
||
'diffs': [1, 6, False],
|
||
'charts': [2, 7, False, 'charts'],
|
||
'ha': [4, 1, True],
|
||
'chara_x': [4, 1, False], # Hariai positioning it seems
|
||
'chara_y': [4, 1, False], # Hariai positioning it seems
|
||
'unk1': [2, 32, False],
|
||
'display_bpm': [2, 12, False],
|
||
'hold_flags': [1, 8, False],
|
||
}
|
||
|
||
data_struct_file = {
|
||
'folder': [4, 1, True],
|
||
'filename': [4, 1, True],
|
||
'audio_param1': [4, 1, False, 'signed'], # Something relating to volume/pan/etc?
|
||
'audio_param2': [4, 1, False, 'signed'], # Something relating to volume/pan/etc?
|
||
'audio_param3': [4, 1, False, 'signed'], # Something relating to volume/pan/etc?
|
||
'audio_param4': [4, 1, False, 'signed'], # Something relating to volume/pan/etc?
|
||
'file_type': [4, 1, False], # <= 0 is shiri.ifs, <= 5 is shiri_%d.ifs, anything else is shiri_diff.ifs
|
||
'used_keys': [2, 1, False], # Bit field that says what notes were used in the chart
|
||
'pad': [2, 1, False, 'ignore'],
|
||
}
|
||
|
||
data_struct_chara = {
|
||
'chara_id': [4, 1, True],
|
||
'flags': [4, 1, False], # Controls visibility, etc. bit 1 = C_DEL, bit 2 = CPU-only, bit 5 = disabled/off?
|
||
'folder': [4, 1, True],
|
||
'gg': [4, 1, True],
|
||
'cs': [4, 1, True],
|
||
'icon1': [4, 1, True],
|
||
'icon2': [4, 1, True],
|
||
'chara_xw': [2, 1, False], # Some kind of width or x position. If mask in data_struct_song has bit 23 (0x800000) set then this is ignored
|
||
'chara_yh': [2, 1, False], # Some kind of height or y position. If mask in data_struct_song has bit 23 (0x800000) set then this is ignored
|
||
'display_flags': [4, 1, False], # Some kind of bitfield flags.
|
||
# If bit 1 is set then linear = 1
|
||
# If bit 0 is not set then copy (flags2 & 2) into the linear flag field (doesn't have any effect?)
|
||
# If bit 6 (0x20) is set then clipping = 1
|
||
# If bit 6 (0x20) is not set then copy (flags & 0x10) >> 3 into the clipping flag field
|
||
# Bit 8 (0x100) is unused?? Is set for gg_mimi_15a
|
||
'flavor': [2, 1, False, 'signed'],
|
||
'chara_variation_num': [1, 1, False],
|
||
'pad': [1, 1, False, 'ignore'],
|
||
'sort_name': [4, 1, True],
|
||
'disp_name': [4, 1, True],
|
||
'file_type': [4, 1, False], # <= 0 is shiri.ifs, <= 5 is shiri_%d.ifs, anything else is shiri_diff.ifs
|
||
'lapis_shape': [4, 1, False], # non/dia/tear/heart/squ
|
||
'lapis_color': [1, 1, False], # non/blue/pink/red/green/normal/yellow/purple/black
|
||
'pad2': [1, 3, False, 'ignore'],
|
||
'ha': [4, 1, True],
|
||
'catchtext': [4, 1, True],
|
||
'win2_trigger': [2, 1, False, 'signed'], # If played against a specific character ID, it triggers a win 2 animation
|
||
'pad3': [1, 2, False, 'ignore'],
|
||
'game_version': [4, 1, False], # What version this particular style was introduced
|
||
}
|
||
|
||
data_struct_flavors = {
|
||
'phrase1': [13, 1, False, 'string'],
|
||
'phrase2': [13, 1, False, 'string'],
|
||
'phrase3': [13, 1, False, 'string'],
|
||
'phrase4': [13, 1, False, 'string'],
|
||
'phrase5': [13, 1, False, 'string'],
|
||
'phrase6': [13, 1, False, 'string'],
|
||
'pad': [2, 1, False, 'ignore'],
|
||
'birthday': [4, 1, True],
|
||
'chara1_birth_month': [1, 1, False],
|
||
'chara2_birth_month': [1, 1, False],
|
||
'chara3_birth_month': [1, 1, False],
|
||
'chara1_birth_date': [1, 1, False],
|
||
'chara2_birth_date': [1, 1, False],
|
||
'chara3_birth_date': [1, 1, False],
|
||
'style1': [2, 1, False], # Font and other related stylings
|
||
'style2': [2, 1, False], # Font and other related stylings
|
||
'style3': [2, 1, False], # Font and other related stylings
|
||
}
|
||
|
||
data_struct_fontstyle = {
|
||
'fontface': [4, 1, False],
|
||
'color': [4, 1, False],
|
||
'height': [4, 1, False],
|
||
'width': [4, 1, False],
|
||
}
|
||
|
||
# Read XML file
|
||
patch_xml = etree_parse(input_patch_xml_filename)
|
||
music_db_limit = patch_xml.find('limits').find('music').text
|
||
music_db_limit = int(music_db_limit, 16 if music_db_limit.startswith("0x") else 10)
|
||
|
||
chart_table_limit = patch_xml.find('limits').find('chart').text
|
||
chart_table_limit = int(chart_table_limit, 16 if chart_table_limit.startswith("0x") else 10)
|
||
|
||
style_table_limit = patch_xml.find('limits').find('style').text
|
||
style_table_limit = int(style_table_limit, 16 if style_table_limit.startswith("0x") else 10)
|
||
|
||
flavor_table_limit = patch_xml.find('limits').find('flavor').text
|
||
flavor_table_limit = int(flavor_table_limit, 16 if flavor_table_limit.startswith("0x") else 10)
|
||
|
||
chara_table_limit = patch_xml.find('limits').find('chara').text
|
||
chara_table_limit = int(chara_table_limit, 16 if chara_table_limit.startswith("0x") else 10)
|
||
|
||
music_db_addr = int(patch_xml.find('buffer_base_addrs').find('music').text, 16)
|
||
chart_table_addr = int(patch_xml.find('buffer_base_addrs').find('chart').text, 16)
|
||
style_table_addr = int(patch_xml.find('buffer_base_addrs').find('style').text, 16)
|
||
flavor_table_addr = int(patch_xml.find('buffer_base_addrs').find('flavor').text, 16)
|
||
chara_table_addr = int(patch_xml.find('buffer_base_addrs').find('chara').text, 16)
|
||
|
||
# Modified an old one off script for this so I don't feel like refactoring it too much to get rid of these
|
||
music_db_end_addr = (music_db_limit) * calculate_struct_len(data_struct_song) + music_db_addr
|
||
chart_table_end_addr = (chart_table_limit) * calculate_struct_len(data_struct_file) + chart_table_addr
|
||
style_table_end_addr = (style_table_limit) * calculate_struct_len(data_struct_fontstyle) + style_table_addr
|
||
flavor_table_end_addr = (flavor_table_limit) * calculate_struct_len(data_struct_flavors) + flavor_table_addr
|
||
chara_table_end_addr = (chara_table_limit) * calculate_struct_len(data_struct_chara) + chara_table_addr
|
||
|
||
pe = pefile.PE(input_dll_filename, fast_load=True)
|
||
|
||
# Read font style table
|
||
data = pe.get_data(style_table_addr - 0x10000000, style_table_end_addr - style_table_addr)
|
||
fontstyle_table = [read_struct_data(pe, data_struct_fontstyle, data, i) for i in range(len(data) // calculate_struct_len(data_struct_fontstyle))]
|
||
|
||
# Read flavor table
|
||
data = pe.get_data(flavor_table_addr - 0x10000000, flavor_table_end_addr - flavor_table_addr)
|
||
flavor_table = [read_struct_data(pe, data_struct_flavors, data, i) for i in range(len(data) // calculate_struct_len(data_struct_flavors))]
|
||
|
||
for c in flavor_table:
|
||
if c['style2'] == 0:
|
||
c['style2'] = None
|
||
|
||
elif c['style2'] - 11 >= 0:
|
||
c['style2'] = fontstyle_table[c['style2'] - 11]
|
||
|
||
# Read chara table
|
||
data = pe.get_data(chara_table_addr - 0x10000000, chara_table_end_addr - chara_table_addr)
|
||
charadb = [read_struct_data(pe, data_struct_chara, data, i) for i in range(len(data) // calculate_struct_len(data_struct_chara))]
|
||
|
||
data_struct_chara['lapis_shape'].append('string')
|
||
data_struct_chara['lapis_color'].append('string')
|
||
flavors = []
|
||
for c in charadb:
|
||
c['lapis_shape'] = ["", "dia", "tear", "heart", "squ"][c['lapis_shape']]
|
||
c['lapis_color'] = ["", "blue", "pink", "red", "green", "normal", "yellow", "purple", "black"][c['lapis_color']]
|
||
flavors.append(c['flavor'])
|
||
c['flavor'] = flavor_table[c['flavor']] if c['flavor'] >= 0 else None
|
||
|
||
# Read chart/file table
|
||
data = pe.get_data(chart_table_addr - 0x10000000, chart_table_end_addr - chart_table_addr)
|
||
file_lookup = [read_struct_data(pe, data_struct_file, data, i) for i in range(len(data) // calculate_struct_len(data_struct_file))]
|
||
|
||
# Read music database
|
||
data = pe.get_data(music_db_addr - 0x10000000, music_db_end_addr - music_db_addr)
|
||
musicdb = [read_struct_data(pe, data_struct_song, data, i) for i in range(len(data) // calculate_struct_len(data_struct_song))]
|
||
|
||
# Add connections to other tables
|
||
data_struct_song['chara1'].append('string')
|
||
data_struct_song['chara2'].append('string')
|
||
for c in musicdb:
|
||
c['_type'] = copy.deepcopy(c['_type'])
|
||
|
||
if not is_placeholder_song(c):
|
||
charts = []
|
||
|
||
for chart_idx, idx in enumerate(c['charts']):
|
||
if CHART_MASKS[chart_idx] is not None and (CHART_MASKS[chart_idx] == 0 or c['mask'] & CHART_MASKS[chart_idx] != 0):
|
||
charts.append(copy.deepcopy(file_lookup[idx]))
|
||
charts[-1]['_type'] = copy.deepcopy(charts[-1]['_type'])
|
||
charts[-1]['_type']['diff'] = [1, 1, False]
|
||
charts[-1]['_type']['hold_flag'] = [1, 1, False]
|
||
|
||
charts[-1]['diff'] = c['diffs'][chart_idx]
|
||
charts[-1]['_id'] = chart_idx
|
||
charts[-1]['_idx'] = ['ep', 'np', 'hp', 'op', 'bp_n', 'bp_h'][chart_idx]
|
||
charts[-1]['hold_flag'] = c['hold_flags'][chart_idx]
|
||
|
||
else:
|
||
charts.append(0)
|
||
|
||
c['charts'] = charts
|
||
|
||
# Remove chart mask flags because they'll be added later in popnmusichax based on the charts available
|
||
mask_full = sum([x for x in CHART_MASKS if x is not None])
|
||
c['mask'] = c['mask'] & ~mask_full
|
||
|
||
for k in ['diffs', 'hold_flags']:
|
||
if k in c['_type']:
|
||
del c['_type'][k]
|
||
|
||
if k in c:
|
||
del c[k]
|
||
|
||
c['chara1'] = charadb[c['chara1']]['chara_id'] if c['chara1'] != 0 else 0
|
||
c['chara2'] = charadb[c['chara2']]['chara_id'] if c['chara2'] != 0 else 0
|
||
|
||
database = {
|
||
'musicdb': musicdb,
|
||
'charadb': charadb
|
||
}
|
||
|
||
return database
|