Add battle backgrounds

This commit is contained in:
Luke Hubmayer-Werner 2023-08-14 21:48:18 +09:30
parent e80af7bdda
commit 4fde52bb9e
9 changed files with 277 additions and 32 deletions

View File

@ -51,7 +51,7 @@ I have mostly solved parsing of SNES menus in a sister project, however there ar
- [ ] Weapon animations (will hardcode these)
- [x] Enemy sprites
- [ ] Enemy sprite separate shadows
- [ ] Backgrounds (solved problem, soon™)
- [x] Backgrounds
- [ ] Enemy AI (needs research)
- [ ] Abilities (will hardcode these, with fixes and extensions where appropriate)
- [ ] Calculations (will hardcode these from Algorithms guide, with fixes and extensions where appropriate)

View File

@ -44,6 +44,15 @@ enemy_battle_sprite_data 0x14B180 384 of EnemySpriteData length 0x780
character_battle_sprite_layouts 0x14B997 /btl/ff5_btl.bin 0x028997 11 of 6 of u8
tbl_battle_backgrounds 0x14BA21 34 of BattleBackgroundData
battle_background_palettes 0x14BB31 84 of Palette16Of555
ptrs_battle_background_tile_animations 0x14C5B1 8 of u16 bank 0x140000 (0xD40000)
battle_background_tile_animations_data 0x14C5C1 see above
ptrs_battle_background_palette_animations 0x14C6CD 3 of u16 bank 0x140000 (0xD40000)
battle_background_palette_animations_data 0x14C6D3 see above
ptrs_battle_background_tilemap_flips 0x14C736 9 of u16 bank 0x140000 (0xD40000)
battle_background_tilemap_flips_data 0x14C75C see above
ptrs_battle_background_tilemaps 0x14C86D 28 of u16 bank 0x140000 (0xD40000)
? 0x14C8A5
battle_background_tilemaps_data 0x14E09B see above
enemy_battle_sprite_tiles 0x150000 See enemy_battle_sprite_data for pointers
ptrs_battle_background_tileset_skips 0x184157 21 of u24 RAM addresses, subtract 0x7FC000 from results to get offset from tileset
ptrs_battle_background_tilesets 0x184196 21 of u24 ROM addresses, subtract 0xC00000

1 Label SNES PSX_file PSX_offset format Comment
44 character_battle_sprite_layouts 0x14B997 /btl/ff5_btl.bin 0x028997 11 of 6 of u8
45 tbl_battle_backgrounds 0x14BA21 34 of BattleBackgroundData
46 battle_background_palettes 0x14BB31 84 of Palette16Of555
47 ptrs_battle_background_tile_animations 0x14C5B1 8 of u16 bank 0x140000 (0xD40000)
48 battle_background_tile_animations_data 0x14C5C1 see above
49 ptrs_battle_background_palette_animations 0x14C6CD 3 of u16 bank 0x140000 (0xD40000)
50 battle_background_palette_animations_data 0x14C6D3 see above
51 ptrs_battle_background_tilemap_flips 0x14C736 9 of u16 bank 0x140000 (0xD40000)
52 battle_background_tilemap_flips_data 0x14C75C see above
53 ptrs_battle_background_tilemaps 0x14C86D 28 of u16 bank 0x140000 (0xD40000)
54 ? 0x14C8A5
55 battle_background_tilemaps_data 0x14E09B see above
56 enemy_battle_sprite_tiles 0x150000 See enemy_battle_sprite_data for pointers
57 ptrs_battle_background_tileset_skips 0x184157 21 of u24 RAM addresses, subtract 0x7FC000 from results to get offset from tileset
58 ptrs_battle_background_tilesets 0x184196 21 of u24 ROM addresses, subtract 0xC00000

View File

@ -130,9 +130,9 @@ u8 layout_id # Small? <<3, + 0x10D004, take 8 bytes. Large? <<5, + 0x10D334, tak
struct BattleBackgroundData
u8 tileset_id
u8 pal1_id
u8 pal2_id
2 of u8 palette_ids
u8 tilemap_id
u8 tilemap_flips_id
u8 tilemap_v_flips_id # Unused, all 0xFF = no flips
u8 tilecycle_id
u8 palcycle_id

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

View File

@ -68,9 +68,10 @@ func load_snes_rom(filename: String):
var ability_learned: int = buffer.get_u8()
ability_list.append({'ABP': abp_requirement, 'ability': ability_learned})
snes_data.job_levels.append(ability_list)
print(snes_data.job_levels)
#print(snes_data.job_levels)
SpriteLoader.load_from_structs(snes_data)
SpriteLoader.load_enemy_battle_sprites(snes_data, buffer)
SpriteLoader.load_battle_bgs(snes_data, buffer)
MapLoader.load_snes_rom(rom_snes)
func load_psx_folder(_dirname: String):

View File

@ -3,6 +3,7 @@ extends Node
const INDEX_FORMAT := globals.INDEX_FORMAT
const snes_graphics := preload('res://scripts/loaders/snes/graphics.gd')
const snes_battle_bgs := preload('res://scripts/loaders/snes/battle_bgs.gd')
const gba_graphics := preload('res://scripts/loaders/gba/graphics.gd')
var shader_material = load('res://palette_mat.tres')
const TILE_RECT := Rect2(0, 0, 8, 8)
@ -369,6 +370,60 @@ func load_enemy_battle_sprites(data: Dictionary, buffer: StreamPeerBuffer):
# Save monster
data.monster_battle_sprites.append(entry)
class BattleBackground:
var tilemap_image: Image
var tilemap_tex: Texture
var palette_images: Array
var palette_texs: Array
var tile_atlas_images: Array
var tile_atlas_texs: Array
var battle_backgrounds := []
func load_battle_bgs(data: Dictionary, buffer: StreamPeerBuffer):
var bg_palettes = data.battle_background_palettes
for map in snes_battle_bgs.get_all_battle_background_tilemaps(buffer, data):
var bg := BattleBackground.new()
bg.tilemap_image = map.tilemap_image
bg.tilemap_tex = texture_from_image(bg.tilemap_image)
var palette_images = []
var palette_texs = []
for ids in map.palette_ids:
var pal_image := generate_palette_from_colorarray(bg_palettes[ids[0]] + bg_palettes[ids[1]])
palette_images.append(pal_image)
palette_texs.append(texture_from_image(pal_image))
bg.palette_images = palette_images
bg.palette_texs = palette_texs
var tiles := []
var ts: PoolByteArray = map.tileset
var ts_l = len(ts)
for i in 128:
var start: int = i*32
var end: int = start+31 # inclusive...
if end >= ts_l:
break
tiles.append(snes_graphics._4plane_to_tile(ts.subarray(start, end)))
if 'animated_tiles' in map:
bg.tile_atlas_images = []
bg.tile_atlas_texs = []
var frames = []
for frame in 4:
frames.append(tiles.duplicate())
for pair in map.animated_tiles:
var frame = pair[0] >> 6
var tile_dst = pair[0] & 0x3F
var tile_src = pair[1]
frames[frame][tile_dst] = tiles[tile_src]
for frame in 4:
var atlas_image = make_tile_atlas(frames[frame])
bg.tile_atlas_images.append(atlas_image)
bg.tile_atlas_texs.append(texture_from_image(atlas_image))
else:
var atlas_image = make_tile_atlas(tiles)
bg.tile_atlas_images = [atlas_image]
bg.tile_atlas_texs = [texture_from_image(atlas_image)]
battle_backgrounds.append(bg)
static func bias_tile(unbiased: PoolByteArray, bias: int) -> Image:
var image := Image.new()
var biased = ByteArray(64)

View File

@ -1,7 +1,50 @@
extends Node
const LENGTH := 0x500 / 2 # Expanded length 0x500, RLE step produces 0x280 tile mappings
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')
func decompress_battle_tilemap(buffer: StreamPeerBuffer) -> Array:
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 := []
@ -27,12 +70,7 @@ func decompress_battle_tilemap(buffer: StreamPeerBuffer) -> Array:
byte += inc
return mappings
func apply_battle_tilemap_flips(buffer: StreamPeerBuffer, id: int, tilemap: Array):
if id==0xFF:
return
buffer.seek(0x14C736+(id*2))
var ptr := 0x140000 + buffer.get_u16()
buffer.seek(ptr)
static func apply_battle_tilemap_flips(buffer: StreamPeer, tilemap: Array):
var tile_i := 0
while tile_i < LENGTH:
var a := buffer.get_u8()
@ -44,25 +82,78 @@ func apply_battle_tilemap_flips(buffer: StreamPeerBuffer, id: int, tilemap: Arra
tilemap[tile_i].h_flip = bool((a>>b) & 0x01)
tile_i += 1
static func add_anim_palettes(buffer: StreamPeer, palettes: Array):
# TODO: check if the very first entry is added too
var pal1_id: int = palettes[0][0]
var pal2_id: int = palettes[0][1]
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])
class TileMapping:
var tile_index: int
var palette: int
var priority: bool
var h_flip: bool
var v_flip: bool
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 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 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 from_battle_byte(b: int) -> TileMapping:
var t := TileMapping.new()
t.tile_index = b | 0x80
t.palette = 1 + (b >> 7)
return t
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

View File

@ -0,0 +1,33 @@
static func decompress_lzss(rom: StreamPeer, uncompressed_length:=0) -> PoolByteArray:
# Algorithm from http://slickproductions.org/slickwiki/index.php/Noisecross:Final_Fantasy_V_Compression
# Reuploaded at https://www.ff6hacking.com/ff5wiki/index.php/Compression#Decompression_Type_02_.28LZSS.29
if not uncompressed_length:
uncompressed_length = rom.get_u16()
var output := PoolByteArray()
var buffer := PoolByteArray()
buffer.resize(0x800)
buffer.fill(0)
var buffer_p := 0x07DE
while len(output) < uncompressed_length:
var bitmap_byte := rom.get_u8()
for i in range(8):
var bit := (bitmap_byte >> i) & 1
if bit:
var b := rom.get_u8()
output.append(b)
buffer[buffer_p] = b
buffer_p = (buffer_p+1) % 0x800
else:
# Reuse bytes from previously decompressed buffer
var b1 := rom.get_u8() # lower 8 bits of offset
var b2 := rom.get_u8() # upper 3 bits of offset, 5bit length-3
var offset := b1 | ((b2 & 0xE0) << 3) # 11bit is [0, 0x7FF]
var length := (b2 & 0x1F) + 3 # The +3 is likely because the compression overhead makes this a sensible minimum
for j in range(length):
var b := buffer[offset]
output.append(b)
buffer[buffer_p] = b
buffer_p = (buffer_p+1) % 0x800
offset = (offset+1) % 0x800
output.resize(uncompressed_length)
return output

View File

@ -0,0 +1,43 @@
shader_type canvas_item;
uniform sampler2D tile_atlas : hint_normal;
uniform sampler2D palette : hint_normal;
uniform float palette_rows = 8.0; // 128 colours
// uniform float tile_width = 8.0;
uniform vec2 tilemap_size = vec2(32.0, 20.0); // Battle tilemaps are 32x20, zone tilemaps are larger (64x64?)
const float INDEX_SCALE = 255.0 / 16.0;
// This shader maps from tileID texels to Tiles, and then applies 4bpp (16-colour) palette.
// tile_atlas hardcoded to 16x16 tiles for now
const float ATLAS_SIZE = 16.0;
// palette hardcoded to 16 columns of colors for now
void fragment() {
// GLES2
vec2 xy = UV * tilemap_size; // Texel-space coord of our texture
vec2 t = texture(TEXTURE, UV).ra;
int tile_idx = int(t.x * 255.0); // Luminosity channel (any RGB channel works)
int palette_idx = int(t.y * 255.0); // Alpha channel
// Extract flip bits from palette_idx byte
int v_flip = (palette_idx / 128);
palette_idx = palette_idx % 128;
int h_flip = (palette_idx / 64);
palette_idx = palette_idx % 64;
// Convert tile_idx to a texel coordinate, then to a UV coordinate
ivec2 tile_xy = ivec2(tile_idx%16, tile_idx/16);
vec2 tile_uv = vec2(tile_xy)/16.0;
// Get sub-tile UV
vec2 sub_tile_uv = fract(xy);
sub_tile_uv.x = mix(sub_tile_uv.x, 1.0 - sub_tile_uv.x, float(h_flip)); // Branchless mirroring, maybe test perf against branched version at some point
sub_tile_uv.y = mix(sub_tile_uv.y, 1.0 - sub_tile_uv.y, float(v_flip));
vec2 lut_uv = tile_uv + (sub_tile_uv/ATLAS_SIZE);
// TODO: move cycling palette to a sampler2DArray or sampler3D rather than rebinding
float color_id = texture(tile_atlas, lut_uv).r;
float color_idx16 = color_id * INDEX_SCALE;
float pal_col = fract(color_idx16);
float pal_row = float(palette_idx);
COLOR = texture(palette, vec2(pal_col, pal_row));
// COLOR.a = step(0.000001, color_idx16); // Branchless transparency
}

View File

@ -1,5 +1,6 @@
extends Control
const palette_mat := preload('res://palette_mat.tres')
const battle_bg_shader = preload('res://shaders/tilemap_shader.gdshader')
var save_slots = []
var save_slot_dicts = []
@ -35,6 +36,18 @@ func _ready():
t.material.set_shader_param('palette', mon.palette)
monster_box.add_child(t)
var battle_bg_mat := ShaderMaterial.new()
battle_bg_mat.shader = battle_bg_shader
# var bg = SpriteLoader.battle_backgrounds[1]
for bg in SpriteLoader.battle_backgrounds.slice(0, 2):
var t := TextureRect.new()
t.material = battle_bg_mat.duplicate()
t.texture = bg.tilemap_tex
t.material.set_shader_param('palette', bg.palette_texs[0])
t.material.set_shader_param('tile_atlas', bg.tile_atlas_texs[0])
t.rect_scale = Vector2(8, 8)
t.name = 'BattleBG'
add_child(t)
# var lbl = Label.new()
# for i in 22:
# lbl.text = lbl.text + '%s - %s\n' % [StringLoader.get_job_name(i), StringLoader.get_job_desc(i)]