ChocolateBird/scripts/loaders/snes/battle_bgs.gd

160 lines
5.6 KiB
GDScript

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