Add WorldMap dynamic regional changes

This commit is contained in:
Luke Hubmayer-Werner 2024-07-05 02:11:24 +09:30
parent 9a5435f01f
commit 334545fcc4
7 changed files with 168 additions and 12 deletions

View File

@ -36,7 +36,7 @@ I have mostly solved parsing of SNES menus in a sister project, however there ar
#### World Maps (Fields?) #### World Maps (Fields?)
- [x] Tiles - [x] Tiles
- [x] Tilemaps - [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) - [ ] Pathing (Data is in place, just needs moving around with collisions)
- [ ] Mode 7 Effects (...might hardcode these later) - [ ] Mode 7 Effects (...might hardcode these later)
#### Dungeon/Town/Zone Maps (idk what accepted terminology is) #### 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 ### Sound System
- [x] Instrument samples - [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 - [ ] DSP stuff including ADSR envelopes
# What's different? # What's different?

View File

@ -1,4 +1,10 @@
Label SNES PSX_file PSX_offset format Comment 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_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 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 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 worldmap_compressed_tilesets2 0x080000 tilesets 0x434 up to 0x500
ptrs_jp_speech 0x082220 2160 of u16 ptrs_jp_speech 0x082220 2160 of u16
ptrs_extended_event_data 0x083320 1940 of u24 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 jp_speech 0x0A0000 See 0x082220 for offsets
ptrs_tilemaps 0x0B0000 328 of u16 ptrs_tilemaps 0x0B0000 328 of u16
tilemaps 0x0B0290 See above for offsets tilemaps 0x0B0290 See above for offsets

1 Label SNES PSX_file PSX_offset format Comment
2 ptrs_worldmap_event_replacements 0x006ABD 6 of u16 5 worlds + the end address
3 worldmap_event_replacements.0 0x00726C 61 of WorldMapEventReplacement hardcoded of above
4 worldmap_event_replacements.1 0x0073DA 53 of WorldMapEventReplacement hardcoded of above
5 worldmap_event_replacements.2 0x007518 106 of WorldMapEventReplacement hardcoded of above
6 worldmap_event_replacements.3 0x007794 1 of WorldMapEventReplacement hardcoded of above
7 worldmap_event_replacements.4 0x00779A 98 of WorldMapEventReplacement hardcoded of above
8 character_battle_sprite_stone_palette 0x00F807 N/A N/A 16 of ColorBGR555 Also 0x199835
9 character_battle_sprite_disabled_palette 0x00F867 /mnu/memsave.bin 0x000034 16 of ColorBGR555
10 locations_bg_palettes 0x03BB00 /nar/ff5_binx.bin 0x03BF80 43 of 128 of ColorBGR555
25 worldmap_compressed_tilesets2 0x080000 tilesets 0x434 up to 0x500
26 ptrs_jp_speech 0x082220 2160 of u16
27 ptrs_extended_event_data 0x083320 1940 of u24
28 extended_event_data 0x0849DF 0x0849DC See above for addresses
29 jp_speech 0x0A0000 See 0x082220 for offsets
30 ptrs_tilemaps 0x0B0000 328 of u16
31 tilemaps 0x0B0290 See above for offsets

View File

@ -197,3 +197,10 @@ u8 palette_id
u8 23 u8 23
u8 24 u8 24
u8 music_id 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)

Can't render this file because it has a wrong number of fields in line 43.

View File

@ -66,8 +66,32 @@ class WorldMap:
const tile_width := block_width * 2 const tile_width := block_width * 2
const tile_height := block_height * 2 const tile_height := block_height * 2
var blockmap: PoolByteArray var blockmap: PoolByteArray
var blockmap_original: PoolByteArray
var block_tile_ids: PoolByteArray var block_tile_ids: PoolByteArray
var block_pathing: PoolIntArray 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: func get_block_tiles(id: int) -> PoolByteArray:
var i = id * 4 var i = id * 4
@ -92,6 +116,59 @@ class WorldMap:
image.create_from_data(tile_width, tile_height, false, SpriteLoader.INDEX_FORMAT, data) image.create_from_data(tile_width, tile_height, false, SpriteLoader.INDEX_FORMAT, data)
return image 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 worldmaps = [WorldMap.new(), WorldMap.new(), WorldMap.new(), WorldMap.new(), WorldMap.new()]
var worldmap_block_properties = [] var worldmap_block_properties = []
@ -150,6 +227,8 @@ func load_worldmaps(data: Dictionary, buffer: StreamPeerBuffer):
blockmap.append(b+2) blockmap.append(b+2)
chunk_size += 2 chunk_size += 2
worldmaps[worldmap_id].blockmap = blockmap 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): func update_worldmap_block_tile_ids(worldmap_block_tile_ids: Array):
# Called by SpriteLoader # Called by SpriteLoader

View File

@ -183,7 +183,7 @@ static func get_structarraytype(type: String, existing_structs: Dictionary):
'of': 'of':
i -= 1 i -= 1
var l1 = int(tokens[i]) 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 inner_type = StructArrayType.new(l1, inner_type) # Might be worth caching these later on if we use them more
i -= 1 i -= 1
var k: var k:

View File

@ -5,8 +5,10 @@ var worldmap_shader_mat := preload('res://worldmap_palette_mat.tres')
onready var minimap := $tr_minimap onready var minimap := $tr_minimap
var current_map_id := 0 var current_map_id := 0
var map_images := [] var map_images := [null, null, null, null, null]
var map_textures := [] 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 current_texture: Texture
var minimap_mode := 0 var minimap_mode := 0
var minimap_tween := 0.0 var minimap_tween := 0.0
@ -15,13 +17,20 @@ var minimap_tween := 0.0
const map_tilesets = [0, 1, 0, 2, 2] const map_tilesets = [0, 1, 0, 2, 2]
const waterfall_scrolls = [true, false, true, false, false] const waterfall_scrolls = [true, false, true, false, false]
const sea_scrolls = [true, true, 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: func _create_worldmap_textures() -> void:
for i in 5: for id in 5:
var tileset = map_tilesets[i] self._create_worldmap_texture(id)
var image = MapLoader.worldmaps[i].make_tile_map() for flag_block in MapLoader.worldmaps[id].event_replacements.values():
self.map_images.append(image) for region in flag_block:
var tex := SpriteLoader.texture_from_image(image, Texture.FLAG_REPEAT) self.map_regional_replacement_lists[id].append(region)
self.map_textures.append(tex)
func _set_map(id: int) -> void: func _set_map(id: int) -> void:
if id < 0 or id >= len(map_images): 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_sea_scroll', false)
minimap.material.set_shader_param('enable_tile_globbing', true) 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. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
# Only create this after MapLoader and SpriteLoader have loaded! # 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', 64 * minimap_scale)
minimap.material.set_shader_param('uv_scale', lerp(512, 1<<5, l)) minimap.material.set_shader_param('uv_scale', lerp(512, 1<<5, l))
# minimap.material.set_shader_param('uv_scale', 512/16) # 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() update()
var pos := Vector2(0, 0) var pos := Vector2(0, 0)
@ -111,5 +168,11 @@ func _input(event: InputEvent) -> void:
_set_map(4) _set_map(4)
KEY_0: KEY_0:
self.minimap_mode = 1 - minimap_mode 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: KEY_H:
$lbl_help.visible = !$lbl_help.visible $lbl_help.visible = !$lbl_help.visible

View File

@ -17,4 +17,5 @@ text = "H: Toggle this help
1-5: Change world 1-5: Change world
0: Toggle minimap size 0: Toggle minimap size
Arrow keys: Move around Arrow keys: Move around
7-9: Reset, Decrement, or Increment regional replacements
Backspace: Return to debug menu" Backspace: Return to debug menu"