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)