Add battle backgrounds
This commit is contained in:
parent
e80af7bdda
commit
4fde52bb9e
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
|
@ -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.
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in New Issue