backend-infra-engineer: Release v0.3.2 snapshot
This commit is contained in:
207
src/zelda3/screen/dungeon_map.cc
Normal file
207
src/zelda3/screen/dungeon_map.cc
Normal file
@@ -0,0 +1,207 @@
|
||||
#include "dungeon_map.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
#include "util/file_util.h"
|
||||
#include "app/platform/window.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gfx/types/snes_tile.h"
|
||||
#include "app/gfx/render/tilemap.h"
|
||||
#include "app/gfx/backend/irenderer.h"
|
||||
#include "app/snes.h"
|
||||
#include "util/hex.h"
|
||||
|
||||
namespace yaze::zelda3 {
|
||||
|
||||
absl::StatusOr<std::vector<DungeonMap>> LoadDungeonMaps(
|
||||
Rom &rom, DungeonMapLabels &dungeon_map_labels) {
|
||||
std::vector<DungeonMap> dungeon_maps;
|
||||
std::vector<std::array<uint8_t, kNumRooms>> current_floor_rooms_d;
|
||||
std::vector<std::array<uint8_t, kNumRooms>> current_floor_gfx_d;
|
||||
int total_floors_d;
|
||||
uint8_t nbr_floor_d;
|
||||
uint8_t nbr_basement_d;
|
||||
|
||||
for (int d = 0; d < kNumDungeons; d++) {
|
||||
current_floor_rooms_d.clear();
|
||||
current_floor_gfx_d.clear();
|
||||
ASSIGN_OR_RETURN(int ptr, rom.ReadWord(kDungeonMapRoomsPtr + (d * 2)));
|
||||
ASSIGN_OR_RETURN(int ptr_gfx, rom.ReadWord(kDungeonMapGfxPtr + (d * 2)));
|
||||
ptr |= 0x0A0000; // Add bank to the short ptr
|
||||
ptr_gfx |= 0x0A0000; // Add bank to the short ptr
|
||||
int pc_ptr = SnesToPc(ptr); // Contains data for the next 25 rooms
|
||||
int pc_ptr_gfx = SnesToPc(ptr_gfx); // Contains data for the next 25 rooms
|
||||
|
||||
ASSIGN_OR_RETURN(uint16_t boss_room_d,
|
||||
rom.ReadWord(kDungeonMapBossRooms + (d * 2)));
|
||||
|
||||
ASSIGN_OR_RETURN(nbr_basement_d, rom.ReadByte(kDungeonMapFloors + (d * 2)));
|
||||
nbr_basement_d &= 0x0F;
|
||||
|
||||
ASSIGN_OR_RETURN(nbr_floor_d, rom.ReadByte(kDungeonMapFloors + (d * 2)));
|
||||
nbr_floor_d &= 0xF0;
|
||||
nbr_floor_d = nbr_floor_d >> 4;
|
||||
|
||||
total_floors_d = nbr_basement_d + nbr_floor_d;
|
||||
|
||||
// for each floor in the dungeon
|
||||
for (int i = 0; i < total_floors_d; i++) {
|
||||
dungeon_map_labels[d].emplace_back();
|
||||
|
||||
std::array<uint8_t, kNumRooms> rdata;
|
||||
std::array<uint8_t, kNumRooms> gdata;
|
||||
|
||||
// for each room on the floor
|
||||
for (int j = 0; j < kNumRooms; j++) {
|
||||
gdata[j] = 0xFF;
|
||||
rdata[j] = rom.data()[pc_ptr + j + (i * kNumRooms)]; // Set the rooms
|
||||
|
||||
gdata[j] = rdata[j] == 0x0F ? 0xFF : rom.data()[pc_ptr_gfx++];
|
||||
|
||||
std::string label = util::HexByte(rdata[j]);
|
||||
dungeon_map_labels[d][i][j] = label;
|
||||
}
|
||||
|
||||
current_floor_gfx_d.push_back(gdata); // Add new floor gfx data
|
||||
current_floor_rooms_d.push_back(rdata); // Add new floor data
|
||||
}
|
||||
|
||||
dungeon_maps.emplace_back(boss_room_d, nbr_floor_d, nbr_basement_d,
|
||||
current_floor_rooms_d, current_floor_gfx_d);
|
||||
}
|
||||
|
||||
return dungeon_maps;
|
||||
}
|
||||
|
||||
absl::Status SaveDungeonMaps(Rom &rom, std::vector<DungeonMap> &dungeon_maps) {
|
||||
for (int d = 0; d < kNumDungeons; d++) {
|
||||
int ptr = kDungeonMapRoomsPtr + (d * 2);
|
||||
int ptr_gfx = kDungeonMapGfxPtr + (d * 2);
|
||||
int pc_ptr = SnesToPc(ptr);
|
||||
int pc_ptr_gfx = SnesToPc(ptr_gfx);
|
||||
|
||||
const int nbr_floors = dungeon_maps[d].nbr_of_floor;
|
||||
const int nbr_basements = dungeon_maps[d].nbr_of_basement;
|
||||
for (int i = 0; i < nbr_floors + nbr_basements; i++) {
|
||||
for (int j = 0; j < kNumRooms; j++) {
|
||||
RETURN_IF_ERROR(rom.WriteByte(pc_ptr + j + (i * kNumRooms),
|
||||
dungeon_maps[d].floor_rooms[i][j]));
|
||||
RETURN_IF_ERROR(rom.WriteByte(pc_ptr_gfx + j + (i * kNumRooms),
|
||||
dungeon_maps[d].floor_gfx[i][j]));
|
||||
pc_ptr_gfx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status LoadDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom,
|
||||
const std::vector<uint8_t> &gfx_data,
|
||||
bool bin_mode) {
|
||||
tile16_blockset.tile_size = {16, 16};
|
||||
tile16_blockset.map_size = {186, 186};
|
||||
tile16_blockset.atlas.Create(256, 192, 8,
|
||||
std::vector<uint8_t>(256 * 192, 0x00));
|
||||
|
||||
for (int i = 0; i < kNumDungeonMapTile16; i++) {
|
||||
int addr = kDungeonMapTile16;
|
||||
if (rom.data()[kDungeonMapExpCheck] != 0xB9) {
|
||||
addr = kDungeonMapTile16Expanded;
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(auto tl, rom.ReadWord(addr + (i * 8)));
|
||||
gfx::TileInfo t1 = gfx::WordToTileInfo(tl); // Top left
|
||||
|
||||
ASSIGN_OR_RETURN(auto tr, rom.ReadWord(addr + 2 + (i * 8)));
|
||||
gfx::TileInfo t2 = gfx::WordToTileInfo(tr); // Top right
|
||||
|
||||
ASSIGN_OR_RETURN(auto bl, rom.ReadWord(addr + 4 + (i * 8)));
|
||||
gfx::TileInfo t3 = gfx::WordToTileInfo(bl); // Bottom left
|
||||
|
||||
ASSIGN_OR_RETURN(auto br, rom.ReadWord(addr + 6 + (i * 8)));
|
||||
gfx::TileInfo t4 = gfx::WordToTileInfo(br); // Bottom right
|
||||
|
||||
int sheet_offset = 212;
|
||||
if (bin_mode) {
|
||||
sheet_offset = 0;
|
||||
}
|
||||
ComposeTile16(tile16_blockset, gfx_data, t1, t2, t3, t4, sheet_offset);
|
||||
}
|
||||
|
||||
tile16_blockset.atlas.SetPalette(*rom.mutable_dungeon_palette(3));
|
||||
|
||||
// Queue texture creation via Arena's deferred system
|
||||
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
|
||||
&tile16_blockset.atlas);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status SaveDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom) {
|
||||
for (int i = 0; i < kNumDungeonMapTile16; i++) {
|
||||
int addr = kDungeonMapTile16;
|
||||
if (rom.data()[kDungeonMapExpCheck] != 0xB9) {
|
||||
addr = kDungeonMapTile16Expanded;
|
||||
}
|
||||
|
||||
gfx::TileInfo t1 = tile16_blockset.tile_info[i][0];
|
||||
gfx::TileInfo t2 = tile16_blockset.tile_info[i][1];
|
||||
gfx::TileInfo t3 = tile16_blockset.tile_info[i][2];
|
||||
gfx::TileInfo t4 = tile16_blockset.tile_info[i][3];
|
||||
|
||||
auto tl = gfx::TileInfoToWord(t1);
|
||||
RETURN_IF_ERROR(rom.WriteWord(addr + (i * 8), tl));
|
||||
|
||||
auto tr = gfx::TileInfoToWord(t2);
|
||||
RETURN_IF_ERROR(rom.WriteWord(addr + 2 + (i * 8), tr));
|
||||
|
||||
auto bl = gfx::TileInfoToWord(t3);
|
||||
RETURN_IF_ERROR(rom.WriteWord(addr + 4 + (i * 8), bl));
|
||||
|
||||
auto br = gfx::TileInfoToWord(t4);
|
||||
RETURN_IF_ERROR(rom.WriteWord(addr + 6 + (i * 8), br));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status LoadDungeonMapGfxFromBinary(Rom &rom,
|
||||
gfx::Tilemap &tile16_blockset,
|
||||
std::array<gfx::Bitmap, 4> &sheets,
|
||||
std::vector<uint8_t> &gfx_bin_data) {
|
||||
std::string bin_file = util::FileDialogWrapper::ShowOpenFileDialog();
|
||||
if (bin_file.empty()) {
|
||||
return absl::InternalError("No file selected");
|
||||
}
|
||||
|
||||
std::ifstream file(bin_file, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return absl::InternalError("Failed to open file");
|
||||
}
|
||||
|
||||
// Read the gfx data into a buffer
|
||||
std::vector<uint8_t> bin_data((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
auto converted_bin = gfx::SnesTo8bppSheet(bin_data, 4, 4);
|
||||
gfx_bin_data = converted_bin;
|
||||
if (LoadDungeonMapTile16(tile16_blockset, rom, converted_bin, true).ok()) {
|
||||
std::vector<std::vector<uint8_t>> gfx_sheets;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
gfx_sheets.emplace_back(converted_bin.begin() + (i * 0x1000),
|
||||
converted_bin.begin() + ((i + 1) * 0x1000));
|
||||
sheets[i] = gfx::Bitmap(128, 32, 8, gfx_sheets[i]);
|
||||
sheets[i].SetPalette(*rom.mutable_dungeon_palette(3));
|
||||
|
||||
// Queue texture creation via Arena's deferred system
|
||||
gfx::Arena::Get().QueueTextureCommand(gfx::Arena::TextureCommandType::CREATE,
|
||||
&sheets[i]);
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace yaze::zelda3
|
||||
114
src/zelda3/screen/dungeon_map.h
Normal file
114
src/zelda3/screen/dungeon_map.h
Normal file
@@ -0,0 +1,114 @@
|
||||
#ifndef YAZE_APP_ZELDA3_SCREEN_DUNGEON_MAP_H
|
||||
#define YAZE_APP_ZELDA3_SCREEN_DUNGEON_MAP_H
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/render/tilemap.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze::zelda3 {
|
||||
|
||||
constexpr int kDungeonMapRoomsPtr = 0x57605; // 14 pointers of map data
|
||||
constexpr int kDungeonMapFloors = 0x575D9; // 14 words values
|
||||
|
||||
constexpr int kDungeonMapGfxPtr = 0x57BE4; // 14 pointers of gfx data
|
||||
|
||||
// data start for floors/gfx MUST skip 575D9 to 57621 (pointers)
|
||||
constexpr int kDungeonMapDataStart = 0x57039;
|
||||
|
||||
// IF Byte = 0xB9 dungeon maps are not expanded
|
||||
constexpr int kDungeonMapExpCheck = 0x56652; // $0A:E652
|
||||
constexpr int kDungeonMapTile16 = 0x57009; // $0A:F009
|
||||
constexpr int kDungeonMapTile16Expanded = 0x109010; // $21:9010
|
||||
|
||||
// 14 words values 0x000F = no boss
|
||||
constexpr int kDungeonMapBossRooms = 0x56807;
|
||||
constexpr int kTriforceVertices = 0x04FFD2; // group of 3, X, Y ,Z
|
||||
constexpr int kTriforceFaces = 0x04FFE4; // group of 5
|
||||
|
||||
constexpr int kCrystalVertices = 0x04FF98;
|
||||
|
||||
constexpr int kNumDungeons = 14;
|
||||
constexpr int kNumRooms = 25;
|
||||
constexpr int kNumDungeonMapTile16 = 186;
|
||||
|
||||
/**
|
||||
* @brief DungeonMap represents the map menu for a dungeon.
|
||||
*/
|
||||
struct DungeonMap {
|
||||
unsigned short boss_room = 0xFFFF;
|
||||
unsigned char nbr_of_floor = 0;
|
||||
unsigned char nbr_of_basement = 0;
|
||||
std::vector<std::array<uint8_t, kNumRooms>> floor_rooms;
|
||||
std::vector<std::array<uint8_t, kNumRooms>> floor_gfx;
|
||||
|
||||
DungeonMap(unsigned short boss_room, unsigned char nbr_of_floor,
|
||||
unsigned char nbr_of_basement,
|
||||
const std::vector<std::array<uint8_t, kNumRooms>> &floor_rooms,
|
||||
const std::vector<std::array<uint8_t, kNumRooms>> &floor_gfx)
|
||||
: boss_room(boss_room),
|
||||
nbr_of_floor(nbr_of_floor),
|
||||
nbr_of_basement(nbr_of_basement),
|
||||
floor_rooms(floor_rooms),
|
||||
floor_gfx(floor_gfx) {}
|
||||
};
|
||||
|
||||
using DungeonMapLabels =
|
||||
std::array<std::vector<std::array<std::string, kNumRooms>>, kNumDungeons>;
|
||||
|
||||
/**
|
||||
* @brief Load the dungeon maps from the ROM.
|
||||
*
|
||||
* @param rom
|
||||
* @param dungeon_map_labels
|
||||
* @return absl::StatusOr<std::vector<DungeonMap>>
|
||||
*/
|
||||
absl::StatusOr<std::vector<DungeonMap>> LoadDungeonMaps(
|
||||
Rom &rom, DungeonMapLabels &dungeon_map_labels);
|
||||
|
||||
/**
|
||||
* @brief Save the dungeon maps to the ROM.
|
||||
*
|
||||
* @param rom
|
||||
* @param dungeon_maps
|
||||
*/
|
||||
absl::Status SaveDungeonMaps(Rom &rom, std::vector<DungeonMap> &dungeon_maps);
|
||||
|
||||
/**
|
||||
* @brief Load the dungeon map tile16 from the ROM.
|
||||
*
|
||||
* @param tile16_blockset
|
||||
* @param rom
|
||||
* @param gfx_data
|
||||
* @param bin_mode
|
||||
*/
|
||||
absl::Status LoadDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom,
|
||||
const std::vector<uint8_t> &gfx_data,
|
||||
bool bin_mode);
|
||||
|
||||
/**
|
||||
* @brief Save the dungeon map tile16 to the ROM.
|
||||
*
|
||||
* @param tile16_blockset
|
||||
* @param rom
|
||||
*/
|
||||
absl::Status SaveDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom);
|
||||
|
||||
/**
|
||||
* @brief Load the dungeon map gfx from binary.
|
||||
*
|
||||
* @param rom
|
||||
* @param tile16_blockset
|
||||
* @param sheets
|
||||
* @param gfx_bin_data
|
||||
*/
|
||||
absl::Status LoadDungeonMapGfxFromBinary(Rom &rom,
|
||||
gfx::Tilemap &tile16_blockset,
|
||||
std::array<gfx::Bitmap, 4> &sheets,
|
||||
std::vector<uint8_t> &gfx_bin_data);
|
||||
} // namespace yaze::zelda3
|
||||
|
||||
#endif // YAZE_APP_ZELDA3_SCREEN_DUNGEON_MAP_H
|
||||
156
src/zelda3/screen/inventory.cc
Normal file
156
src/zelda3/screen/inventory.cc
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "inventory.h"
|
||||
|
||||
#include "app/gfx/backend/irenderer.h"
|
||||
#include "app/platform/window.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gfx/types/snes_tile.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/snes.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
absl::Status Inventory::Create(Rom* rom) {
|
||||
if (!rom || !rom->is_loaded()) {
|
||||
return absl::InvalidArgumentError("ROM is not loaded");
|
||||
}
|
||||
|
||||
// Build the tileset first (loads 2BPP graphics)
|
||||
RETURN_IF_ERROR(BuildTileset(rom));
|
||||
|
||||
// Load item icons from ROM
|
||||
RETURN_IF_ERROR(LoadItemIcons(rom));
|
||||
|
||||
// TODO(scawful): For now, create a simple display bitmap
|
||||
// Future: Oracle of Secrets menu editor will handle full menu layout
|
||||
data_.reserve(256 * 256);
|
||||
for (int i = 0; i < 256 * 256; i++) {
|
||||
data_.push_back(0xFF);
|
||||
}
|
||||
|
||||
bitmap_.Create(256, 256, 8, data_);
|
||||
bitmap_.SetPalette(palette_);
|
||||
|
||||
// Queue texture creation via Arena's deferred system
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &bitmap_);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status Inventory::BuildTileset(Rom* rom) {
|
||||
tilesheets_.reserve(6 * 0x2000);
|
||||
for (int i = 0; i < 6 * 0x2000; i++) tilesheets_.push_back(0xFF);
|
||||
ASSIGN_OR_RETURN(tilesheets_, Load2BppGraphics(*rom));
|
||||
std::vector<uint8_t> test;
|
||||
for (int i = 0; i < 0x4000; i++) {
|
||||
test_.push_back(tilesheets_[i]);
|
||||
}
|
||||
for (int i = 0x8000; i < +0x8000 + 0x2000; i++) {
|
||||
test_.push_back(tilesheets_[i]);
|
||||
}
|
||||
tilesheets_bmp_.Create(128, 0x130, 64, test_);
|
||||
auto hud_pal_group = rom->palette_group().hud;
|
||||
palette_ = hud_pal_group[0];
|
||||
tilesheets_bmp_.SetPalette(palette_);
|
||||
|
||||
// Queue texture creation via Arena's deferred system
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &tilesheets_bmp_);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status Inventory::LoadItemIcons(Rom* rom) {
|
||||
// Convert SNES address to PC address
|
||||
int pc_addr = SnesToPc(kItemIconsPtr);
|
||||
|
||||
// Define icon categories and their ROM offsets (relative to kItemIconsPtr)
|
||||
// Based on bank_0D.asm ItemIcons structure
|
||||
struct IconDef {
|
||||
int offset;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// Bow icons (.bows section)
|
||||
std::vector<IconDef> bow_icons = {
|
||||
{0x00, "No bow"},
|
||||
{0x08, "Empty bow"},
|
||||
{0x10, "Bow and arrows"},
|
||||
{0x18, "Empty silvers bow"},
|
||||
{0x20, "Silver bow and arrows"}
|
||||
};
|
||||
|
||||
// Boomerang icons (.booms section)
|
||||
std::vector<IconDef> boom_icons = {
|
||||
{0x28, "No boomerang"},
|
||||
{0x30, "Blue boomerang"},
|
||||
{0x38, "Red boomerang"}
|
||||
};
|
||||
|
||||
// Hookshot icons (.hook section)
|
||||
std::vector<IconDef> hook_icons = {
|
||||
{0x40, "No hookshot"},
|
||||
{0x48, "Hookshot"}
|
||||
};
|
||||
|
||||
// Bomb icons (.bombs section)
|
||||
std::vector<IconDef> bomb_icons = {
|
||||
{0x50, "No bombs"},
|
||||
{0x58, "Bombs"}
|
||||
};
|
||||
|
||||
// Load all icon categories
|
||||
auto load_icons = [&](const std::vector<IconDef>& icons) -> absl::Status {
|
||||
for (const auto& icon_def : icons) {
|
||||
ItemIcon icon;
|
||||
int icon_addr = pc_addr + icon_def.offset;
|
||||
|
||||
ASSIGN_OR_RETURN(icon.tile_tl, rom->ReadWord(icon_addr));
|
||||
ASSIGN_OR_RETURN(icon.tile_tr, rom->ReadWord(icon_addr + 2));
|
||||
ASSIGN_OR_RETURN(icon.tile_bl, rom->ReadWord(icon_addr + 4));
|
||||
ASSIGN_OR_RETURN(icon.tile_br, rom->ReadWord(icon_addr + 6));
|
||||
icon.name = icon_def.name;
|
||||
|
||||
item_icons_.push_back(icon);
|
||||
}
|
||||
return absl::OkStatus();
|
||||
};
|
||||
|
||||
RETURN_IF_ERROR(load_icons(bow_icons));
|
||||
RETURN_IF_ERROR(load_icons(boom_icons));
|
||||
RETURN_IF_ERROR(load_icons(hook_icons));
|
||||
RETURN_IF_ERROR(load_icons(bomb_icons));
|
||||
|
||||
// TODO(scawful): Load remaining icon categories:
|
||||
// - Mushroom/Powder (.shroom)
|
||||
// - Magic powder (.powder)
|
||||
// - Fire rod (.fires)
|
||||
// - Ice rod (.ices)
|
||||
// - Bombos medallion (.bombos)
|
||||
// - Ether medallion (.ether)
|
||||
// - Quake medallion (.quake)
|
||||
// - Lantern (.lamp)
|
||||
// - Hammer (.hammer)
|
||||
// - Flute (.flute)
|
||||
// - Bug net (.net)
|
||||
// - Book of Mudora (.book)
|
||||
// - Bottles (.bottles) - Multiple variants (empty, red potion, green potion, etc.)
|
||||
// - Cane of Somaria (.canes)
|
||||
// - Cane of Byrna (.byrn)
|
||||
// - Magic cape (.cape)
|
||||
// - Magic mirror (.mirror)
|
||||
// - Gloves (.glove)
|
||||
// - Boots (.boots)
|
||||
// - Flippers (.flippers)
|
||||
// - Moon pearl (.pearl)
|
||||
// - Swords (.swords)
|
||||
// - Shields (.shields)
|
||||
// - Armor (.armor)
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
77
src/zelda3/screen/inventory.h
Normal file
77
src/zelda3/screen/inventory.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#ifndef YAZE_APP_ZELDA3_INVENTORY_H
|
||||
#define YAZE_APP_ZELDA3_INVENTORY_H
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
#include "app/gfx/types/snes_tile.h"
|
||||
#include "app/gui/canvas/canvas.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
constexpr int kInventoryStart = 0x6564A;
|
||||
// ItemIcons base address in SNES format (0x0DF629)
|
||||
constexpr int kItemIconsPtr = 0x0DF629;
|
||||
|
||||
/**
|
||||
* @brief Represents a single item icon (2x2 tiles = 4 tile words)
|
||||
*/
|
||||
struct ItemIcon {
|
||||
uint16_t tile_tl; // Top-left tile word (vhopppcc cccccccc format)
|
||||
uint16_t tile_tr; // Top-right tile word
|
||||
uint16_t tile_bl; // Bottom-left tile word
|
||||
uint16_t tile_br; // Bottom-right tile word
|
||||
std::string name; // Human-readable name for debugging
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Inventory manages the inventory screen graphics and layout.
|
||||
*
|
||||
* The inventory screen consists of a 256x256 bitmap displaying equipment,
|
||||
* items, and UI elements using 2BPP graphics and HUD palette.
|
||||
*/
|
||||
class Inventory {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialize and load inventory screen data from ROM
|
||||
* @param rom ROM instance to read data from
|
||||
*/
|
||||
absl::Status Create(Rom* rom);
|
||||
|
||||
auto &bitmap() { return bitmap_; }
|
||||
auto &tilesheet() { return tilesheets_bmp_; }
|
||||
auto &palette() { return palette_; }
|
||||
auto &item_icons() { return item_icons_; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Build the tileset from 2BPP graphics
|
||||
* @param rom ROM instance to read graphics from
|
||||
*/
|
||||
absl::Status BuildTileset(Rom* rom);
|
||||
|
||||
/**
|
||||
* @brief Load individual item icons from ROM
|
||||
* @param rom ROM instance to read icon data from
|
||||
*/
|
||||
absl::Status LoadItemIcons(Rom* rom);
|
||||
|
||||
std::vector<uint8_t> data_;
|
||||
gfx::Bitmap bitmap_;
|
||||
|
||||
std::vector<uint8_t> tilesheets_;
|
||||
std::vector<uint8_t> test_;
|
||||
gfx::Bitmap tilesheets_bmp_;
|
||||
gfx::SnesPalette palette_;
|
||||
|
||||
gui::Canvas canvas_;
|
||||
std::vector<gfx::TileInfo> tiles_;
|
||||
std::vector<ItemIcon> item_icons_;
|
||||
};
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_ZELDA3_INVENTORY_H
|
||||
365
src/zelda3/screen/overworld_map_screen.cc
Normal file
365
src/zelda3/screen/overworld_map_screen.cc
Normal file
@@ -0,0 +1,365 @@
|
||||
#include "zelda3/screen/overworld_map_screen.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gfx/types/snes_color.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/snes.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
absl::Status OverworldMapScreen::Create(Rom* rom) {
|
||||
if (!rom || !rom->is_loaded()) {
|
||||
return absl::InvalidArgumentError("ROM is not loaded");
|
||||
}
|
||||
|
||||
// Set metadata for overworld map bitmaps
|
||||
// Mode 7 graphics use full 128-color palettes
|
||||
|
||||
// Load Mode 7 graphics (256 tiles, 8x8 pixels each, 8BPP)
|
||||
const int mode7_gfx_addr = 0x0C4000;
|
||||
std::vector<uint8_t> mode7_gfx_raw(0x4000); // Raw tileset data from ROM
|
||||
|
||||
for (int i = 0; i < 0x4000; i++) {
|
||||
ASSIGN_OR_RETURN(mode7_gfx_raw[i], rom->ReadByte(mode7_gfx_addr + i));
|
||||
}
|
||||
|
||||
// Mode 7 tiles are stored in tiled format (each tile's rows are consecutive)
|
||||
// but we need linear bitmap format (all tiles' first rows, then all second rows)
|
||||
// Convert from tiled to linear bitmap layout
|
||||
std::vector<uint8_t> mode7_gfx(0x4000);
|
||||
int pos = 0;
|
||||
for (int sy = 0; sy < 16 * 1024; sy += 1024) { // 16 rows of tiles
|
||||
for (int sx = 0; sx < 16 * 8; sx += 8) { // 16 columns of tiles
|
||||
for (int y = 0; y < 8 * 128; y += 128) { // 8 pixel rows within tile
|
||||
for (int x = 0; x < 8; x++) { // 8 pixels per row
|
||||
mode7_gfx[x + sx + y + sy] = mode7_gfx_raw[pos];
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create tiles8 bitmap: 128×128 pixels (16×16 tiles = 256 tiles)
|
||||
tiles8_bitmap_.Create(128, 128, 8, mode7_gfx);
|
||||
tiles8_bitmap_.metadata().source_bpp = 8;
|
||||
tiles8_bitmap_.metadata().palette_format = 0;
|
||||
tiles8_bitmap_.metadata().source_type = "mode7_tileset";
|
||||
tiles8_bitmap_.metadata().palette_colors = 128;
|
||||
|
||||
// Create map bitmap (512x512 for 64x64 tiles at 8x8 each)
|
||||
map_bitmap_.Create(512, 512, 8, std::vector<uint8_t>(512 * 512));
|
||||
map_bitmap_.metadata().source_bpp = 8;
|
||||
map_bitmap_.metadata().palette_format = 0;
|
||||
map_bitmap_.metadata().source_type = "mode7_map";
|
||||
map_bitmap_.metadata().palette_colors = 128;
|
||||
|
||||
// Light World palette at 0x055B27
|
||||
const int lw_pal_addr = 0x055B27;
|
||||
for (int i = 0; i < 128; i++) {
|
||||
ASSIGN_OR_RETURN(uint16_t snes_color, rom->ReadWord(lw_pal_addr + (i * 2)));
|
||||
// Create SnesColor directly from SNES 15-bit format
|
||||
lw_palette_.AddColor(gfx::SnesColor(snes_color));
|
||||
}
|
||||
|
||||
// Dark World palette at 0x055C27
|
||||
const int dw_pal_addr = 0x055C27;
|
||||
for (int i = 0; i < 128; i++) {
|
||||
ASSIGN_OR_RETURN(uint16_t snes_color, rom->ReadWord(dw_pal_addr + (i * 2)));
|
||||
// Create SnesColor directly from SNES 15-bit format
|
||||
dw_palette_.AddColor(gfx::SnesColor(snes_color));
|
||||
}
|
||||
|
||||
// Load map tile data
|
||||
RETURN_IF_ERROR(LoadMapData(rom));
|
||||
|
||||
// Render initial map (Light World)
|
||||
RETURN_IF_ERROR(RenderMapLayer(false));
|
||||
|
||||
// Apply palettes AFTER bitmaps are fully initialized
|
||||
tiles8_bitmap_.SetPalette(lw_palette_);
|
||||
map_bitmap_.SetPalette(lw_palette_); // Map also needs palette
|
||||
|
||||
// Ensure bitmaps are marked as active
|
||||
tiles8_bitmap_.set_active(true);
|
||||
map_bitmap_.set_active(true);
|
||||
|
||||
// Queue texture creation
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &tiles8_bitmap_);
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &map_bitmap_);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status OverworldMapScreen::LoadMapData(Rom* rom) {
|
||||
// Map data is stored in interleaved format across 4 sections + 1 DW section
|
||||
// Based on ZScream's Constants.IDKZarby = 0x054727
|
||||
// The data alternates between left (32 columns) and right (32 columns)
|
||||
// for the first 2048 tiles, then continues for bottom half
|
||||
|
||||
const int base_addr = 0x054727; // IDKZarby constant from ZScream
|
||||
int p1 = base_addr + 0x0000; // Top-left quadrant data
|
||||
int p2 = base_addr + 0x0400; // Top-right quadrant data
|
||||
int p3 = base_addr + 0x0800; // Bottom-left quadrant data
|
||||
int p4 = base_addr + 0x0C00; // Bottom-right quadrant data
|
||||
int p5 = base_addr + 0x1000; // Dark World additional section
|
||||
|
||||
bool rSide = false; // false = left side, true = right side
|
||||
int cSide = 0; // Column counter within side (0-31)
|
||||
int count = 0; // Output tile index
|
||||
|
||||
// Load 64x64 map with interleaved left/right format
|
||||
while (count < 64 * 64) {
|
||||
if (count < 0x800) { // Top half (first 2048 tiles)
|
||||
if (!rSide) {
|
||||
// Read from left side (p1)
|
||||
ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p1));
|
||||
lw_map_tiles_[count] = tile;
|
||||
dw_map_tiles_[count] = tile;
|
||||
p1++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = true;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Read from right side (p2)
|
||||
ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p2));
|
||||
lw_map_tiles_[count] = tile;
|
||||
dw_map_tiles_[count] = tile;
|
||||
p2++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = false;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else { // Bottom half (remaining 2048 tiles)
|
||||
if (!rSide) {
|
||||
// Read from left side (p3)
|
||||
ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p3));
|
||||
lw_map_tiles_[count] = tile;
|
||||
dw_map_tiles_[count] = tile;
|
||||
p3++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = true;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Read from right side (p4)
|
||||
ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p4));
|
||||
lw_map_tiles_[count] = tile;
|
||||
dw_map_tiles_[count] = tile;
|
||||
p4++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = false;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cSide++;
|
||||
count++;
|
||||
}
|
||||
|
||||
// Load Dark World specific data (bottom-right 32x32 section)
|
||||
count = 0;
|
||||
int line = 0;
|
||||
while (true) {
|
||||
ASSIGN_OR_RETURN(uint8_t tile, rom->ReadByte(p5));
|
||||
dw_map_tiles_[1040 + count + (line * 64)] = tile;
|
||||
p5++;
|
||||
count++;
|
||||
if (count >= 32) {
|
||||
count = 0;
|
||||
line++;
|
||||
if (line >= 32) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status OverworldMapScreen::RenderMapLayer(bool use_dark_world) {
|
||||
auto& map_data = map_bitmap_.mutable_data();
|
||||
const auto& tiles8_data = tiles8_bitmap_.vector();
|
||||
const auto& tile_source = use_dark_world ? dw_map_tiles_ : lw_map_tiles_;
|
||||
|
||||
// Render 64x64 tiles (each 8x8 pixels) into 512x512 bitmap
|
||||
for (int yy = 0; yy < 64; yy++) {
|
||||
for (int xx = 0; xx < 64; xx++) {
|
||||
uint8_t tile_id = tile_source[xx + (yy * 64)];
|
||||
|
||||
// Calculate tile position in tiles8_bitmap (16 tiles per row)
|
||||
int tile_x = (tile_id % 16) * 8;
|
||||
int tile_y = (tile_id / 16) * 8;
|
||||
|
||||
// Copy 8x8 tile pixels
|
||||
for (int py = 0; py < 8; py++) {
|
||||
for (int px = 0; px < 8; px++) {
|
||||
int src_index = (tile_x + px) + ((tile_y + py) * 128);
|
||||
int dest_index = (xx * 8 + px) + ((yy * 8 + py) * 512);
|
||||
|
||||
if (src_index < tiles8_data.size() && dest_index < map_data.size()) {
|
||||
map_data[dest_index] = tiles8_data[src_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy pixel data to SDL surface
|
||||
map_bitmap_.UpdateSurfacePixels();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status OverworldMapScreen::Save(Rom* rom) {
|
||||
// Write data back in the same interleaved format
|
||||
const int base_addr = 0x054727;
|
||||
int p1 = base_addr + 0x0000;
|
||||
int p2 = base_addr + 0x0400;
|
||||
int p3 = base_addr + 0x0800;
|
||||
int p4 = base_addr + 0x0C00;
|
||||
int p5 = base_addr + 0x1000;
|
||||
|
||||
bool rSide = false;
|
||||
int cSide = 0;
|
||||
int count = 0;
|
||||
|
||||
// Write 64x64 map with interleaved left/right format
|
||||
while (count < 64 * 64) {
|
||||
if (count < 0x800) {
|
||||
if (!rSide) {
|
||||
RETURN_IF_ERROR(rom->WriteByte(p1, lw_map_tiles_[count]));
|
||||
p1++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = true;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
RETURN_IF_ERROR(rom->WriteByte(p2, lw_map_tiles_[count]));
|
||||
p2++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = false;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!rSide) {
|
||||
RETURN_IF_ERROR(rom->WriteByte(p3, lw_map_tiles_[count]));
|
||||
p3++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = true;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
RETURN_IF_ERROR(rom->WriteByte(p4, lw_map_tiles_[count]));
|
||||
p4++;
|
||||
|
||||
if (cSide >= 31) {
|
||||
cSide = 0;
|
||||
rSide = false;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cSide++;
|
||||
count++;
|
||||
}
|
||||
|
||||
// Write Dark World specific data
|
||||
count = 0;
|
||||
int line = 0;
|
||||
while (true) {
|
||||
RETURN_IF_ERROR(rom->WriteByte(p5, dw_map_tiles_[1040 + count + (line * 64)]));
|
||||
p5++;
|
||||
count++;
|
||||
if (count >= 32) {
|
||||
count = 0;
|
||||
line++;
|
||||
if (line >= 32) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status OverworldMapScreen::LoadCustomMap(const std::string& file_path) {
|
||||
// Load custom map from external binary file
|
||||
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open()) {
|
||||
return absl::NotFoundError("Could not open custom map file: " + file_path);
|
||||
}
|
||||
|
||||
std::streamsize size = file.tellg();
|
||||
if (size != 4096) {
|
||||
return absl::InvalidArgumentError(
|
||||
"Custom map file must be exactly 4096 bytes (64×64 tiles)");
|
||||
}
|
||||
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Read into Light World map buffer (could add option for Dark World later)
|
||||
file.read(reinterpret_cast<char*>(lw_map_tiles_.data()), 4096);
|
||||
|
||||
if (!file) {
|
||||
return absl::InternalError("Failed to read custom map data");
|
||||
}
|
||||
|
||||
// Re-render with new data
|
||||
RETURN_IF_ERROR(RenderMapLayer(false));
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::UPDATE, &map_bitmap_);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status OverworldMapScreen::SaveCustomMap(const std::string& file_path,
|
||||
bool use_dark_world) {
|
||||
std::ofstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return absl::InternalError("Could not create custom map file: " + file_path);
|
||||
}
|
||||
|
||||
const auto& tiles = use_dark_world ? dw_map_tiles_ : lw_map_tiles_;
|
||||
file.write(reinterpret_cast<const char*>(tiles.data()), tiles.size());
|
||||
|
||||
if (!file) {
|
||||
return absl::InternalError("Failed to write custom map data");
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
94
src/zelda3/screen/overworld_map_screen.h
Normal file
94
src/zelda3/screen/overworld_map_screen.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#ifndef YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H
|
||||
#define YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
/**
|
||||
* @brief OverworldMapScreen manages the overworld map (pause menu) graphics.
|
||||
*
|
||||
* The overworld map screen shows the mini-map when the player pauses.
|
||||
* It consists of:
|
||||
* - 64x64 tiles (8x8 pixels each) for Light World map
|
||||
* - 64x64 tiles (8x8 pixels each) for Dark World map
|
||||
* - Mode 7 graphics stored at 0x0C4000
|
||||
* - Tile data in interleaved format across 4 sections
|
||||
*/
|
||||
class OverworldMapScreen {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialize and load overworld map data from ROM
|
||||
* @param rom ROM instance to read data from
|
||||
*/
|
||||
absl::Status Create(Rom* rom);
|
||||
|
||||
/**
|
||||
* @brief Save changes back to ROM
|
||||
* @param rom ROM instance to write data to
|
||||
*/
|
||||
absl::Status Save(Rom* rom);
|
||||
|
||||
// Accessors for tile data
|
||||
auto& lw_tiles() { return lw_map_tiles_; }
|
||||
auto& dw_tiles() { return dw_map_tiles_; }
|
||||
|
||||
// Mutable accessors for editing
|
||||
auto& mutable_lw_tiles() { return lw_map_tiles_; }
|
||||
auto& mutable_dw_tiles() { return dw_map_tiles_; }
|
||||
|
||||
// Bitmap accessors
|
||||
auto& tiles8_bitmap() { return tiles8_bitmap_; }
|
||||
auto& map_bitmap() { return map_bitmap_; }
|
||||
|
||||
// Palette accessors
|
||||
auto& lw_palette() { return lw_palette_; }
|
||||
auto& dw_palette() { return dw_palette_; }
|
||||
|
||||
/**
|
||||
* @brief Render map tiles into bitmap
|
||||
* @param use_dark_world If true, render DW tiles, otherwise LW tiles
|
||||
*/
|
||||
absl::Status RenderMapLayer(bool use_dark_world);
|
||||
|
||||
/**
|
||||
* @brief Load custom map from external binary file
|
||||
* @param file_path Path to .bin file containing 64×64 tile indices
|
||||
*/
|
||||
absl::Status LoadCustomMap(const std::string& file_path);
|
||||
|
||||
/**
|
||||
* @brief Save map data to external binary file
|
||||
* @param file_path Path to output .bin file
|
||||
* @param use_dark_world If true, save DW tiles, otherwise LW tiles
|
||||
*/
|
||||
absl::Status SaveCustomMap(const std::string& file_path, bool use_dark_world);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Load map tile data from ROM
|
||||
* Reads the interleaved tile format from 4 ROM sections
|
||||
*/
|
||||
absl::Status LoadMapData(Rom* rom);
|
||||
|
||||
std::array<uint8_t, 64 * 64> lw_map_tiles_; // Light World tile indices
|
||||
std::array<uint8_t, 64 * 64> dw_map_tiles_; // Dark World tile indices
|
||||
|
||||
gfx::Bitmap tiles8_bitmap_; // 128x128 tileset (mode 7 graphics)
|
||||
gfx::Bitmap map_bitmap_; // 512x512 rendered map (64 tiles × 8 pixels)
|
||||
|
||||
gfx::SnesPalette lw_palette_; // Light World palette
|
||||
gfx::SnesPalette dw_palette_; // Dark World palette
|
||||
};
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_ZELDA3_OVERWORLD_MAP_SCREEN_H
|
||||
|
||||
718
src/zelda3/screen/title_screen.cc
Normal file
718
src/zelda3/screen/title_screen.cc
Normal file
@@ -0,0 +1,718 @@
|
||||
#include "title_screen.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/snes.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
absl::Status TitleScreen::Create(Rom* rom) {
|
||||
if (!rom || !rom->is_loaded()) {
|
||||
return absl::InvalidArgumentError("ROM is not loaded");
|
||||
}
|
||||
|
||||
// Initialize bitmaps for each layer
|
||||
tiles8_bitmap_.Create(128, 512, 8, std::vector<uint8_t>(0x20000));
|
||||
tiles_bg1_bitmap_.Create(256, 256, 8, std::vector<uint8_t>(0x80000));
|
||||
tiles_bg2_bitmap_.Create(256, 256, 8, std::vector<uint8_t>(0x80000));
|
||||
oam_bg_bitmap_.Create(256, 256, 8, std::vector<uint8_t>(0x80000));
|
||||
|
||||
// Set metadata for title screen bitmaps
|
||||
// Title screen uses 3BPP graphics (like all LTTP data) with composite 64-color palette
|
||||
tiles8_bitmap_.metadata().source_bpp = 3;
|
||||
tiles8_bitmap_.metadata().palette_format = 0; // Full 64-color palette
|
||||
tiles8_bitmap_.metadata().source_type = "graphics_sheet";
|
||||
tiles8_bitmap_.metadata().palette_colors = 64;
|
||||
|
||||
tiles_bg1_bitmap_.metadata().source_bpp = 3;
|
||||
tiles_bg1_bitmap_.metadata().palette_format = 0; // Uses full palette with sub-palette indexing
|
||||
tiles_bg1_bitmap_.metadata().source_type = "screen_buffer";
|
||||
tiles_bg1_bitmap_.metadata().palette_colors = 64;
|
||||
|
||||
tiles_bg2_bitmap_.metadata().source_bpp = 3;
|
||||
tiles_bg2_bitmap_.metadata().palette_format = 0;
|
||||
tiles_bg2_bitmap_.metadata().source_type = "screen_buffer";
|
||||
tiles_bg2_bitmap_.metadata().palette_colors = 64;
|
||||
|
||||
oam_bg_bitmap_.metadata().source_bpp = 3;
|
||||
oam_bg_bitmap_.metadata().palette_format = 0;
|
||||
oam_bg_bitmap_.metadata().source_type = "screen_buffer";
|
||||
oam_bg_bitmap_.metadata().palette_colors = 64;
|
||||
|
||||
// Initialize composite bitmap for stacked BG rendering (256x256 = 65536 bytes)
|
||||
title_composite_bitmap_.Create(256, 256, 8, std::vector<uint8_t>(256 * 256));
|
||||
title_composite_bitmap_.metadata().source_bpp = 3;
|
||||
title_composite_bitmap_.metadata().palette_format = 0;
|
||||
title_composite_bitmap_.metadata().source_type = "screen_buffer";
|
||||
title_composite_bitmap_.metadata().palette_colors = 64;
|
||||
|
||||
// Initialize tilemap buffers
|
||||
tiles_bg1_buffer_.fill(0x492); // Default empty tile
|
||||
tiles_bg2_buffer_.fill(0x492);
|
||||
|
||||
// Load palette (title screen uses 3BPP graphics with 8 palettes of 8 colors each)
|
||||
// Build composite palette from multiple sources (matches ZScream's SetColorsPalette)
|
||||
// Palette 0: OverworldMainPalettes[5]
|
||||
// Palette 1: OverworldAnimatedPalettes[0]
|
||||
// Palette 2: OverworldAuxPalettes[3]
|
||||
// Palette 3: OverworldAuxPalettes[3]
|
||||
// Palette 4: HudPalettes[0]
|
||||
// Palette 5: Transparent/black
|
||||
// Palette 6: SpritesAux1Palettes[1]
|
||||
// Palette 7: SpritesAux1Palettes[1]
|
||||
|
||||
auto pal_group = rom->palette_group();
|
||||
|
||||
// Add each 8-color palette in sequence (EXACTLY 8 colors each for 64 total)
|
||||
size_t palette_start = palette_.size();
|
||||
|
||||
// Palette 0: OverworldMainPalettes[5]
|
||||
if (pal_group.overworld_main.size() > 5) {
|
||||
const auto& src = pal_group.overworld_main[5];
|
||||
size_t added = 0;
|
||||
for (size_t i = 0; i < 8 && i < src.size(); i++) {
|
||||
palette_.AddColor(src[i]);
|
||||
added++;
|
||||
}
|
||||
// Pad with black if less than 8 colors
|
||||
while (added < 8) {
|
||||
palette_.AddColor(gfx::SnesColor(0, 0, 0));
|
||||
added++;
|
||||
}
|
||||
LOG_INFO("TitleScreen", "Palette 0: added %zu colors from overworld_main[5]", added);
|
||||
}
|
||||
|
||||
// Palette 1: OverworldAnimatedPalettes[0]
|
||||
if (pal_group.overworld_animated.size() > 0) {
|
||||
const auto& src = pal_group.overworld_animated[0];
|
||||
size_t added = 0;
|
||||
for (size_t i = 0; i < 8 && i < src.size(); i++) {
|
||||
palette_.AddColor(src[i]);
|
||||
added++;
|
||||
}
|
||||
while (added < 8) {
|
||||
palette_.AddColor(gfx::SnesColor(0, 0, 0));
|
||||
added++;
|
||||
}
|
||||
LOG_INFO("TitleScreen", "Palette 1: added %zu colors from overworld_animated[0]", added);
|
||||
}
|
||||
|
||||
// Palette 2 & 3: OverworldAuxPalettes[3] (used twice)
|
||||
if (pal_group.overworld_aux.size() > 3) {
|
||||
auto src = pal_group.overworld_aux[3]; // Copy, as this returns by value
|
||||
for (int pal = 0; pal < 2; pal++) {
|
||||
size_t added = 0;
|
||||
for (size_t i = 0; i < 8 && i < src.size(); i++) {
|
||||
palette_.AddColor(src[i]);
|
||||
added++;
|
||||
}
|
||||
while (added < 8) {
|
||||
palette_.AddColor(gfx::SnesColor(0, 0, 0));
|
||||
added++;
|
||||
}
|
||||
LOG_INFO("TitleScreen", "Palette %d: added %zu colors from overworld_aux[3]", 2+pal, added);
|
||||
}
|
||||
}
|
||||
|
||||
// Palette 4: HudPalettes[0]
|
||||
if (pal_group.hud.size() > 0) {
|
||||
auto src = pal_group.hud.palette(0); // Copy, as this returns by value
|
||||
size_t added = 0;
|
||||
for (size_t i = 0; i < 8 && i < src.size(); i++) {
|
||||
palette_.AddColor(src[i]);
|
||||
added++;
|
||||
}
|
||||
while (added < 8) {
|
||||
palette_.AddColor(gfx::SnesColor(0, 0, 0));
|
||||
added++;
|
||||
}
|
||||
LOG_INFO("TitleScreen", "Palette 4: added %zu colors from hud[0]", added);
|
||||
}
|
||||
|
||||
// Palette 5: 8 transparent/black colors
|
||||
for (int i = 0; i < 8; i++) {
|
||||
palette_.AddColor(gfx::SnesColor(0, 0, 0));
|
||||
}
|
||||
LOG_INFO("TitleScreen", "Palette 5: added 8 transparent/black colors");
|
||||
|
||||
// Palette 6 & 7: SpritesAux1Palettes[1] (used twice)
|
||||
if (pal_group.sprites_aux1.size() > 1) {
|
||||
auto src = pal_group.sprites_aux1[1]; // Copy, as this returns by value
|
||||
for (int pal = 0; pal < 2; pal++) {
|
||||
size_t added = 0;
|
||||
for (size_t i = 0; i < 8 && i < src.size(); i++) {
|
||||
palette_.AddColor(src[i]);
|
||||
added++;
|
||||
}
|
||||
while (added < 8) {
|
||||
palette_.AddColor(gfx::SnesColor(0, 0, 0));
|
||||
added++;
|
||||
}
|
||||
LOG_INFO("TitleScreen", "Palette %d: added %zu colors from sprites_aux1[1]", 6+pal, added);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("TitleScreen", "Built composite palette: %zu colors (should be 64)", palette_.size());
|
||||
|
||||
// Build tile16 blockset from graphics
|
||||
RETURN_IF_ERROR(BuildTileset(rom));
|
||||
|
||||
// Load tilemap data from ROM
|
||||
RETURN_IF_ERROR(LoadTitleScreen(rom));
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status TitleScreen::BuildTileset(Rom* rom) {
|
||||
// Title screen uses specific graphics sheets
|
||||
// Load sheet configuration from ROM (matches ZScream implementation)
|
||||
uint8_t staticgfx[16] = {0};
|
||||
|
||||
// Read title screen GFX group indices from ROM
|
||||
constexpr int kTitleScreenTilesGFX = 0x064207;
|
||||
constexpr int kTitleScreenSpritesGFX = 0x06420C;
|
||||
|
||||
ASSIGN_OR_RETURN(uint8_t tiles_gfx_index, rom->ReadByte(kTitleScreenTilesGFX));
|
||||
ASSIGN_OR_RETURN(uint8_t sprites_gfx_index, rom->ReadByte(kTitleScreenSpritesGFX));
|
||||
|
||||
LOG_INFO("TitleScreen", "GFX group indices: tiles=%d, sprites=%d",
|
||||
tiles_gfx_index, sprites_gfx_index);
|
||||
|
||||
// Load main graphics sheets (slots 0-7) from GFX groups
|
||||
// First, read the GFX groups pointer (2 bytes at 0x6237)
|
||||
constexpr int kGfxGroupsPointer = 0x6237;
|
||||
ASSIGN_OR_RETURN(uint16_t gfx_groups_snes, rom->ReadWord(kGfxGroupsPointer));
|
||||
uint32_t main_gfx_table = SnesToPc(gfx_groups_snes);
|
||||
|
||||
LOG_INFO("TitleScreen", "GFX groups table: SNES=0x%04X, PC=0x%06X",
|
||||
gfx_groups_snes, main_gfx_table);
|
||||
|
||||
// Read 8 bytes from mainGfx[tiles_gfx_index]
|
||||
int main_gfx_offset = main_gfx_table + (tiles_gfx_index * 8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
ASSIGN_OR_RETURN(staticgfx[i], rom->ReadByte(main_gfx_offset + i));
|
||||
}
|
||||
|
||||
// Load sprite graphics sheets (slots 8-12) - matches ZScream logic
|
||||
// Sprite GFX groups are after the 37 main groups (37 * 8 = 296 bytes)
|
||||
// and 82 room groups (82 * 4 = 328 bytes) = 624 bytes offset
|
||||
int sprite_gfx_table = main_gfx_table + (37 * 8) + (82 * 4);
|
||||
int sprite_gfx_offset = sprite_gfx_table + (sprites_gfx_index * 4);
|
||||
|
||||
staticgfx[8] = 115 + 0; // Title logo base
|
||||
ASSIGN_OR_RETURN(uint8_t sprite3, rom->ReadByte(sprite_gfx_offset + 3));
|
||||
staticgfx[9] = 115 + sprite3; // Sprite graphics slot 3
|
||||
staticgfx[10] = 115 + 6; // Additional graphics
|
||||
staticgfx[11] = 115 + 7; // Additional graphics
|
||||
ASSIGN_OR_RETURN(uint8_t sprite0, rom->ReadByte(sprite_gfx_offset + 0));
|
||||
staticgfx[12] = 115 + sprite0; // Sprite graphics slot 0
|
||||
staticgfx[13] = 112; // UI graphics
|
||||
staticgfx[14] = 112; // UI graphics
|
||||
staticgfx[15] = 112; // UI graphics
|
||||
|
||||
// Use pre-converted graphics from ROM buffer - simple and matches rest of yaze
|
||||
// Title screen uses standard 3BPP graphics, no special offset needed
|
||||
const auto& gfx_buffer = rom->graphics_buffer();
|
||||
auto& tiles8_data = tiles8_bitmap_.mutable_data();
|
||||
|
||||
LOG_INFO("TitleScreen", "Graphics buffer size: %zu bytes", gfx_buffer.size());
|
||||
LOG_INFO("TitleScreen", "Tiles8 bitmap size: %zu bytes", tiles8_data.size());
|
||||
|
||||
// Copy graphics sheets to tiles8_bitmap
|
||||
LOG_INFO("TitleScreen", "Loading 16 graphics sheets:");
|
||||
for (int i = 0; i < 16; i++) {
|
||||
LOG_INFO("TitleScreen", " staticgfx[%d] = %d", i, staticgfx[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int sheet_id = staticgfx[i];
|
||||
|
||||
// Validate sheet ID (ROM has 223 sheets: 0-222)
|
||||
if (sheet_id > 222) {
|
||||
LOG_ERROR("TitleScreen", "Sheet %d: Invalid sheet_id=%d (max 222), using sheet 0 instead",
|
||||
i, sheet_id);
|
||||
sheet_id = 0; // Fallback to a valid sheet
|
||||
}
|
||||
|
||||
int source_offset = sheet_id * 0x1000; // Each 8BPP sheet is 0x1000 bytes
|
||||
int dest_offset = i * 0x1000;
|
||||
|
||||
if (source_offset + 0x1000 <= gfx_buffer.size() &&
|
||||
dest_offset + 0x1000 <= tiles8_data.size()) {
|
||||
|
||||
std::copy(gfx_buffer.begin() + source_offset,
|
||||
gfx_buffer.begin() + source_offset + 0x1000,
|
||||
tiles8_data.begin() + dest_offset);
|
||||
|
||||
// Sample first few pixels
|
||||
LOG_INFO("TitleScreen", "Sheet %d (ID %d): Sample pixels: %02X %02X %02X %02X",
|
||||
i, sheet_id,
|
||||
tiles8_data[dest_offset], tiles8_data[dest_offset+1],
|
||||
tiles8_data[dest_offset+2], tiles8_data[dest_offset+3]);
|
||||
} else {
|
||||
LOG_ERROR("TitleScreen", "Sheet %d (ID %d): out of bounds! source=%d, dest=%d, buffer_size=%zu",
|
||||
i, sheet_id, source_offset, dest_offset, gfx_buffer.size());
|
||||
}
|
||||
}
|
||||
|
||||
// Set palette on tiles8 bitmap
|
||||
tiles8_bitmap_.SetPalette(palette_);
|
||||
|
||||
LOG_INFO("TitleScreen", "Applied palette to tiles8_bitmap: %zu colors", palette_.size());
|
||||
// Log first few colors
|
||||
if (palette_.size() >= 8) {
|
||||
LOG_INFO("TitleScreen", " Palette colors 0-7: %04X %04X %04X %04X %04X %04X %04X %04X",
|
||||
palette_[0].snes(), palette_[1].snes(), palette_[2].snes(), palette_[3].snes(),
|
||||
palette_[4].snes(), palette_[5].snes(), palette_[6].snes(), palette_[7].snes());
|
||||
}
|
||||
|
||||
// Queue texture creation via Arena's deferred system
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &tiles8_bitmap_);
|
||||
|
||||
// TODO: Build tile16 blockset from tile8 data
|
||||
// This would involve composing 16x16 tiles from 8x8 tiles
|
||||
// For now, we'll use the tile8 data directly
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status TitleScreen::LoadTitleScreen(Rom* rom) {
|
||||
// Check if ROM uses ZScream's expanded format (data at 0x108000 PC)
|
||||
// by reading the title screen pointer at 0x137A+3, 0x1383+3, 0x138C+3
|
||||
ASSIGN_OR_RETURN(uint8_t bank_byte, rom->ReadByte(0x138C + 3));
|
||||
ASSIGN_OR_RETURN(uint8_t high_byte, rom->ReadByte(0x1383 + 3));
|
||||
ASSIGN_OR_RETURN(uint8_t low_byte, rom->ReadByte(0x137A + 3));
|
||||
|
||||
uint32_t snes_addr = (bank_byte << 16) | (high_byte << 8) | low_byte;
|
||||
uint32_t pc_addr = SnesToPc(snes_addr);
|
||||
|
||||
LOG_INFO("TitleScreen", "Title screen pointer: SNES=0x%06X, PC=0x%06X", snes_addr, pc_addr);
|
||||
|
||||
// Initialize buffers with default empty tile
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
tiles_bg1_buffer_[i] = 0x492;
|
||||
tiles_bg2_buffer_[i] = 0x492;
|
||||
}
|
||||
|
||||
// ZScream expanded format at 0x108000 (PC)
|
||||
if (pc_addr >= 0x108000 && pc_addr <= 0x10FFFF) {
|
||||
LOG_INFO("TitleScreen", "Detected ZScream expanded format");
|
||||
|
||||
int pos = pc_addr;
|
||||
|
||||
// Read BG1 header: dest (word), length (word)
|
||||
ASSIGN_OR_RETURN(uint16_t bg1_dest, rom->ReadWord(pos));
|
||||
pos += 2;
|
||||
ASSIGN_OR_RETURN(uint16_t bg1_length, rom->ReadWord(pos));
|
||||
pos += 2;
|
||||
|
||||
LOG_INFO("TitleScreen", "BG1 Header: dest=0x%04X, length=0x%04X", bg1_dest, bg1_length);
|
||||
|
||||
// Read 1024 BG1 tiles (2 bytes each = 2048 bytes)
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
ASSIGN_OR_RETURN(uint16_t tile, rom->ReadWord(pos));
|
||||
tiles_bg1_buffer_[i] = tile;
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
// Read BG2 header: dest (word), length (word)
|
||||
ASSIGN_OR_RETURN(uint16_t bg2_dest, rom->ReadWord(pos));
|
||||
pos += 2;
|
||||
ASSIGN_OR_RETURN(uint16_t bg2_length, rom->ReadWord(pos));
|
||||
pos += 2;
|
||||
|
||||
LOG_INFO("TitleScreen", "BG2 Header: dest=0x%04X, length=0x%04X", bg2_dest, bg2_length);
|
||||
|
||||
// Read 1024 BG2 tiles (2 bytes each = 2048 bytes)
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
ASSIGN_OR_RETURN(uint16_t tile, rom->ReadWord(pos));
|
||||
tiles_bg2_buffer_[i] = tile;
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
LOG_INFO("TitleScreen", "Loaded 2048 tilemap entries from ZScream expanded format");
|
||||
}
|
||||
// Vanilla format: Sequential DMA blocks at pointer location
|
||||
// NOTE: This reads from the pointer but may not be the correct format
|
||||
// See docs/screen-editor-status.md for details on this ongoing issue
|
||||
else {
|
||||
LOG_INFO("TitleScreen", "Using vanilla DMA format (EXPERIMENTAL)");
|
||||
|
||||
int pos = pc_addr;
|
||||
int total_entries = 0;
|
||||
int blocks_read = 0;
|
||||
|
||||
// Read DMA blocks until we hit terminator or safety limit
|
||||
while (pos < rom->size() && blocks_read < 20) {
|
||||
// Read destination address (word)
|
||||
ASSIGN_OR_RETURN(uint16_t dest_addr, rom->ReadWord(pos));
|
||||
pos += 2;
|
||||
|
||||
// Check for terminator
|
||||
if (dest_addr == 0xFFFF || (dest_addr & 0xFF) == 0xFF) {
|
||||
LOG_INFO("TitleScreen", "Found DMA terminator at pos=0x%06X", pos - 2);
|
||||
break;
|
||||
}
|
||||
|
||||
// Read length/flags (word)
|
||||
ASSIGN_OR_RETURN(uint16_t length_flags, rom->ReadWord(pos));
|
||||
pos += 2;
|
||||
|
||||
bool increment64 = (length_flags & 0x8000) == 0x8000;
|
||||
bool fixsource = (length_flags & 0x4000) == 0x4000;
|
||||
int length = (length_flags & 0x0FFF);
|
||||
|
||||
LOG_INFO("TitleScreen", "Block %d: dest=0x%04X, len=%d, inc64=%d, fix=%d",
|
||||
blocks_read, dest_addr, length, increment64, fixsource);
|
||||
|
||||
int tile_count = (length / 2) + 1;
|
||||
int source_start = pos;
|
||||
|
||||
// Read tiles
|
||||
for (int j = 0; j < tile_count; j++) {
|
||||
ASSIGN_OR_RETURN(uint16_t tiledata, rom->ReadWord(pos));
|
||||
|
||||
// Determine which layer based on destination address
|
||||
if (dest_addr >= 0x1000 && dest_addr < 0x1400) {
|
||||
// BG1 layer
|
||||
int index = (dest_addr - 0x1000) / 2;
|
||||
if (index < 1024) {
|
||||
tiles_bg1_buffer_[index] = tiledata;
|
||||
total_entries++;
|
||||
}
|
||||
} else if (dest_addr < 0x0800) {
|
||||
// BG2 layer
|
||||
int index = dest_addr / 2;
|
||||
if (index < 1024) {
|
||||
tiles_bg2_buffer_[index] = tiledata;
|
||||
total_entries++;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance destination address
|
||||
if (increment64) {
|
||||
dest_addr += 64;
|
||||
} else {
|
||||
dest_addr += 2;
|
||||
}
|
||||
|
||||
// Advance source position
|
||||
if (!fixsource) {
|
||||
pos += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// If fixsource, only advance by one tile
|
||||
if (fixsource) {
|
||||
pos = source_start + 2;
|
||||
}
|
||||
|
||||
blocks_read++;
|
||||
}
|
||||
|
||||
LOG_INFO("TitleScreen", "Loaded %d tilemap entries from %d DMA blocks (may be incorrect)",
|
||||
total_entries, blocks_read);
|
||||
}
|
||||
|
||||
pal_selected_ = 2;
|
||||
|
||||
// Render tilemaps into bitmap pixels
|
||||
RETURN_IF_ERROR(RenderBG1Layer());
|
||||
RETURN_IF_ERROR(RenderBG2Layer());
|
||||
|
||||
// Apply palettes to layer bitmaps AFTER rendering
|
||||
tiles_bg1_bitmap_.SetPalette(palette_);
|
||||
tiles_bg2_bitmap_.SetPalette(palette_);
|
||||
oam_bg_bitmap_.SetPalette(palette_);
|
||||
title_composite_bitmap_.SetPalette(palette_);
|
||||
|
||||
// Ensure bitmaps are marked as active
|
||||
tiles_bg1_bitmap_.set_active(true);
|
||||
tiles_bg2_bitmap_.set_active(true);
|
||||
oam_bg_bitmap_.set_active(true);
|
||||
title_composite_bitmap_.set_active(true);
|
||||
|
||||
// Queue texture creation for all layer bitmaps
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &tiles_bg1_bitmap_);
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &tiles_bg2_bitmap_);
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &oam_bg_bitmap_);
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, &title_composite_bitmap_);
|
||||
|
||||
// Initial composite render (both layers visible)
|
||||
RETURN_IF_ERROR(RenderCompositeLayer(true, true));
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status TitleScreen::RenderBG1Layer() {
|
||||
// BG1 layer is 32x32 tiles (256x256 pixels)
|
||||
auto& bg1_data = tiles_bg1_bitmap_.mutable_data();
|
||||
const auto& tile8_bitmap_data = tiles8_bitmap_.vector();
|
||||
|
||||
// Render each tile in the 32x32 tilemap
|
||||
for (int tile_y = 0; tile_y < 32; tile_y++) {
|
||||
for (int tile_x = 0; tile_x < 32; tile_x++) {
|
||||
int tilemap_index = tile_y * 32 + tile_x;
|
||||
uint16_t tile_word = tiles_bg1_buffer_[tilemap_index];
|
||||
|
||||
// Extract tile info from SNES tile word (vhopppcc cccccccc format)
|
||||
int tile_id = tile_word & 0x3FF; // Bits 0-9: tile ID
|
||||
int palette = (tile_word >> 10) & 0x07; // Bits 10-12: palette
|
||||
bool h_flip = (tile_word & 0x4000) != 0; // Bit 14: horizontal flip
|
||||
bool v_flip = (tile_word & 0x8000) != 0; // Bit 15: vertical flip
|
||||
|
||||
// Debug: Log suspicious tile IDs
|
||||
if (tile_id > 512) {
|
||||
LOG_WARN("TitleScreen", "BG1: Suspicious tile_id=%d at (%d,%d), word=0x%04X",
|
||||
tile_id, tile_x, tile_y, tile_word);
|
||||
}
|
||||
|
||||
// Calculate source position in tiles8_bitmap_
|
||||
// tiles8_bitmap_ is 128 pixels wide, 512 pixels tall (16 sheets × 32 pixels)
|
||||
// Each sheet has 256 tiles (16×16 tiles, 128×32 pixels, 0x1000 bytes)
|
||||
int sheet_index = tile_id / 256; // Which sheet (0-15)
|
||||
int tile_in_sheet = tile_id % 256; // Tile within sheet (0-255)
|
||||
int src_tile_x = (tile_in_sheet % 16) * 8;
|
||||
int src_tile_y = (sheet_index * 32) + ((tile_in_sheet / 16) * 8);
|
||||
|
||||
// Copy 8x8 tile pixels from tile8 bitmap to BG1 bitmap
|
||||
for (int py = 0; py < 8; py++) {
|
||||
for (int px = 0; px < 8; px++) {
|
||||
// Apply flipping
|
||||
int src_px = h_flip ? (7 - px) : px;
|
||||
int src_py = v_flip ? (7 - py) : py;
|
||||
|
||||
// Calculate source and destination positions
|
||||
int src_x = src_tile_x + src_px;
|
||||
int src_y = src_tile_y + src_py;
|
||||
int src_pos = src_y * 128 + src_x; // tiles8_bitmap_ is 128 pixels wide
|
||||
|
||||
int dest_x = tile_x * 8 + px;
|
||||
int dest_y = tile_y * 8 + py;
|
||||
int dest_pos = dest_y * 256 + dest_x; // BG1 is 256 pixels wide
|
||||
|
||||
// Copy pixel with palette application
|
||||
// Graphics are 3BPP in ROM, converted to 8BPP indexed with +0x88 offset
|
||||
if (src_pos < tile8_bitmap_data.size() && dest_pos < bg1_data.size()) {
|
||||
uint8_t pixel_value = tile8_bitmap_data[src_pos];
|
||||
// Pixel values already include palette information from +0x88 offset
|
||||
// Just copy directly (color index 0 = transparent)
|
||||
bg1_data[dest_pos] = pixel_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update surface with rendered pixel data
|
||||
tiles_bg1_bitmap_.UpdateSurfacePixels();
|
||||
|
||||
// Queue texture update
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::UPDATE, &tiles_bg1_bitmap_);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status TitleScreen::RenderBG2Layer() {
|
||||
// BG2 layer is 32x32 tiles (256x256 pixels)
|
||||
auto& bg2_data = tiles_bg2_bitmap_.mutable_data();
|
||||
const auto& tile8_bitmap_data = tiles8_bitmap_.vector();
|
||||
|
||||
// Render each tile in the 32x32 tilemap
|
||||
for (int tile_y = 0; tile_y < 32; tile_y++) {
|
||||
for (int tile_x = 0; tile_x < 32; tile_x++) {
|
||||
int tilemap_index = tile_y * 32 + tile_x;
|
||||
uint16_t tile_word = tiles_bg2_buffer_[tilemap_index];
|
||||
|
||||
// Extract tile info from SNES tile word (vhopppcc cccccccc format)
|
||||
int tile_id = tile_word & 0x3FF; // Bits 0-9: tile ID
|
||||
int palette = (tile_word >> 10) & 0x07; // Bits 10-12: palette
|
||||
bool h_flip = (tile_word & 0x4000) != 0; // Bit 14: horizontal flip
|
||||
bool v_flip = (tile_word & 0x8000) != 0; // Bit 15: vertical flip
|
||||
|
||||
// Calculate source position in tiles8_bitmap_
|
||||
// tiles8_bitmap_ is 128 pixels wide, 512 pixels tall (16 sheets × 32 pixels)
|
||||
// Each sheet has 256 tiles (16×16 tiles, 128×32 pixels, 0x1000 bytes)
|
||||
int sheet_index = tile_id / 256; // Which sheet (0-15)
|
||||
int tile_in_sheet = tile_id % 256; // Tile within sheet (0-255)
|
||||
int src_tile_x = (tile_in_sheet % 16) * 8;
|
||||
int src_tile_y = (sheet_index * 32) + ((tile_in_sheet / 16) * 8);
|
||||
|
||||
// Copy 8x8 tile pixels from tile8 bitmap to BG2 bitmap
|
||||
for (int py = 0; py < 8; py++) {
|
||||
for (int px = 0; px < 8; px++) {
|
||||
// Apply flipping
|
||||
int src_px = h_flip ? (7 - px) : px;
|
||||
int src_py = v_flip ? (7 - py) : py;
|
||||
|
||||
// Calculate source and destination positions
|
||||
int src_x = src_tile_x + src_px;
|
||||
int src_y = src_tile_y + src_py;
|
||||
int src_pos = src_y * 128 + src_x; // tiles8_bitmap_ is 128 pixels wide
|
||||
|
||||
int dest_x = tile_x * 8 + px;
|
||||
int dest_y = tile_y * 8 + py;
|
||||
int dest_pos = dest_y * 256 + dest_x; // BG2 is 256 pixels wide
|
||||
|
||||
// Copy pixel with palette application
|
||||
// Graphics are 3BPP in ROM, converted to 8BPP indexed with +0x88 offset
|
||||
if (src_pos < tile8_bitmap_data.size() && dest_pos < bg2_data.size()) {
|
||||
uint8_t pixel_value = tile8_bitmap_data[src_pos];
|
||||
// Pixel values already include palette information from +0x88 offset
|
||||
// Just copy directly (color index 0 = transparent)
|
||||
bg2_data[dest_pos] = pixel_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update surface with rendered pixel data
|
||||
tiles_bg2_bitmap_.UpdateSurfacePixels();
|
||||
|
||||
// Queue texture update
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::UPDATE, &tiles_bg2_bitmap_);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status TitleScreen::Save(Rom* rom) {
|
||||
if (!rom || !rom->is_loaded()) {
|
||||
return absl::InvalidArgumentError("ROM is not loaded");
|
||||
}
|
||||
|
||||
// Title screen uses compressed tilemap format
|
||||
// We'll write the data back in the same compressed format
|
||||
std::vector<uint8_t> compressed_data;
|
||||
|
||||
// Helper to write word (little endian)
|
||||
auto WriteWord = [&compressed_data](uint16_t value) {
|
||||
compressed_data.push_back(value & 0xFF);
|
||||
compressed_data.push_back((value >> 8) & 0xFF);
|
||||
};
|
||||
|
||||
// Compress BG2 layer (dest < 0x1000)
|
||||
uint16_t bg2_dest = 0x0000;
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
if (i == 0 || tiles_bg2_buffer_[i] != tiles_bg2_buffer_[i - 1]) {
|
||||
// Start a new run
|
||||
WriteWord(bg2_dest + i); // Destination address
|
||||
|
||||
// Count consecutive identical tiles
|
||||
int run_length = 1;
|
||||
uint16_t tile_value = tiles_bg2_buffer_[i];
|
||||
while (i + run_length < 1024 && tiles_bg2_buffer_[i + run_length] == tile_value) {
|
||||
run_length++;
|
||||
}
|
||||
|
||||
// Write length/flags (bit 14 = fixsource if run > 1)
|
||||
uint16_t length_flags = (run_length - 1) * 2; // Length in bytes
|
||||
if (run_length > 1) {
|
||||
length_flags |= 0x4000; // fixsource flag
|
||||
}
|
||||
WriteWord(length_flags);
|
||||
|
||||
// Write tile data
|
||||
WriteWord(tile_value);
|
||||
|
||||
i += run_length - 1; // Skip already processed tiles
|
||||
}
|
||||
}
|
||||
|
||||
// Compress BG1 layer (dest >= 0x1000)
|
||||
uint16_t bg1_dest = 0x1000;
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
if (i == 0 || tiles_bg1_buffer_[i] != tiles_bg1_buffer_[i - 1]) {
|
||||
// Start a new run
|
||||
WriteWord(bg1_dest + i); // Destination address
|
||||
|
||||
// Count consecutive identical tiles
|
||||
int run_length = 1;
|
||||
uint16_t tile_value = tiles_bg1_buffer_[i];
|
||||
while (i + run_length < 1024 && tiles_bg1_buffer_[i + run_length] == tile_value) {
|
||||
run_length++;
|
||||
}
|
||||
|
||||
// Write length/flags (bit 14 = fixsource if run > 1)
|
||||
uint16_t length_flags = (run_length - 1) * 2; // Length in bytes
|
||||
if (run_length > 1) {
|
||||
length_flags |= 0x4000; // fixsource flag
|
||||
}
|
||||
WriteWord(length_flags);
|
||||
|
||||
// Write tile data
|
||||
WriteWord(tile_value);
|
||||
|
||||
i += run_length - 1; // Skip already processed tiles
|
||||
}
|
||||
}
|
||||
|
||||
// Write terminator byte
|
||||
compressed_data.push_back(0x80);
|
||||
|
||||
// Calculate ROM address to write to
|
||||
ASSIGN_OR_RETURN(uint8_t byte0, rom->ReadByte(0x137A + 3));
|
||||
ASSIGN_OR_RETURN(uint8_t byte1, rom->ReadByte(0x1383 + 3));
|
||||
ASSIGN_OR_RETURN(uint8_t byte2, rom->ReadByte(0x138C + 3));
|
||||
|
||||
int pos = (byte2 << 16) + (byte1 << 8) + byte0;
|
||||
int write_pos = SnesToPc(pos);
|
||||
|
||||
// Write compressed data to ROM
|
||||
for (size_t i = 0; i < compressed_data.size(); i++) {
|
||||
RETURN_IF_ERROR(rom->WriteByte(write_pos + i, compressed_data[i]));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status TitleScreen::RenderCompositeLayer(bool show_bg1, bool show_bg2) {
|
||||
auto& composite_data = title_composite_bitmap_.mutable_data();
|
||||
const auto& bg1_data = tiles_bg1_bitmap_.vector();
|
||||
const auto& bg2_data = tiles_bg2_bitmap_.vector();
|
||||
|
||||
// Clear to transparent (color index 0)
|
||||
std::fill(composite_data.begin(), composite_data.end(), 0);
|
||||
|
||||
// Layer BG2 first (if visible) - background layer
|
||||
if (show_bg2) {
|
||||
for (int i = 0; i < 256 * 256; i++) {
|
||||
composite_data[i] = bg2_data[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Layer BG1 on top (if visible), respecting transparency
|
||||
if (show_bg1) {
|
||||
for (int i = 0; i < 256 * 256; i++) {
|
||||
uint8_t pixel = bg1_data[i];
|
||||
// Check if color 0 in the sub-palette (transparent)
|
||||
// Pixel format is (palette<<3) | color, so color is bits 0-2
|
||||
if ((pixel & 0x07) != 0) {
|
||||
composite_data[i] = pixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy pixel data to SDL surface
|
||||
title_composite_bitmap_.UpdateSurfacePixels();
|
||||
|
||||
// Queue texture update
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::UPDATE, &title_composite_bitmap_);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
107
src/zelda3/screen/title_screen.h
Normal file
107
src/zelda3/screen/title_screen.h
Normal file
@@ -0,0 +1,107 @@
|
||||
#ifndef YAZE_APP_ZELDA3_SCREEN_H
|
||||
#define YAZE_APP_ZELDA3_SCREEN_H
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/render/tilemap.h"
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
#include "app/gfx/types/snes_tile.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
/**
|
||||
* @brief TitleScreen manages the title screen graphics and tilemap data.
|
||||
*
|
||||
* The title screen consists of three layers:
|
||||
* - BG1: Main logo and graphics
|
||||
* - BG2: Background elements
|
||||
* - OAM: Sprite layer (sword, etc.)
|
||||
*
|
||||
* Each layer is stored as a 32x32 tilemap (0x400 tiles = 0x1000 bytes as words)
|
||||
*/
|
||||
class TitleScreen {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialize and load title screen data from ROM
|
||||
* @param rom ROM instance to read data from
|
||||
*/
|
||||
absl::Status Create(Rom* rom);
|
||||
|
||||
// Accessors for layer data
|
||||
auto& bg1_buffer() { return tiles_bg1_buffer_; }
|
||||
auto& bg2_buffer() { return tiles_bg2_buffer_; }
|
||||
auto& oam_buffer() { return oam_data_; }
|
||||
|
||||
// Mutable accessors for editing
|
||||
auto& mutable_bg1_buffer() { return tiles_bg1_buffer_; }
|
||||
auto& mutable_bg2_buffer() { return tiles_bg2_buffer_; }
|
||||
|
||||
// Accessors for bitmaps
|
||||
auto& bg1_bitmap() { return tiles_bg1_bitmap_; }
|
||||
auto& bg2_bitmap() { return tiles_bg2_bitmap_; }
|
||||
auto& oam_bitmap() { return oam_bg_bitmap_; }
|
||||
auto& tiles8_bitmap() { return tiles8_bitmap_; }
|
||||
auto& composite_bitmap() { return title_composite_bitmap_; }
|
||||
auto& blockset() { return tile16_blockset_; }
|
||||
|
||||
// Palette access
|
||||
auto& palette() { return palette_; }
|
||||
|
||||
// Save changes back to ROM
|
||||
absl::Status Save(Rom* rom);
|
||||
|
||||
/**
|
||||
* @brief Render BG1 tilemap into bitmap pixels
|
||||
* Converts tile IDs from tiles_bg1_buffer_ into pixel data
|
||||
*/
|
||||
absl::Status RenderBG1Layer();
|
||||
|
||||
/**
|
||||
* @brief Render BG2 tilemap into bitmap pixels
|
||||
* Converts tile IDs from tiles_bg2_buffer_ into pixel data
|
||||
*/
|
||||
absl::Status RenderBG2Layer();
|
||||
|
||||
/**
|
||||
* @brief Render composite layer with BG1 on top of BG2 with transparency
|
||||
* @param show_bg1 Whether to include BG1 layer
|
||||
* @param show_bg2 Whether to include BG2 layer
|
||||
*/
|
||||
absl::Status RenderCompositeLayer(bool show_bg1, bool show_bg2);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Build the tile16 blockset from ROM graphics
|
||||
* @param rom ROM instance to read graphics from
|
||||
*/
|
||||
absl::Status BuildTileset(Rom* rom);
|
||||
|
||||
/**
|
||||
* @brief Load title screen tilemap data from ROM
|
||||
* @param rom ROM instance to read tilemap from
|
||||
*/
|
||||
absl::Status LoadTitleScreen(Rom* rom);
|
||||
|
||||
int pal_selected_ = 2;
|
||||
|
||||
std::array<uint16_t, 0x1000> tiles_bg1_buffer_; // BG1 tilemap (32x32 tiles)
|
||||
std::array<uint16_t, 0x1000> tiles_bg2_buffer_; // BG2 tilemap (32x32 tiles)
|
||||
|
||||
gfx::OamTile oam_data_[10];
|
||||
|
||||
gfx::Bitmap tiles_bg1_bitmap_; // Rendered BG1 layer
|
||||
gfx::Bitmap tiles_bg2_bitmap_; // Rendered BG2 layer
|
||||
gfx::Bitmap oam_bg_bitmap_; // Rendered OAM layer
|
||||
gfx::Bitmap tiles8_bitmap_; // 8x8 tile graphics
|
||||
gfx::Bitmap title_composite_bitmap_; // Composite BG1+BG2 with transparency
|
||||
|
||||
gfx::Tilemap tile16_blockset_; // 16x16 tile blockset
|
||||
gfx::SnesPalette palette_; // Title screen palette
|
||||
};
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_ZELDA3_SCREEN_H
|
||||
Reference in New Issue
Block a user