From 334545fcc4f5d6e0b83066e4c6d1bc4e694f2e5a Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Fri, 5 Jul 2024 02:11:24 +0930 Subject: [PATCH] Add WorldMap dynamic regional changes --- README.md | 4 +- data/5/addresses_SNES_PSX.tsv | 8 +++- data/5/structs/SNES.tsv | 7 ++++ scripts/loaders/MapLoader.gd | 79 +++++++++++++++++++++++++++++++++++ scripts/struct.gd | 2 +- test/worldmap_system.gd | 79 +++++++++++++++++++++++++++++++---- test/worldmap_system.tscn | 1 + 7 files changed, 168 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9f2c786..6dd1e2c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ I have mostly solved parsing of SNES menus in a sister project, however there ar #### World Maps (Fields?) - [x] Tiles - [x] Tilemaps -- [ ] Dynamic changes (e.g. meteors, breaking seals, sinking island, pirate cave, voids) (might hardcode these later) +- [x] Dynamic changes (e.g. meteors, breaking seals, sinking island, pirate cave, voids) (didn't have to hardcode it! just need to hook it into the script system later) - [ ] Pathing (Data is in place, just needs moving around with collisions) - [ ] Mode 7 Effects (...might hardcode these later) #### Dungeon/Town/Zone Maps (idk what accepted terminology is) @@ -64,7 +64,7 @@ This will be an interpreter, I am not hardcoding the thousands of scripts. ### Sound System - [x] Instrument samples -- [x] Basic Music playing (currently loops correctly, but tuning is a bit off) +- [x] Basic Music playing (currently loops correctly, but tuning is a bit off on looped samples, possible Godot bug to investigate) - [ ] DSP stuff including ADSR envelopes # What's different? diff --git a/data/5/addresses_SNES_PSX.tsv b/data/5/addresses_SNES_PSX.tsv index 37fb3eb..e710e52 100644 --- a/data/5/addresses_SNES_PSX.tsv +++ b/data/5/addresses_SNES_PSX.tsv @@ -1,4 +1,10 @@ Label SNES PSX_file PSX_offset format Comment +ptrs_worldmap_event_replacements 0x006ABD 6 of u16 5 worlds + the end address +worldmap_event_replacements.0 0x00726C 61 of WorldMapEventReplacement hardcoded of above +worldmap_event_replacements.1 0x0073DA 53 of WorldMapEventReplacement hardcoded of above +worldmap_event_replacements.2 0x007518 106 of WorldMapEventReplacement hardcoded of above +worldmap_event_replacements.3 0x007794 1 of WorldMapEventReplacement hardcoded of above +worldmap_event_replacements.4 0x00779A 98 of WorldMapEventReplacement hardcoded of above character_battle_sprite_stone_palette 0x00F807 N/A N/A 16 of ColorBGR555 Also 0x199835 character_battle_sprite_disabled_palette 0x00F867 /mnu/memsave.bin 0x000034 16 of ColorBGR555 locations_bg_palettes 0x03BB00 /nar/ff5_binx.bin 0x03BF80 43 of 128 of ColorBGR555 @@ -19,7 +25,7 @@ worldmap_compressed_tilesets 0x070000 tilesets 0 up to 0x434 worldmap_compressed_tilesets2 0x080000 tilesets 0x434 up to 0x500 ptrs_jp_speech 0x082220 2160 of u16 ptrs_extended_event_data 0x083320 1940 of u24 -extended_event_data 0x0849DF See above for addresses +extended_event_data 0x0849DC See above for addresses jp_speech 0x0A0000 See 0x082220 for offsets ptrs_tilemaps 0x0B0000 328 of u16 tilemaps 0x0B0290 See above for offsets diff --git a/data/5/structs/SNES.tsv b/data/5/structs/SNES.tsv index d0dfea4..ccb7032 100644 --- a/data/5/structs/SNES.tsv +++ b/data/5/structs/SNES.tsv @@ -197,3 +197,10 @@ u8 palette_id u8 23 u8 24 u8 music_id + +struct WorldMapEventReplacement +u8 y +u8 x +u8 num_bytes +u8 event_flag # Add 0x1D0 to this for actual event flag +u16 ptr_bytes # Read num_bytes from this address (0xC0 bank implied, so just from the start of the ROM) diff --git a/scripts/loaders/MapLoader.gd b/scripts/loaders/MapLoader.gd index 2fec119..a3733ab 100644 --- a/scripts/loaders/MapLoader.gd +++ b/scripts/loaders/MapLoader.gd @@ -66,8 +66,32 @@ class WorldMap: const tile_width := block_width * 2 const tile_height := block_height * 2 var blockmap: PoolByteArray + var blockmap_original: PoolByteArray var block_tile_ids: PoolByteArray var block_pathing: PoolIntArray + var event_replacements: Dictionary # Dictionary[Array[EventReplacementRegion]] + + class EventReplacementRegion: + var start_y: int + var rows: Array # Array[Array[int, PoolByteArray]] + func _init() -> void: + self.rows = [] + + func get_min_x() -> int: + var min_x = block_width + for row in rows: + var x = row[0] + if x < min_x: + min_x = x + return min_x + + func get_max_x() -> int: + var max_x = 0 + for row in rows: + var x = row[0] + if x > max_x: + max_x = x + return max_x func get_block_tiles(id: int) -> PoolByteArray: var i = id * 4 @@ -92,6 +116,59 @@ class WorldMap: image.create_from_data(tile_width, tile_height, false, SpriteLoader.INDEX_FORMAT, data) return image + func apply_event_region_replacement(region: WorldMap.EventReplacementRegion): + # Apply a single event region replacement + var y := region.start_y + var y_offset = y * block_width + for row in region.rows: + var x: int = row[0] + var blocks: PoolByteArray = row[1] + var offset = y_offset + x + var new_blockmap := blocks + # A simple array splice shows the API weakness of GDScript's PoolByteArrays, sadly + if offset > 0: # Prepend behind if non-empty (weakness of PoolXArray::subarray) + new_blockmap = self.blockmap.subarray(0, offset-1) + new_blockmap + if len(new_blockmap) < len(self.blockmap): # Append behind if non-empty (weakness of PoolXArray::subarray) + new_blockmap = new_blockmap + self.blockmap.subarray(len(new_blockmap), -1) + self.blockmap = new_blockmap + y_offset += block_width + + func apply_event_replacements(event_flags): # Any integer array is fine + for event_flag in event_flags: + if self.event_replacements.has(event_flag): + for region in self.event_replacements[event_flag]: + self.apply_event_region_replacement(region) + + func init_event_replacements(_data: Dictionary, buffer: StreamPeerBuffer, worldmap_event_replacements: Array): + # Turn deserialized WorldMapEventReplacement structs into EventReplacementRegions + self.event_replacements = {} + var last_event_flag: int = -1 + var last_y: int = -1 + var region := WorldMap.EventReplacementRegion.new() + + for entry in worldmap_event_replacements: + var event_flag = entry.event_flag + 0x1D0 + var y = entry.y + var x = entry.x + buffer.seek(entry.ptr_bytes) + var blocks = PoolByteArray(buffer.get_data(entry.num_bytes)[1]) + + if last_event_flag == -1: # Finish initializing the initial region + region.start_y = y + elif last_event_flag != event_flag or last_y != y-1: + # Save last region and start a new one + self.event_replacements.get_or_add(last_event_flag, []).append(region) + # Start a new region + region = WorldMap.EventReplacementRegion.new() + region.start_y = y + # Keep building existing region + last_event_flag = event_flag + last_y = y + region.rows.append([x, blocks]) + # Save final region + self.event_replacements.get_or_add(last_event_flag, []).append(region) + + var worldmaps = [WorldMap.new(), WorldMap.new(), WorldMap.new(), WorldMap.new(), WorldMap.new()] var worldmap_block_properties = [] @@ -150,6 +227,8 @@ func load_worldmaps(data: Dictionary, buffer: StreamPeerBuffer): blockmap.append(b+2) chunk_size += 2 worldmaps[worldmap_id].blockmap = blockmap + worldmaps[worldmap_id].blockmap_original = blockmap + worldmaps[worldmap_id].init_event_replacements(data, buffer, data.worldmap_event_replacements[worldmap_id]) func update_worldmap_block_tile_ids(worldmap_block_tile_ids: Array): # Called by SpriteLoader diff --git a/scripts/struct.gd b/scripts/struct.gd index 7fe39e4..5bdaadc 100644 --- a/scripts/struct.gd +++ b/scripts/struct.gd @@ -183,7 +183,7 @@ static func get_structarraytype(type: String, existing_structs: Dictionary): 'of': i -= 1 var l1 = int(tokens[i]) - if l1 > 1: + if l1 >= 0: # 0-of and 1-of may seem nonsensical, but they may help in generic array-of-array parsing inner_type = StructArrayType.new(l1, inner_type) # Might be worth caching these later on if we use them more i -= 1 var k: diff --git a/test/worldmap_system.gd b/test/worldmap_system.gd index 2d8c0b3..86eecae 100644 --- a/test/worldmap_system.gd +++ b/test/worldmap_system.gd @@ -5,8 +5,10 @@ var worldmap_shader_mat := preload('res://worldmap_palette_mat.tres') onready var minimap := $tr_minimap var current_map_id := 0 -var map_images := [] -var map_textures := [] +var map_images := [null, null, null, null, null] +var map_textures := [null, null, null, null, null] +var map_regional_replacement_amounts := [0, 0, 0, 0, 0] +var map_regional_replacement_lists := [[], [], [], [], []] var current_texture: Texture var minimap_mode := 0 var minimap_tween := 0.0 @@ -15,13 +17,20 @@ var minimap_tween := 0.0 const map_tilesets = [0, 1, 0, 2, 2] const waterfall_scrolls = [true, false, true, false, false] const sea_scrolls = [true, true, true, false, false] + +func _create_worldmap_texture(id: int) -> void: + var tileset = map_tilesets[id] + var image = MapLoader.worldmaps[id].make_tile_map() + self.map_images[id] = image + var tex := SpriteLoader.texture_from_image(image, Texture.FLAG_REPEAT) + self.map_textures[id] = tex + func _create_worldmap_textures() -> void: - for i in 5: - var tileset = map_tilesets[i] - var image = MapLoader.worldmaps[i].make_tile_map() - self.map_images.append(image) - var tex := SpriteLoader.texture_from_image(image, Texture.FLAG_REPEAT) - self.map_textures.append(tex) + for id in 5: + self._create_worldmap_texture(id) + for flag_block in MapLoader.worldmaps[id].event_replacements.values(): + for region in flag_block: + self.map_regional_replacement_lists[id].append(region) func _set_map(id: int) -> void: if id < 0 or id >= len(map_images): @@ -41,6 +50,46 @@ func _set_map(id: int) -> void: minimap.material.set_shader_param('enable_sea_scroll', false) minimap.material.set_shader_param('enable_tile_globbing', true) +func _jump_to_regional_replacement(region) -> void: + var y_min = region.start_y + var y_max = y_min + len(region.rows) + var x_min = region.get_min_x() + var x_max = region.get_max_x() + self.pos = Vector2((x_max+x_min)/2, (y_max+y_min)/2) + print_debug('Jumping to event position ', self.pos.x, self.pos.y) + +var delay_map_update: float = -1.0 +func _perform_regional_replacement(amount: int) -> void: + var map = MapLoader.worldmaps[current_map_id] + var last_region + match amount: + 0: + map.blockmap = map.blockmap_original + self.map_regional_replacement_amounts[current_map_id] = 0 + self.delay_map_update = 0.0001 + 1: + if self.map_regional_replacement_amounts[current_map_id] >= len(self.map_regional_replacement_lists[current_map_id]): + return + last_region = self.map_regional_replacement_lists[current_map_id][self.map_regional_replacement_amounts[current_map_id]] + map.apply_event_region_replacement(last_region) + self._jump_to_regional_replacement(last_region) + self.map_regional_replacement_amounts[current_map_id] += 1 + self.delay_map_update = 1.0 + -1: + if self.map_regional_replacement_amounts[current_map_id] < 1: + return + self.map_regional_replacement_amounts[current_map_id] -= 1 + # Reset and reapply the stack + map.blockmap = map.blockmap_original + for i in self.map_regional_replacement_amounts[current_map_id]: + last_region = self.map_regional_replacement_lists[current_map_id][i] + map.apply_event_region_replacement(last_region) + # self._jump_to_regional_replacement(last_region) + self.delay_map_update = 0.0001 + # self._create_worldmap_texture(current_map_id) + # self._set_map(current_map_id) # refresh shader etc. + + # Called when the node enters the scene tree for the first time. func _ready() -> void: # Only create this after MapLoader and SpriteLoader have loaded! @@ -80,6 +129,14 @@ func _process(delta: float) -> void: # minimap.material.set_shader_param('uv_scale', 64 * minimap_scale) minimap.material.set_shader_param('uv_scale', lerp(512, 1<<5, l)) # minimap.material.set_shader_param('uv_scale', 512/16) + + # Hack to do a presentation of map region replacements with a nice before and after + if self.delay_map_update > 0.000: + self.delay_map_update -= delta + if self.delay_map_update <= 0.000: + self._create_worldmap_texture(current_map_id) + self._set_map(current_map_id) # refresh shader etc. + update() var pos := Vector2(0, 0) @@ -111,5 +168,11 @@ func _input(event: InputEvent) -> void: _set_map(4) KEY_0: self.minimap_mode = 1 - minimap_mode + KEY_7: + self._perform_regional_replacement(0) + KEY_8: + self._perform_regional_replacement(-1) + KEY_9: + self._perform_regional_replacement(1) KEY_H: $lbl_help.visible = !$lbl_help.visible diff --git a/test/worldmap_system.tscn b/test/worldmap_system.tscn index aab4ab5..853ccf2 100644 --- a/test/worldmap_system.tscn +++ b/test/worldmap_system.tscn @@ -17,4 +17,5 @@ text = "H: Toggle this help 1-5: Change world 0: Toggle minimap size Arrow keys: Move around +7-9: Reset, Decrement, or Increment regional replacements Backspace: Return to debug menu"