From fae527ec01574e707f72fbca8be26f983b350a5c Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Sun, 10 Dec 2023 23:17:06 +1030 Subject: [PATCH] Add Item Description loading Basic Dialog macro expansion added Handle rstripped tsv lines safely --- data/string_blocks.tsv | 3 +- scripts/loaders/StringLoader.gd | 139 ++++++++++++++++------------- scripts/loaders/snes/ff5_dialog.gd | 112 +++++++++++++++++++++++ scripts/managers/Common.gd | 7 +- widgets/PartyMenu.gd | 13 ++- widgets/PartyMenu.tscn | 5 +- 6 files changed, 212 insertions(+), 67 deletions(-) create mode 100644 scripts/loaders/snes/ff5_dialog.gd diff --git a/data/string_blocks.tsv b/data/string_blocks.tsv index e754ccf..d50ae7c 100644 --- a/data/string_blocks.tsv +++ b/data/string_blocks.tsv @@ -2,9 +2,10 @@ name num_entries address snes_address bytes snes_bytes rpge_ptr_offset snes_ptr_ ability_names 33 0x116200 8 battle_commands 96 0x201150 0x115800 7 5 character_names 5 0x115500 6 -dialogue 0x900 0x2013F0 0x082220 3 2 0x000000 0x0A0000 True +dialogue 0x900 0x2013F0 0x082220 3 2 0x000000 0x0A0000 True enemy_names 384 0x200050 0x105C00 10 8 items 0x100 0x111380 9 +item_descriptions 128 0x114000 2 0x110000 0x110000 True True job_names 22 0x115600 8 job_and_ability_descs 133 0x117140 2 0x110000 0x110000 True magics 87 0x111C80 6 diff --git a/scripts/loaders/StringLoader.gd b/scripts/loaders/StringLoader.gd index 8ff9efc..bc8f966 100644 --- a/scripts/loaders/StringLoader.gd +++ b/scripts/loaders/StringLoader.gd @@ -1,4 +1,5 @@ extends Node +const ff5_dialog := preload('res://scripts/loaders/snes/ff5_dialog.gd') var SNES_block_addresses := Common.load_tsv('res://data/string_blocks.tsv') var tables_raw = {} @@ -28,68 +29,76 @@ func decode_array(array, glyph_table, trim_trailing_whitespace: bool = true) -> output.append(decode_string(s, glyph_table, trim_trailing_whitespace)) return output +func _load_block(block_name: String, buffer: StreamPeerBuffer, is_RPGe: bool = false) -> void: + var block: Dictionary = SNES_block_addresses[block_name] + var glyph_table_small: PoolStringArray = glyph_tables.RPGe_small if is_RPGe else glyph_tables.SNES_small + var glyph_table_dialog: PoolStringArray = glyph_tables.RPGe_dialog if is_RPGe else glyph_tables.SNES_dialog + var glyph_table_kanji: PoolStringArray = glyph_tables.SNES_kanji + var raw_strings := [] + var strings := PoolStringArray() + var num_entries: int = block.num_entries + var l1_width: int = block.bytes + if (not is_RPGe) and block.snes_bytes: + l1_width = block.snes_bytes + var l1_address: int = block.address + if (not is_RPGe) and block.snes_address: + l1_address = block.snes_address + buffer.seek(l1_address) + + var ptr_offset = block.rpge_ptr_offset if is_RPGe else block.snes_ptr_offset + if ptr_offset is int: + var ptrs = PoolIntArray() + match l1_width: + 1: + for i in num_entries: + ptrs.append(ptr_offset + buffer.get_u8()) + 2: + for i in num_entries: + ptrs.append(ptr_offset + buffer.get_u16()) # Bank wrapping + 3: + for i in num_entries: + ptrs.append(ptr_offset + buffer.get_u16() + (buffer.get_u8() << 16)) # Bank wrapping + _: + assert(false, 'Indirect l1_width of %d is not possible' % l1_width) + if block.null_terminated: + for i in num_entries: + buffer.seek(ptrs[i] & 0x3FFFFF) # Bank wrapping + var bytes = PoolByteArray() + while true: + var b = buffer.get_u8() + if b == 0: + break + bytes.append(b) + raw_strings.append(bytes) + else: + for i in num_entries-1: + var size: int = ptrs[i+1] - ptrs[i] + if size > 0: + buffer.seek(ptrs[i] & 0x3FFFFF) # Bank wrapping + raw_strings.append(buffer.get_data(size)[1]) + elif size == 0: + raw_strings.append(PoolByteArray()) + else: + print_debug('String pointer mismatch: "%s" index %d: 0x%06X:0x%06X, effective size of %d bytes' % [block_name, i, ptrs[i], ptrs[i+1], size]) + break + else: + # Get first level of data + for i in num_entries: + raw_strings.append(buffer.get_data(l1_width)[1]) + + # Decode + if block.dialog: + for raw in raw_strings: + strings.append(ff5_dialog.decode_string_dialog(raw, glyph_table_dialog, glyph_table_kanji)) + else: + for raw in raw_strings: + strings.append(decode_string(raw, glyph_table_small)) + self.tables_raw[block_name] = raw_strings + self.tables[block_name] = strings + func load_snes_rom(buffer: StreamPeerBuffer, is_RPGe: bool = false) -> void: for block_name in SNES_block_addresses: - var block: Dictionary = SNES_block_addresses[block_name] - var glyph_table_small: PoolStringArray = glyph_tables.RPGe_small if is_RPGe else glyph_tables.SNES_small - var raw_strings := [] - var strings := PoolStringArray() - var num_entries: int = block.num_entries - var l1_width: int = block.bytes - if (not is_RPGe) and block.snes_bytes: - l1_width = block.snes_bytes - var l1_address: int = block.address - if (not is_RPGe) and block.snes_address: - l1_address = block.snes_address - buffer.seek(l1_address) - - var ptr_offset = block.rpge_ptr_offset if is_RPGe else block.snes_ptr_offset - if ptr_offset is int: - var ptrs = PoolIntArray() - match l1_width: - 1: - for i in num_entries: - ptrs.append((ptr_offset + buffer.get_u8()) & 0x3FFFFF) # Bank wrapping - 2: - for i in num_entries: - ptrs.append((ptr_offset + buffer.get_u16()) & 0x3FFFFF) # Bank wrapping - 3: - for i in num_entries: - ptrs.append((ptr_offset + buffer.get_u16() + (buffer.get_u8() << 16)) & 0x3FFFFF) # Bank wrapping - _: - assert(false, 'Indirect l1_width of %d is not possible' % l1_width) - if block.null_terminated: - for i in num_entries: - buffer.seek(ptrs[i]) - var bytes = PoolByteArray() - while true: - var b = buffer.get_u8() - if b == 0: - break - bytes.append(b) - raw_strings.append(bytes) - else: - for i in num_entries-1: - buffer.seek(ptrs[i]) - var size: int = ptrs[i+1] - ptrs[i] - if size > 0: - raw_strings.append(buffer.get_data(size)[1]) - else: - print_debug('String pointer mismatch: "%s" index %d: 0x%06X:0x%06X, effective size of %d bytes' % [block_name, i, ptrs[i], ptrs[i+1], size]) - raw_strings.append(PoolByteArray()) - else: - # Get first level of data - for i in num_entries: - raw_strings.append(buffer.get_data(l1_width)[1]) - - # Decode - if block.dialog: - pass # TODO - else: - for raw in raw_strings: - strings.append(decode_string(raw, glyph_table_small)) - self.tables_raw[block_name] = raw_strings - self.tables[block_name] = strings + self._load_block(block_name, buffer, is_RPGe) func get_ability_name(id: int) -> String: if id < 128: @@ -107,6 +116,16 @@ func get_ability_desc(id: int) -> String: assert(false, 'ability id %d out of description ranges' % id) return 'ability id %d out of description ranges' % id +func get_inventory_item_desc(id: int) -> String: + var desc_id := 0 + if id < 0x80: + desc_id = RomLoader.snes_data.tbl_weapons[id].description + elif id < 0xE0: + desc_id = RomLoader.snes_data.tbl_armors[id-0x80].description + else: + desc_id = RomLoader.snes_data.tbl_items[id-0xE0].description + return self.tables.item_descriptions[desc_id] + func get_job_name(id: int) -> String: return self.tables.job_names[id] diff --git a/scripts/loaders/snes/ff5_dialog.gd b/scripts/loaders/snes/ff5_dialog.gd new file mode 100644 index 0000000..45b6da6 --- /dev/null +++ b/scripts/loaders/snes/ff5_dialog.gd @@ -0,0 +1,112 @@ +const LITERAL_MACROS := { + # Is 0x00 a wait for input marker? + # 0x01 is linebreak + #0x02: [0x20, 0xBC, 0x82], # 0x02 expands to Bartz's name バッツ. Used for his dialogue in EN, only used for other chars in JP. + 0x03: [0x6E, 0xA8, 0x78, 0x7E, 0xAA], # 0x03 is クリスタル + 0x04: [0x7E, 0x8C, 0x6E, 0xC5, 0xB8], # expands to タイクーン + 0x06: [0x37, 0xBF], # expands to じゃ + 0x07: [0x8D, 0xAB], # expands to いる + 0x08: [0xFF, 0xFF, 0xFF, 0xFF], # 4 spaces + 0x09: [0xFF, 0xFF, 0xFF], # 3 spaces + 0x0A: [0xFF, 0xFF], # 2 spaces + 0x0B: [0x1E12, 0x1E13], # expands to 魔物 + # 0x0C appears to be a pause in delivery - affects previous char + 0x0D: [0x1E24, 0x9B, 0x1E52, 0x1E57], # expands to 風の神殿 + 0x0E: [0x1E04, 0x1E0A], # expands to 飛竜 + # 0x0F - unknown (invisible control char) + # 0x10 is a gil substitution + # 0x11 and 0x12 appear to be item (obtained) substitutions + 0x13: [0x1E07, 0x1E0D], # expands to 封印 + 0x14: [0x76, 0x46, 0xD0], # Cid speaking - シド「 + 0x15: [0x9E, 0x46, 0xD0], # Mid speaking - ミド「 + 0x16: [0x1E05, 0x1E06], # expands to 世界 + # 0x17 uses the next byte for pause duration (seconds?) + 0x18: [0x8E, 0x6E, 0x78, 0x44, 0x78], # expands to エクスデス + 0x19: [0xAC, 0x92, 0xD0], # Lenna speaking - レナ「 + 0x1A: [0x2A, 0xA6, 0x64, 0xD0], # Galuf speaking - ガラフ「 + 0x1B: [0x64, 0xC4, 0xA8, 0x78, 0xD0], # Faris speaking - ファリス「 + 0x1C: [0x6E, 0xAA, 0xAA, 0xD0], # Krile/Kara speaking - クルル「 + 0x1D: [0x91, 0x37, 0x8D, 0x81, 0xBF, 0xB9], # expands to おじいちゃん + # 0x1E-0x1F form kanji with the next byte + # 0x20-0xCC are standard character set + 0xCD: [0xC9, 0xC9], # % (0xCD) to !! + # 0xCE is / + 0xCF: [0xBD, 0x85], # : (0xCF) appears to expand to って + # 0xD0-0xD4 are 「」。AB + 0xD5: [0x1E1B, 0x95, 0x1E08, 0xAD], # expands to 手に入れ + # 0xD6, 0xD7, 0xD8 are YLR + 0xD9: [0x93, 0x8D], # expands to ない + # 0xDA-0xDC are HMP + 0xDD: [0xC7, 0xC7], # S (0xDD) to …… + 0xDE: [0x3F, 0x8D, 0x37, 0xC3, 0x89, 0x25], # C (0xDE) to だいじょうぶ + 0xDF: [0x61, 0xE3], # T (0xDF) to は、 + 0xE0: [0xB9, 0x3F], # expands to んだ + 0xE1: [0x85, 0x8D], # expands to てい + 0xE2: [0x77, 0x7F], # expands to した + # 0xE3 is 、 + 0xE4: [0x77, 0x85], # ◯ (0xE4) appears to expand to して + # 0xE5 is used for Bartz speaking in JP. This only appears as 『 + 0xE6: [0x91, 0x1E0F, 0x1E03], # F (0xE6) appears to expand to otousan (お父様) + 0xE7: [0xC9, 0xCB], # °C (0xE7) to !? - yes this is the wrong order interrobang + 0xE8: [0x45, 0x79], # ・ (0xE8) appears to expand to です + # 0xE9, 0xEA are () + 0xEB: [0x73, 0x9B], # expands to この + 0xEC: [0x9B, 0x1E02], # expands to の力 + 0xED: [0x70, 0xAA, 0x2A, 0xC5], # expands to ケルガー + 0xEE: [0x1E86, 0x1ED7, 0x1E87, 0x1E62, 0x1EA7], # expands to 古代図書舘 (ancient library?) + 0xEF: [0x1E1C, 0xBD, 0x85], # expands to 言って + 0xF0: [0x1E2B, 0x1E0B, 0xD0], # soldier speaking - 兵士「 + 0xF1: [0x6B, 0xA7], # expands to から + 0xF2: [0x1E2C, 0x6A, 0x1E0C], # expands to 火カ船 + 0xF3: [0x1E0E, 0x3D, 0x6F], # expands to 海ぞく + 0xF4: [0x8D, 0x37, 0xC3, 0x89], # expands to いじょう + 0xF5: [0x2B, 0xE3], # expands to が、 + 0xF6: [0x7F, 0x81], # expands to たち + 0xF7: [0x7F, 0x9B], # expands to たの + 0xF8: [0x9D, 0x79], # expands to ます + 0xF9: [0x6F, 0x3F, 0x75, 0x8D], # expands to ください + 0xFA: [0x6B, 0xBD, 0x7F], # expands to かった + 0xFB: [0x7F, 0xC9], # expands to た! + 0xFC: [0x95, 0xE3], # expands to に、 + 0xFD: [0x8D, 0x93, 0x8D, 0x6B, 0xA7, 0x93, 0xB9, 0x3F], # expands to いないからなんだ + 0xFE: [0x1F20, 0x1F38, 0x9B, 0x61, 0x35, 0x9D], # expands to 次元のはざま + # 0xFF is space +} + +static func decode_string_dialog(bytes, glyph_table: PoolStringArray, glyph_table_kanji: PoolStringArray, variables:={}, trim_trailing_whitespace: bool = true) -> String: + # Dialog requires multibyte handling including kanji and other macros + var expanded_bytes = PoolIntArray() # Kanji are 16bit codes + var output = '' + var i := 0 + var end := len(bytes) + while i < end: + var c: int = bytes[i] + match c: + # 0x17 uses the next byte for pause duration (seconds?) + 0x17, 0x1E, 0x1F: # Kanji + i += 1 + expanded_bytes.append((c << 8) + bytes[i]) + _: + expanded_bytes.append_array(LITERAL_MACROS.get(c, [c])) + i += 1 + for c in expanded_bytes: + match c: + 0x02: + output += variables.get('bartz_name', 'Bartz') + 0x0C: # 0x0C appears to be a pause in delivery - affects previous char + pass + 0x0F: # 0x0F - unknown (invisible control char) + pass + 0x10: # 0x10 is a gil substitution + output += '%d' % variables.get('gil_amount', 0) + 0x11, 0x12: # 0x11 and 0x12 appear to be item (obtained) substitutions + output += variables.get('item_name', 'unk_item') + _: + if c < 0x1700: + output += glyph_table[c] + elif c < 0x1E00: + var delay = c - 0x1700 + pass # TODO: work out a way to signal dialog delivery pauses + else: + output += glyph_table_kanji[c - 0x1E00] + return output.trim_suffix(' ') if trim_trailing_whitespace else output diff --git a/scripts/managers/Common.gd b/scripts/managers/Common.gd index 53261ca..2e4195d 100644 --- a/scripts/managers/Common.gd +++ b/scripts/managers/Common.gd @@ -80,7 +80,7 @@ static func load_glyph_table(filename: String) -> PoolStringArray: OK: var l: int = file.get_len() while file.get_position() < l: - output.append(file.get_line()) + output.append(file.get_line().replace('\\n', '\n').replace('’', "'").replace('”', '"')) # TODO: Fix font to accept the special quote marks var error: print_debug('Failed to open glyph table "%s" - %d' % [filename, error]) return output @@ -145,6 +145,9 @@ static func limited_eval(token: String): var hex := token.hex_to_int() if hex > 0: return hex + elif token.substr(2).is_valid_integer(): + # Special case for 0x000000... + return 0 # Try int literal if token.is_valid_integer(): return int(token) @@ -174,6 +177,8 @@ static func load_tsv(filename: String, delimiter: String = '\t') -> Dictionary: for i in range(1, n): if line.size() > i: entry[headers[i]] = limited_eval(line[i]) + else: + entry[headers[i]] = limited_eval('') output[line[0]] = entry return output print_debug(error) diff --git a/widgets/PartyMenu.gd b/widgets/PartyMenu.gd index 9ceec28..c1167e4 100644 --- a/widgets/PartyMenu.gd +++ b/widgets/PartyMenu.gd @@ -3,9 +3,12 @@ extends Panel onready var sidebar_classic := $'%sidebar_classic' onready var sbc_menu_item_containers := [$'%menu_items_1', $'%menu_items_2'] +onready var lbl_time := $'%lbl_time' +onready var lbl_gilcount := $'%lbl_gilcount' onready var characters_container := $'%characters_container' onready var items_menu := $'%items_menu' onready var items_container := $'%items_container' +onready var lbl_item_description := $'%lbl_item_description' enum Submenu {PARTY, ITEMS} const str2submenu := { @@ -26,12 +29,12 @@ func update_labels(data: Dictionary): p.visible = not characters[i].is_absent p.update_labels(data, i) - $'%lbl_time'.text = Common.game_time_frames_to_hhmm(data.game_time_frames) - $'%lbl_gilcount'.text = '%d' % data.current_gil + lbl_time.text = Common.game_time_frames_to_hhmm(data.game_time_frames) + lbl_gilcount.text = '%d' % data.current_gil + # Populate inventory for child in items_container.get_children(): child.queue_free() - for i in 256: var item_id = data.inventory_item_ids[i] var item_qty = data.inventory_item_qtys[i] @@ -42,6 +45,10 @@ func update_labels(data: Dictionary): if item_qty > 0: lbl_item_name.text = item_name lbl_item_qty.text = 'x%d'%item_qty + var desc = StringLoader.get_inventory_item_desc(item_id) + if desc: + lbl_item_name.mouse_filter = MOUSE_FILTER_PASS + lbl_item_name.hint_tooltip = desc items_container.add_child(lbl_item_name) items_container.add_child(lbl_item_qty) diff --git a/widgets/PartyMenu.tscn b/widgets/PartyMenu.tscn index 6e92579..cb5c585 100644 --- a/widgets/PartyMenu.tscn +++ b/widgets/PartyMenu.tscn @@ -295,7 +295,6 @@ rect_min_size = Vector2( 320, 240 ) [node name="items_menu" type="Control" parent="submenus"] unique_name_in_owner = true -visible = false anchor_right = 1.0 anchor_bottom = 1.0 @@ -355,7 +354,8 @@ anchor_right = 1.0 margin_top = 22.0 margin_bottom = 66.0 -[node name="Label" type="Label" parent="submenus/items_menu/itemdesc"] +[node name="lbl_item_description" type="Label" parent="submenus/items_menu/itemdesc"] +unique_name_in_owner = true margin_left = 4.0 margin_top = 4.0 margin_right = 316.0 @@ -381,6 +381,7 @@ columns = 4 [node name="characters_container" type="VBoxContainer" parent="submenus"] unique_name_in_owner = true +visible = false anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 8.0