3
0
mirror of synced 2024-11-27 17:00:55 +01:00
popnhax_tools/omnimix/popndll.py
2023-07-24 23:48:07 +02:00

371 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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