ChocolateBird/scripts/loaders/MapLoader.gd

215 lines
8.2 KiB
GDScript

extends Node
# World Map Block Properties
# 3 bytes
# Byte0: movement properties
# 0x01 = passable on foot
# 0x02 = passable on chocobo?
# 0x04 = passable on black chocobo
# 0x08 = passable on hiryuu?
# 0x10 = passable in submarine? set on deep water tiles, and all undersea tiles that aren't cliffs
# 0x20 = passable in ship? set on deep water tiles only (not undersea) - can submerge?
# 0x40 = passable in airship? Pretty much every tile aboveground has this. No undersea.
# 0x80 = only set on clear sea floor. Submarine pathable/can surface?
# Byte1: movement properties
# 0x01 = (water flips) can move from this block rightwards
# 0x02 = (water flips) can move from this block leftwards
# 0x04 = (water flips) can move from this block downwards
# 0x08 = (water flips) can move from this block upwards
# 0x10 = Chocobo can't land/dismount. most aboveground water tiles
# 0x20 = Black Chocobo can't land.
# 0x40 = Hiryuu can't land.
# 0x80 = Airship can't land.
# Byte2: movement properties
# 0x01 = Set on forests, deep water, void.
# 0x02 = Set on deep water, void, desert.
# 0x04 = Only set on diagonal land corners and Galuf World swamp
# 0x08 = No hits.
# 0x10 = Mountains and Exdeath's Castle
# 0x20 = Set on first two rows of forests, also waterfall, but not lower bounds of forests
# 0x40 = Shallow water.
# 0x80 =
# Vehicle landing bit masks: [00 10 20 40 00 00 80] - & with Byte1, if 1, can't land
# Vehicle IDs: [None, Chocobo, BlkChocobo, Hiryuu, Submarine, Ship, Airship]
# Worldmap animations
# World 1 (and probably 3)
# Sea tiles and waterfall tiles have a scrolling effect in tile data
# This may require setting up a proper tile indirect lookup shader
# Shifting sands and the portal have cycling palettes: $6C and $6D swap every frame, $51 through $55 scroll left (i.e. $55->$54, $51->$55)
# This will be best hardcoded as a 10 palette cycle
# World 2:
# Sea tiles have a horizontal scrolling effect in tile data (addresses $1880, $18C0, $1C80, $1CC0)
# ASM at C09660 BF 21 86 7F LDA $7F8621,X
# Probably going to shader this effect instead of storing hundreds of frames
# No palette cycling
class WorldMap:
var block_width: int
var block_height: int
var tile_width: int
var tile_height: int
var blockmap: PoolByteArray
var blockmap_original: PoolByteArray
var block_tile_ids: PoolByteArray
var block_pathing: PoolIntArray
var event_replacements: Array # Array[int flag, EventReplacementRegion]
func _init(width: int = 256, height: int = 256) -> void:
self.block_width = width
self.block_height = height
self.tile_width = width * 2
self.tile_height = height * 2
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 = 1000000
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
return self.block_tile_ids.subarray(i, i+4)
func get_block_pathing_flags(id: int) -> int:
return block_pathing[id]
func make_tile_map() -> Image:
var image := Image.new()
var data := PoolByteArray()
data.resize(self.tile_width * self.tile_height)
var block_idx := 0
for y_off in range(0, self.tile_height*self.tile_width, self.tile_width*2):
for x_off in range(0, self.tile_width, 2):
var tile_offset = self.blockmap[block_idx] * 4
block_idx += 1
data[y_off + x_off] = self.block_tile_ids[tile_offset]
data[y_off + x_off + 1] = self.block_tile_ids[tile_offset+1]
data[y_off + self.tile_width + x_off] = self.block_tile_ids[tile_offset+2]
data[y_off + self.tile_width + x_off + 1] = self.block_tile_ids[tile_offset+3]
image.create_from_data(self.tile_width, self.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 * self.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 += self.block_width
func apply_event_replacements(event_flags): # Any integer array is fine
for flag_and_region in self.event_replacements:
if flag_and_region[0] in event_flags:
self.apply_event_region_replacement(flag_and_region[1])
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.append([last_event_flag, 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.append([last_event_flag, region])
var worldmaps = [WorldMap.new(), WorldMap.new(), WorldMap.new(), WorldMap.new(), WorldMap.new()]
func load_worldmaps(data: Dictionary, buffer: StreamPeerBuffer):
var offset1: int = Common.SNES_PSX_addresses.worldmap_compressed_tilesets.SNES
var offset2: int = Common.SNES_PSX_addresses.worldmap_compressed_tilesets2.SNES
for worldmap_id in 5: # Bartz World, Galuf World, Combined World, Underwater Galuf World, Underwater Combined World
# Worldmap chunks have a basic compression.
# Repeated blocks along a row are run-length-encoded (RLE)
# Certain blocks (mountains) expand to 1x3
var chunk_addresses: Array = data.ptrs_worldmap_tilesets[worldmap_id]
var blockmap = PoolByteArray()
# blockmap.resize(WorldMap.block_height * WorldMap.block_width) # Try this later if performance is a problem
for chunk_id in 0x100:
var bank = offset1
if worldmap_id >= 0x4 and chunk_id >= 0x34: # Chunks 0x434 up to 0x500 are in the next bank
bank = offset2
buffer.seek(bank + chunk_addresses[chunk_id])
var chunk_size := 0
while chunk_size < 256:
# var b: int = (blockmap.size() % 16) + (16 * (chunk_id % 12)); # For debugging the map shader against blocks
var b := buffer.get_u8()
if b >= 0xC0: # RLE
var count := b-0xBF
var block = buffer.get_u8()
for i in count:
blockmap.append(block)
chunk_size += count
else:
blockmap.append(b)
chunk_size += 1
if b == 0x0C or b == 0x1C or b == 0x2C:
# Mountain blocks expand to a 1x3
blockmap.append(b+1)
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
worldmaps[0].block_tile_ids = worldmap_block_tile_ids[0]
worldmaps[1].block_tile_ids = worldmap_block_tile_ids[1]
worldmaps[2].block_tile_ids = worldmap_block_tile_ids[0]
worldmaps[3].block_tile_ids = worldmap_block_tile_ids[2]
worldmaps[4].block_tile_ids = worldmap_block_tile_ids[2]
func load_snes_rom(data: Dictionary, buffer: StreamPeerBuffer):
load_worldmaps(data, buffer)