const NUM_COLUMNS := 32 const NUM_ROWS := 20 const LENGTH := NUM_COLUMNS * NUM_ROWS # 0x500 / 2 # Expanded length 0x500, RLE step produces 0x280 tile mappings const NUM_BATTLE_BG_TILESETS := 21 # These combine with a RAM skip amount to form a subset used as a tile atlas const NUM_BATTLE_BG_TILEMAPS := 28 const NUM_BATTLE_BG_FLIPSETS := 9 const IMAGE_FORMAT := Image.FORMAT_LA8 const compression = preload('res://scripts/loaders/snes/compression.gd') class TileMapping: var tile_index: int var palette: int var priority: bool var h_flip: bool var v_flip: bool static func from_tilemap_word(w: int) -> TileMapping: var t := TileMapping.new() t.tile_index = w & 0x03FF t.palette = (w & 0x1C00) >> 10 t.priority = bool(w & 0x2000) t.h_flip = bool(w & 0x4000) t.v_flip = bool(w & 0x8000) return t static func from_battle_byte(b: int) -> TileMapping: var t := TileMapping.new() t.tile_index = b & 0x7F # In-game this gets |= 0x80, as the BG tiles are from 0x80 to 0xFF t.palette = b >> 7 # In-game this gets incremented by 1, as the BG palettes are #1 and #2 leaving #0 for UI elements return t func serialize(buffer: StreamPeer) -> void: # 8bit for tile_index # 6bit for palette # 1bit for each flip # Do nothing with priority, should have two textures for each layer depending on it buffer.put_u8(self.tile_index) var byte2 := self.palette if self.h_flip: byte2 += 0x40 if self.v_flip: byte2 += 0x80 buffer.put_u8(byte2) static func decompress_battle_tilemap(buffer: StreamPeer) -> Array: # Decompresses the tilemap for a battle background. # Battle BGs use a type of RLE with 2byte repeat and 1byte incremental repeat. var mappings := [] while len(mappings) < LENGTH: var byte := buffer.get_u8() if byte != 0xFF: mappings.append(TileMapping.from_battle_byte(byte)) else: # Byte begins a repeat code var repeat_code := buffer.get_u8() var repeat := repeat_code & 0x3F byte = buffer.get_u8() if repeat_code & 0x80: # Repeat 2 tiles var byte2 := buffer.get_u8() for i in repeat: mappings.append(TileMapping.from_battle_byte(byte)) mappings.append(TileMapping.from_battle_byte(byte2)) else: # Incremental repeat var inc := buffer.get_u8() if repeat_code & 0x40: # Negative increment inc = -inc for i in repeat: mappings.append(TileMapping.from_battle_byte(byte)) byte += inc return mappings static func apply_battle_tilemap_flips(buffer: StreamPeer, tilemap: Array): var tile_i := 0 while tile_i < LENGTH: var a := buffer.get_u8() if a == 0x00: # Skip N*8 tiles tile_i += buffer.get_u8() * 8 else: # Each bit is a tile, 1 means flip horizontally for b in range(7, -1, -1): tilemap[tile_i].h_flip = bool((a>>b) & 0x01) tile_i += 1 static func add_anim_palettes(buffer: StreamPeer, palettes: Array): var pal1_id: int = palettes[0][0] var pal2_id: int = palettes[0][1] palettes.pop_back() # Cycles look more correct without the first palettes while true: var b := buffer.get_u8() if b == 0xFF: break if b & 0x80: pal2_id = b & 0x7F else: pal1_id = b palettes.append([pal1_id, pal2_id]) static func get_anim_tiles(buffer: StreamPeer) -> Array: var output := [] while true: var b := buffer.get_u8() # 2 MSb frame number (0-3), 6 bit tile index to replace var b2 := buffer.get_u8() # tile index to copy from if (b == 0xFF) or (b2 == 0xFF): break output.append([b, b2]) return output static func array_of_tilemappings_to_image(tilemappings: Array) -> Image: var out := Image.new() var buffer := StreamPeerBuffer.new() for tilemapping in tilemappings: tilemapping.serialize(buffer) out.create_from_data(NUM_COLUMNS, NUM_ROWS, false, IMAGE_FORMAT, buffer.data_array) return out static func get_all_battle_background_tilesets(buffer: StreamPeer, tileset_offsets: Array, skip_offsets: Array) -> Array: # Convert these to 4bpp tiles to create your atlases var raw_tilesets := {} # key is offset, value is decompressed tiles var tilesets := [] for i in NUM_BATTLE_BG_TILESETS: var skip: int = skip_offsets[i] - 0x7FC000 var offset: int = tileset_offsets[i] - 0xC00000 if not (offset in raw_tilesets): buffer.seek(offset) raw_tilesets[offset] = compression.decompress_lzss(buffer) var raw: PoolByteArray = raw_tilesets[offset] var skipped_raw := raw.subarray(skip, -1) tilesets.append(skipped_raw) return tilesets static func get_all_battle_background_tilemaps(buffer: StreamPeerBuffer, data: Dictionary) -> Array: var tbl_battle_backgrounds: Array = data.tbl_battle_backgrounds var tilesets = get_all_battle_background_tilesets(buffer, data.ptrs_battle_background_tilesets, data.ptrs_battle_background_tileset_skips) var tilemaps = [] for i in NUM_BATTLE_BG_TILEMAPS: buffer.seek(data.ptrs_battle_background_tilemaps[i] + 0x140000) tilemaps.append(decompress_battle_tilemap(buffer)) var output := [] for tbl in tbl_battle_backgrounds: var out := {} out.tileset = tilesets[tbl.tileset_id] out.palette_ids = [tbl.palette_ids] var tilemap = tilemaps[tbl.tilemap_id] if tbl.tilemap_flips_id < NUM_BATTLE_BG_FLIPSETS: # 0xFF means no flips buffer.seek(data.ptrs_battle_background_tilemap_flips[tbl.tilemap_flips_id] + 0x140000) apply_battle_tilemap_flips(buffer, tilemap) out.tilemap_image = array_of_tilemappings_to_image(tilemap) if tbl.palcycle_id < 0xFF: buffer.seek(data.ptrs_battle_background_palette_animations[tbl.palcycle_id] + 0x140000) add_anim_palettes(buffer, out.palette_ids) if tbl.tilecycle_id > 0: buffer.seek(data.ptrs_battle_background_tile_animations[tbl.tilecycle_id] + 0x140000) out.animated_tiles = get_anim_tiles(buffer) output.append(out) return output