3
0
mirror of synced 2024-12-18 10:15:58 +01:00
popnhax_tools/omnimix/popndll.py

371 lines
14 KiB
Python
Raw Normal View History

2023-07-24 23:48:07 +02:00
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