backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

525
src/zelda3/game_data.cc Normal file
View File

@@ -0,0 +1,525 @@
#include "zelda3/game_data.h"
#include "absl/strings/str_format.h"
#include "app/gfx/util/compression.h"
#include "util/log.h"
#include "util/macro.h"
#include "zelda3/dungeon/dungeon_rom_addresses.h"
#ifdef __EMSCRIPTEN__
#include "app/platform/wasm/wasm_loading_manager.h"
#endif
namespace yaze {
namespace zelda3 {
namespace {
constexpr uint32_t kUncompressedSheetSize = 0x0800;
// constexpr uint32_t kTile16Ptr = 0x78000;
// Helper to get address from bytes
uint32_t AddressFromBytes(uint8_t bank, uint8_t high, uint8_t low) {
return (bank << 16) | (high << 8) | low;
}
// Helper to convert SNES to PC address
uint32_t SnesToPc(uint32_t snes_addr) {
return (snes_addr & 0x7FFF) | ((snes_addr & 0x7F0000) >> 1);
}
// Helper to convert PC to SNES address
// uint32_t PcToSnes(uint32_t pc_addr) {
// return ((pc_addr & 0x7FFF) | 0x8000) | ((pc_addr & 0x3F8000) << 1);
// }
// ============================================================================
// Graphics Address Resolution
// ============================================================================
/// Resolves a graphics sheet index to its PC (file) offset in the ROM.
///
/// ALTTP stores graphics sheet addresses using three separate pointer tables,
/// where each table contains one byte of the 24-bit SNES address:
/// - ptr1 table: Bank byte of address (bits 16-23)
/// - ptr2 table: High byte of address (bits 8-15)
/// - ptr3 table: Low byte of address (bits 0-7)
///
/// For US ROMs, these tables are located at:
/// - kOverworldGfxPtr1 = 0x4F80 (bank bytes)
/// - kOverworldGfxPtr2 = 0x505F (high bytes)
/// - kOverworldGfxPtr3 = 0x513E (low bytes)
///
/// Example for sheet index 0:
/// SNES addr = (data[0x4F80] << 16) | (data[0x505F] << 8) | data[0x513E]
/// PC offset = SnesToPc(SNES addr)
///
/// @param data Pointer to ROM data buffer
/// @param addr Graphics sheet index (0-222)
/// @param ptr1 Offset of bank-byte pointer table in ROM
/// @param ptr2 Offset of high-byte pointer table in ROM
/// @param ptr3 Offset of low-byte pointer table in ROM
/// @param rom_size ROM size for bounds checking
/// @return PC offset where graphics data begins, or rom_size if OOB
///
} // namespace
/// @warning Callers must verify the returned offset is within ROM bounds
/// before attempting to read or decompress data at that location.
uint32_t GetGraphicsAddress(const uint8_t* data, uint8_t addr, uint32_t ptr1,
uint32_t ptr2, uint32_t ptr3, size_t rom_size) {
if (ptr1 > UINT32_MAX - addr || ptr1 + addr >= rom_size ||
ptr2 > UINT32_MAX - addr || ptr2 + addr >= rom_size ||
ptr3 > UINT32_MAX - addr || ptr3 + addr >= rom_size) {
return static_cast<uint32_t>(rom_size);
}
return SnesToPc(AddressFromBytes(data[ptr1 + addr], data[ptr2 + addr],
data[ptr3 + addr]));
}
absl::Status LoadGameData(Rom& rom, GameData& data, const LoadOptions& options) {
data.Clear();
if (options.populate_metadata) {
RETURN_IF_ERROR(LoadMetadata(rom, data));
}
if (options.load_palettes) {
RETURN_IF_ERROR(LoadPalettes(rom, data));
}
if (options.load_gfx_groups) {
RETURN_IF_ERROR(LoadGfxGroups(rom, data));
}
if (options.load_graphics) {
RETURN_IF_ERROR(LoadGraphics(rom, data));
}
if (options.expand_rom) {
if (rom.size() < 1048576 * 2) {
rom.Expand(1048576 * 2);
}
}
return absl::OkStatus();
}
absl::Status SaveGameData(Rom& rom, GameData& data) {
if (core::FeatureFlags::get().kSaveAllPalettes) {
// TODO: Implement SaveAllPalettes logic using Rom::WriteColor
// This was previously in Rom::SaveAllPalettes
return data.palette_groups.for_each([&](gfx::PaletteGroup& group) -> absl::Status {
for (size_t i = 0; i < group.size(); ++i) {
auto* palette = group.mutable_palette(i);
for (size_t j = 0; j < palette->size(); ++j) {
gfx::SnesColor color = (*palette)[j];
if (color.is_modified()) {
RETURN_IF_ERROR(rom.WriteColor(
gfx::GetPaletteAddress(group.name(), i, j), color));
color.set_modified(false);
}
}
}
return absl::OkStatus();
});
}
if (core::FeatureFlags::get().kSaveGfxGroups) {
RETURN_IF_ERROR(SaveGfxGroups(rom, data));
}
// TODO: Implement SaveAllGraphicsData logic
return absl::OkStatus();
}
absl::Status LoadMetadata(const Rom& rom, GameData& data) {
constexpr uint32_t kTitleStringOffset = 0x7FC0;
constexpr uint32_t kTitleStringLength = 20;
if (rom.size() < kTitleStringOffset + kTitleStringLength) {
return absl::OutOfRangeError("ROM too small for metadata");
}
// Check version byte at offset + 0x19 (0x7FD9)
if (kTitleStringOffset + 0x19 < rom.size()) {
// Access directly via data() since ReadByte is non-const
uint8_t version_byte = rom.data()[kTitleStringOffset + 0x19];
data.version = (version_byte == 0) ? zelda3_version::JP : zelda3_version::US;
}
auto title_bytes = rom.ReadByteVector(kTitleStringOffset, kTitleStringLength);
if (title_bytes.ok()) {
data.title.assign(title_bytes->begin(), title_bytes->end());
}
return absl::OkStatus();
}
absl::Status LoadPalettes(const Rom& rom, GameData& data) {
// Create a vector from rom data for palette loading
const std::vector<uint8_t>& rom_vec = rom.vector();
return gfx::LoadAllPalettes(rom_vec, data.palette_groups);
}
absl::Status LoadGfxGroups(Rom& rom, GameData& data) {
if (kVersionConstantsMap.find(data.version) == kVersionConstantsMap.end()) {
return absl::FailedPreconditionError("Unsupported ROM version");
}
auto version_constants = kVersionConstantsMap.at(data.version);
// Load Main Blocksets
auto main_ptr_res = rom.ReadWord(kGfxGroupsPointer);
if (main_ptr_res.ok()) {
uint32_t main_ptr = SnesToPc(*main_ptr_res);
for (uint32_t i = 0; i < kNumMainBlocksets; i++) {
for (int j = 0; j < 8; j++) {
auto val = rom.ReadByte(main_ptr + (i * 8) + j);
if (val.ok()) data.main_blockset_ids[i][j] = *val;
}
}
}
// Load Room Blocksets
for (uint32_t i = 0; i < kNumRoomBlocksets; i++) {
for (int j = 0; j < 4; j++) {
auto val = rom.ReadByte(kEntranceGfxGroup + (i * 4) + j);
if (val.ok()) data.room_blockset_ids[i][j] = *val;
}
}
// Load Sprite Blocksets
for (uint32_t i = 0; i < kNumSpritesets; i++) {
for (int j = 0; j < 4; j++) {
auto val = rom.ReadByte(version_constants.kSpriteBlocksetPointer + (i * 4) + j);
if (val.ok()) data.spriteset_ids[i][j] = *val;
}
}
// Load Palette Sets
for (uint32_t i = 0; i < kNumPalettesets; i++) {
for (int j = 0; j < 4; j++) {
auto val = rom.ReadByte(version_constants.kDungeonPalettesGroups + (i * 4) + j);
if (val.ok()) data.paletteset_ids[i][j] = *val;
}
}
return absl::OkStatus();
}
absl::Status SaveGfxGroups(Rom& rom, const GameData& data) {
auto version_constants = kVersionConstantsMap.at(data.version);
ASSIGN_OR_RETURN(auto main_ptr, rom.ReadWord(kGfxGroupsPointer));
main_ptr = SnesToPc(main_ptr);
// Save Main Blocksets
for (uint32_t i = 0; i < kNumMainBlocksets; i++) {
for (int j = 0; j < 8; j++) {
RETURN_IF_ERROR(rom.WriteByte(main_ptr + (i * 8) + j,
data.main_blockset_ids[i][j]));
}
}
// Save Room Blocksets
for (uint32_t i = 0; i < kNumRoomBlocksets; i++) {
for (int j = 0; j < 4; j++) {
RETURN_IF_ERROR(rom.WriteByte(kEntranceGfxGroup + (i * 4) + j,
data.room_blockset_ids[i][j]));
}
}
// Save Sprite Blocksets
for (uint32_t i = 0; i < kNumSpritesets; i++) {
for (int j = 0; j < 4; j++) {
RETURN_IF_ERROR(rom.WriteByte(version_constants.kSpriteBlocksetPointer + (i * 4) + j,
data.spriteset_ids[i][j]));
}
}
// Save Palette Sets
for (uint32_t i = 0; i < kNumPalettesets; i++) {
for (int j = 0; j < 4; j++) {
RETURN_IF_ERROR(rom.WriteByte(version_constants.kDungeonPalettesGroups + (i * 4) + j,
data.paletteset_ids[i][j]));
}
}
return absl::OkStatus();
}
// ============================================================================
// Main Graphics Loading
// ============================================================================
/// Loads all 223 graphics sheets from the ROM into bitmap format.
///
/// ALTTP uses 223 graphics sheets (kNumGfxSheets) stored in three formats:
///
/// Sheet Categories:
/// - Sheets 0-112: Compressed 3BPP (overworld, dungeons, sprites)
/// - Sheets 113-114: 2BPP sheets (skipped here, loaded separately)
/// - Sheets 115-126: Uncompressed 3BPP (special graphics)
/// - Sheets 127-217: Compressed 3BPP (additional graphics)
/// - Sheets 218+: 2BPP sheets (skipped here)
///
/// Compression Format:
/// Graphics data is compressed using Nintendo's LC-LZ2 algorithm. Each sheet
/// is decompressed to a 0x800 (2048) byte buffer, then converted from SNES
/// planar format to linear 8BPP for easier manipulation.
///
/// Graphics Buffer:
/// All sheet data is also appended to data.graphics_buffer for legacy
/// compatibility. Sheets that fail to load are filled with 0xFF bytes to
/// maintain correct indexing.
///
/// @param rom The loaded ROM to read graphics from
/// @param data The GameData structure to populate with graphics
/// @return OkStatus on success, or error status
///
/// @warning The DecompressV2 size parameter MUST be 0x800, not 0.
/// Passing size=0 causes immediate return of empty data, which
/// was a regression bug that caused all graphics to appear as
/// solid purple/brown (0xFF fill).
absl::Status LoadGraphics(Rom& rom, GameData& data) {
if (kVersionConstantsMap.find(data.version) == kVersionConstantsMap.end()) {
return absl::FailedPreconditionError("Unsupported ROM version for graphics");
}
auto version_constants = kVersionConstantsMap.at(data.version);
data.graphics_buffer.clear();
#ifdef __EMSCRIPTEN__
auto loading_handle = app::platform::WasmLoadingManager::BeginLoading("Loading Graphics");
#endif
// Initialize Diagnostics
auto& diag = data.diagnostics;
diag.rom_size = rom.size();
diag.ptr1_loc = version_constants.kOverworldGfxPtr1;
diag.ptr2_loc = version_constants.kOverworldGfxPtr2;
diag.ptr3_loc = version_constants.kOverworldGfxPtr3;
for (uint32_t i = 0; i < kNumGfxSheets; i++) {
#ifdef __EMSCRIPTEN__
app::platform::WasmLoadingManager::UpdateProgress(loading_handle, static_cast<float>(i) / kNumGfxSheets);
#endif
diag.sheets[i].index = i;
std::vector<uint8_t> sheet;
bool bpp3 = false;
uint32_t offset = 0;
// Uncompressed 3BPP (115-126)
if (i >= 115 && i <= 126) {
diag.sheets[i].is_compressed = false;
offset = GetGraphicsAddress(rom.data(), i, version_constants.kOverworldGfxPtr1, version_constants.kOverworldGfxPtr2, version_constants.kOverworldGfxPtr3, rom.size());
diag.sheets[i].pc_offset = offset;
auto read_res = rom.ReadByteVector(offset, zelda3::kUncompressedSheetSize);
if (read_res.ok()) {
sheet = *read_res;
diag.sheets[i].decompression_succeeded = true;
bpp3 = true;
} else {
sheet.assign(zelda3::kUncompressedSheetSize, 0);
diag.sheets[i].decompression_succeeded = false;
}
}
// 2BPP (113-114, 218+) - Skipped in main loop
else if (i == 113 || i == 114 || i >= 218) {
diag.sheets[i].is_compressed = true;
bpp3 = false;
}
// Compressed 3BPP (Standard)
else {
diag.sheets[i].is_compressed = true;
offset = GetGraphicsAddress(rom.data(), i, version_constants.kOverworldGfxPtr1, version_constants.kOverworldGfxPtr2, version_constants.kOverworldGfxPtr3, rom.size());
diag.sheets[i].pc_offset = offset;
if (offset < rom.size()) {
// Decompress using LC-LZ2 algorithm with 0x800 byte output buffer.
// IMPORTANT: The size parameter (0x800) must NOT be 0, or DecompressV2
// returns an empty vector immediately. This was a regression bug.
// See: docs/internal/graphics-loading-regression-2024.md
auto decomp_res = gfx::lc_lz2::DecompressV2(rom.data(), offset, 0x800, 1, rom.size());
if (decomp_res.ok()) {
sheet = *decomp_res;
diag.sheets[i].decompression_succeeded = true;
bpp3 = true;
} else {
diag.sheets[i].decompression_succeeded = false;
}
}
}
// Post-process
if (bpp3) {
auto converted_sheet = gfx::SnesTo8bppSheet(sheet, 3);
if (converted_sheet.size() != 4096) converted_sheet.resize(4096, 0);
data.raw_gfx_sheets[i] = converted_sheet;
data.gfx_bitmaps[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight,
gfx::kTilesheetDepth, converted_sheet);
// Apply default palettes
if (!data.palette_groups.empty()) {
gfx::SnesPalette default_palette;
if (i < 113 && data.palette_groups.dungeon_main.size() > 0) {
default_palette = data.palette_groups.dungeon_main[0];
} else if (i < 128 && data.palette_groups.sprites_aux1.size() > 0) {
default_palette = data.palette_groups.sprites_aux1[0];
} else if (data.palette_groups.hud.size() > 0) {
default_palette = data.palette_groups.hud.palette(0);
}
if (!default_palette.empty()) {
data.gfx_bitmaps[i].SetPalette(default_palette);
} else {
// Fallback to grayscale if no palette found
std::vector<gfx::SnesColor> grayscale;
for (int color_idx = 0; color_idx < 16; ++color_idx) {
float val = color_idx / 15.0f;
grayscale.emplace_back(ImVec4(val, val, val, 1.0f));
}
// Ensure index 0 is transparent for SNES compatibility
if (!grayscale.empty()) {
grayscale[0].set_transparent(true);
}
data.gfx_bitmaps[i].SetPalette(gfx::SnesPalette(grayscale));
}
} else {
// Fallback to grayscale if no palette groups loaded
std::vector<gfx::SnesColor> grayscale;
for (int color_idx = 0; color_idx < 16; ++color_idx) {
float val = color_idx / 15.0f;
grayscale.emplace_back(ImVec4(val, val, val, 1.0f));
}
// Ensure index 0 is transparent for SNES compatibility
if (!grayscale.empty()) {
grayscale[0].set_transparent(true);
}
data.gfx_bitmaps[i].SetPalette(gfx::SnesPalette(grayscale));
}
data.graphics_buffer.insert(data.graphics_buffer.end(),
data.gfx_bitmaps[i].data(),
data.gfx_bitmaps[i].data() + data.gfx_bitmaps[i].size());
} else {
// Placeholder - Fill with 0 (transparent) instead of 0xFF (white)
std::vector<uint8_t> placeholder(4096, 0);
data.raw_gfx_sheets[i] = placeholder;
data.gfx_bitmaps[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight,
gfx::kTilesheetDepth, placeholder);
data.graphics_buffer.resize(data.graphics_buffer.size() + 4096, 0);
}
}
diag.Analyze();
#ifdef __EMSCRIPTEN__
app::platform::WasmLoadingManager::EndLoading(loading_handle);
#endif
return absl::OkStatus();
}
// ============================================================================
// Link Graphics Loading
// ============================================================================
absl::StatusOr<std::array<gfx::Bitmap, kNumLinkSheets>> LoadLinkGraphics(
const Rom& rom) {
std::array<gfx::Bitmap, kNumLinkSheets> link_graphics;
for (uint32_t i = 0; i < kNumLinkSheets; i++) {
auto link_sheet_data_result =
rom.ReadByteVector(/*offset=*/kLinkGfxOffset + (i * kLinkGfxLength),
/*length=*/kLinkGfxLength);
if (!link_sheet_data_result.ok()) {
return link_sheet_data_result.status();
}
auto link_sheet_8bpp = gfx::SnesTo8bppSheet(*link_sheet_data_result, /*bpp=*/4);
if (link_sheet_8bpp.size() != 4096) link_sheet_8bpp.resize(4096, 0);
link_graphics[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight,
gfx::kTilesheetDepth, link_sheet_8bpp);
// Palette is applied by the caller since GameData may not be available here
}
return link_graphics;
}
// ============================================================================
// 2BPP Graphics Loading
// ============================================================================
absl::StatusOr<std::vector<uint8_t>> Load2BppGraphics(const Rom& rom) {
std::vector<uint8_t> sheet;
const uint8_t sheets[] = {0x71, 0x72, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE};
// Get version constants - default to US if we don't know
auto version_constants = kVersionConstantsMap.at(zelda3_version::US);
for (const auto& sheet_id : sheets) {
auto offset = GetGraphicsAddress(rom.data(), sheet_id,
version_constants.kOverworldGfxPtr1,
version_constants.kOverworldGfxPtr2,
version_constants.kOverworldGfxPtr3,
rom.size());
if (offset >= rom.size()) {
return absl::OutOfRangeError(
absl::StrFormat("2BPP graphics sheet %u offset %u exceeds ROM size %zu",
sheet_id, offset, rom.size()));
}
// Decompress using LC-LZ2 algorithm with 0x800 byte output buffer.
auto decomp_result = gfx::lc_lz2::DecompressV2(rom.data(), offset, 0x800, 1, rom.size());
if (!decomp_result.ok()) {
return decomp_result.status();
}
auto converted_sheet = gfx::SnesTo8bppSheet(*decomp_result, 2);
for (const auto& each_pixel : converted_sheet) {
sheet.push_back(each_pixel);
}
}
return sheet;
}
// ============================================================================
// Font Graphics Loading
// ============================================================================
absl::StatusOr<gfx::Bitmap> LoadFontGraphics(const Rom& rom) {
// Font sprites are located at 0x70000, 2BPP format
constexpr uint32_t kFontDataSize = 0x4000; // 16KB of font data
auto font_data_result = rom.ReadByteVector(kFontSpriteLocation, kFontDataSize);
if (!font_data_result.ok()) {
return font_data_result.status();
}
// Convert from 2BPP SNES format to 8BPP
auto font_8bpp = gfx::SnesTo8bppSheet(*font_data_result, /*bpp=*/2);
gfx::Bitmap font_bitmap;
font_bitmap.Create(gfx::kTilesheetWidth * 2, gfx::kTilesheetHeight * 4,
gfx::kTilesheetDepth, font_8bpp);
return font_bitmap;
}
// ============================================================================
// Graphics Saving
// ============================================================================
absl::Status SaveAllGraphicsData(
[[maybe_unused]] Rom& rom, [[maybe_unused]] const std::array<gfx::Bitmap, kNumGfxSheets>& sheets) {
// For now, return OK status - full implementation would write sheets back
// to ROM at their respective addresses with proper compression
LOG_INFO("SaveAllGraphicsData", "Graphics save not yet fully implemented");
return absl::OkStatus();
}
} // namespace zelda3
} // namespace yaze