Enhance Dungeon Editor with Object Rendering and Layout Management

- Introduced new object rendering features in DungeonEditor, allowing for improved visualization of dungeon objects with options to show outlines, render objects, and display object information.
- Implemented a caching mechanism for rendered objects to optimize performance.
- Added functionality to load and manage room layouts, including walls, floors, and other structural elements, enhancing the overall editing experience.
- Refactored object handling in Room and RoomObject classes to support new rendering logic and ensure compatibility with the updated layout system.
- Introduced ObjectParser for efficient parsing of object data directly from ROM, improving reliability and performance in object rendering.
This commit is contained in:
scawful
2025-09-23 22:00:54 -04:00
parent 8da8014170
commit edaf6427c8
12 changed files with 1537 additions and 176 deletions

View File

@@ -1,6 +1,7 @@
#include "dungeon_editor.h"
#include "absl/container/flat_hash_map.h"
#include "absl/strings/str_format.h"
#include "app/core/window.h"
#include "app/gfx/arena.h"
#include "app/gfx/snes_palette.h"
@@ -464,20 +465,169 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
}
static bool show_objects = false;
ImGui::Checkbox("Show Objects", &show_objects);
ImGui::Checkbox("Show Object Outlines", &show_objects);
static bool render_objects = true;
ImGui::Checkbox("Render Objects", &render_objects);
static bool show_object_info = false;
ImGui::Checkbox("Show Object Info", &show_object_info);
static bool show_layout_objects = false;
ImGui::Checkbox("Show Layout Objects", &show_layout_objects);
static bool show_debug_info = false;
ImGui::Checkbox("Show Debug Info", &show_debug_info);
if (ImGui::Button("Clear Object Cache")) {
object_render_cache_.clear();
}
ImGui::SameLine();
ImGui::Text("Cache: %zu objects", object_render_cache_.size());
// Object statistics and metadata
if (show_debug_info) {
ImGui::Separator();
ImGui::Text("Room Statistics:");
ImGui::Text("Objects: %zu", rooms_[room_id].tile_objects_.size());
ImGui::Text("Layout Objects: %zu",
rooms_[room_id].GetLayout().GetObjects().size());
ImGui::Text("Sprites: %zu", rooms_[room_id].sprites_.size());
ImGui::Text("Chests: %zu", rooms_[room_id].chests_in_room_.size());
// Palette information
ImGui::Text("Current Palette Group: %d", current_palette_group_id_);
ImGui::Text("Palette Hash: %#016llx", last_palette_hash_);
// Object type breakdown
ImGui::Separator();
ImGui::Text("Object Type Breakdown:");
std::map<int, int> object_type_counts;
for (const auto &obj : rooms_[room_id].tile_objects_) {
object_type_counts[obj.id_]++;
}
for (const auto &[type, count] : object_type_counts) {
ImGui::Text("Type 0x%02X: %d objects", type, count);
}
// Layout object type breakdown
ImGui::Separator();
ImGui::Text("Layout Object Types:");
auto walls = rooms_[room_id].GetLayout().GetObjectsByType(
zelda3::RoomLayoutObject::Type::kWall);
auto floors = rooms_[room_id].GetLayout().GetObjectsByType(
zelda3::RoomLayoutObject::Type::kFloor);
auto doors = rooms_[room_id].GetLayout().GetObjectsByType(
zelda3::RoomLayoutObject::Type::kDoor);
ImGui::Text("Walls: %zu", walls.size());
ImGui::Text("Floors: %zu", floors.size());
ImGui::Text("Doors: %zu", doors.size());
}
// Object selection and editing
static int selected_object_id = -1;
if (ImGui::Button("Select Object")) {
// This would open an object selection dialog
// For now, just cycle through objects
if (!rooms_[room_id].tile_objects_.empty()) {
selected_object_id =
(selected_object_id + 1) % rooms_[room_id].tile_objects_.size();
}
}
if (selected_object_id >= 0 &&
selected_object_id < (int)rooms_[room_id].tile_objects_.size()) {
const auto &selected_obj =
rooms_[room_id].tile_objects_[selected_object_id];
ImGui::Separator();
ImGui::Text("Selected Object:");
ImGui::Text("ID: 0x%02X", selected_obj.id_);
ImGui::Text("Position: (%d, %d)", selected_obj.x_, selected_obj.y_);
ImGui::Text("Size: 0x%02X", selected_obj.size_);
ImGui::Text("Layer: %d", static_cast<int>(selected_obj.layer_));
ImGui::Text("Tile Count: %d", selected_obj.GetTileCount());
// Object editing controls
if (ImGui::Button("Edit Object")) {
// This would open an object editing dialog
}
ImGui::SameLine();
if (ImGui::Button("Delete Object")) {
// This would remove the object from the room
}
}
ImGui::EndGroup();
canvas_.DrawBackground();
canvas_.DrawContextMenu();
if (is_loaded_) {
canvas_.DrawBitmap(gfx::Arena::Get().bg1().bitmap(), 0, true);
canvas_.DrawBitmap(gfx::Arena::Get().bg2().bitmap(), 0, true);
canvas_.DrawBitmap(gfx::Arena::Get().bg1().bitmap(), 1.0f, 1.0f);
canvas_.DrawBitmap(gfx::Arena::Get().bg2().bitmap(), 1.0f, 1.0f);
if (show_objects) {
if (show_objects || render_objects) {
// Get the current room's palette for object rendering
auto current_palette =
rom()->palette_group().dungeon_main[current_palette_group_id_];
// Clear cache if palette changed
uint64_t current_palette_hash = 0;
for (size_t i = 0; i < current_palette.size() && i < 16; ++i) {
current_palette_hash ^=
std::hash<uint16_t>{}(current_palette[i].snes()) + 0x9e3779b9 +
(current_palette_hash << 6) + (current_palette_hash >> 2);
}
if (current_palette_hash != last_palette_hash_) {
object_render_cache_.clear();
last_palette_hash_ = current_palette_hash;
}
// Render layout objects (walls, floors, etc.) first
if (show_layout_objects) {
RenderLayoutObjects(rooms_[room_id].GetLayout(), current_palette);
}
if (rooms_[room_id].tile_objects_.empty()) {
// Load the objects for the room
rooms_[room_id].LoadObjects();
}
// Render regular room objects
for (const auto &object : rooms_[room_id].tile_objects_) {
canvas_.DrawOutline(object.x_, object.y_, object.width_ * 16,
object.height_ * 16);
// Convert room coordinates to canvas coordinates
int canvas_x = object.x_ * 16;
int canvas_y = object.y_ * 16;
if (show_objects) {
// Draw object outline - use size_ to determine dimensions
int outline_width = 16; // Default 16x16
int outline_height = 16;
// Calculate dimensions based on object size
if (object.size_ > 0) {
// Size encoding: bits 0-1 = width, bits 2-3 = height
int width_bits = object.size_ & 0x03;
int height_bits = (object.size_ >> 2) & 0x03;
outline_width = (width_bits + 1) * 16;
outline_height = (height_bits + 1) * 16;
}
canvas_.DrawOutline(object.x_, object.y_, outline_width,
outline_height);
}
if (render_objects) {
// Render the actual object using ObjectRenderer
RenderObjectInCanvas(object, current_palette);
}
if (show_object_info) {
// Display object information
DisplayObjectInfo(object, canvas_x, canvas_y);
}
}
}
}
@@ -485,6 +635,134 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
canvas_.DrawOverlay();
}
void DungeonEditor::RenderObjectInCanvas(const zelda3::RoomObject &object,
const gfx::SnesPalette &palette) {
// Create a mutable copy of the object to ensure tiles are loaded
auto mutable_object = object;
mutable_object.set_rom(rom_);
mutable_object.EnsureTilesLoaded();
// Check if tiles were loaded successfully using the new method
auto tiles_result = mutable_object.GetTiles();
if (!tiles_result.ok() || tiles_result->empty()) {
return; // Skip objects without tiles
}
// Calculate palette hash for caching
uint64_t palette_hash = 0;
for (size_t i = 0; i < palette.size() && i < 16; ++i) {
palette_hash ^= std::hash<uint16_t>{}(palette[i].snes()) + 0x9e3779b9 +
(palette_hash << 6) + (palette_hash >> 2);
}
// Check cache first
for (auto &cached : object_render_cache_) {
if (cached.object_id == object.id_ && cached.object_x == object.x_ &&
cached.object_y == object.y_ && cached.object_size == object.size_ &&
cached.palette_hash == palette_hash && cached.is_valid) {
// Use cached bitmap
int canvas_x = object.x_ * 16;
int canvas_y = object.y_ * 16;
canvas_.DrawBitmap(cached.rendered_bitmap, canvas_x, canvas_y, 1.0f, 255);
return;
}
}
// Render the object to a bitmap
auto render_result = object_renderer_.RenderObject(mutable_object, palette);
if (!render_result.ok()) {
return; // Skip if rendering failed
}
auto object_bitmap = std::move(render_result.value());
// Set the palette for the bitmap
object_bitmap.SetPalette(palette);
// Render the bitmap to a texture so it can be drawn
core::Renderer::Get().RenderBitmap(&object_bitmap);
// Convert room coordinates to canvas coordinates
// Room coordinates are in 16x16 tile units, canvas coordinates are in pixels
int canvas_x = object.x_ * 16;
int canvas_y = object.y_ * 16;
// Draw the object bitmap to the canvas immediately
canvas_.DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255);
// Cache the rendered bitmap (create a copy for caching)
ObjectRenderCache cache_entry;
cache_entry.object_id = object.id_;
cache_entry.object_x = object.x_;
cache_entry.object_y = object.y_;
cache_entry.object_size = object.size_;
cache_entry.palette_hash = palette_hash;
cache_entry.rendered_bitmap = object_bitmap; // Copy instead of move
cache_entry.is_valid = true;
// Add to cache (limit cache size)
if (object_render_cache_.size() >= 100) {
object_render_cache_.erase(object_render_cache_.begin());
}
object_render_cache_.push_back(std::move(cache_entry));
}
void DungeonEditor::DisplayObjectInfo(const zelda3::RoomObject &object,
int canvas_x, int canvas_y) {
// Display object information as text overlay
std::string info_text = absl::StrFormat("ID:%d X:%d Y:%d S:%d", object.id_,
object.x_, object.y_, object.size_);
// Draw text at the object position
canvas_.DrawText(info_text, canvas_x, canvas_y - 12);
}
void DungeonEditor::RenderLayoutObjects(const zelda3::RoomLayout &layout,
const gfx::SnesPalette &palette) {
// Render layout objects (walls, floors, etc.) as simple colored rectangles
// This provides a visual representation of the room's structure
for (const auto &layout_obj : layout.GetObjects()) {
int canvas_x = layout_obj.x() * 16;
int canvas_y = layout_obj.y() * 16;
// Choose color based on object type
gfx::SnesColor color;
switch (layout_obj.type()) {
case zelda3::RoomLayoutObject::Type::kWall:
color = gfx::SnesColor(0x7FFF); // Gray
break;
case zelda3::RoomLayoutObject::Type::kFloor:
color = gfx::SnesColor(0x4210); // Dark brown
break;
case zelda3::RoomLayoutObject::Type::kCeiling:
color = gfx::SnesColor(0x739C); // Light gray
break;
case zelda3::RoomLayoutObject::Type::kPit:
color = gfx::SnesColor(0x0000); // Black
break;
case zelda3::RoomLayoutObject::Type::kWater:
color = gfx::SnesColor(0x001F); // Blue
break;
case zelda3::RoomLayoutObject::Type::kStairs:
color = gfx::SnesColor(0x7E0F); // Yellow
break;
case zelda3::RoomLayoutObject::Type::kDoor:
color = gfx::SnesColor(0xF800); // Red
break;
default:
color = gfx::SnesColor(0x7C1F); // Magenta for unknown
break;
}
// Draw a simple rectangle for the layout object
// This is a placeholder - in a real implementation, you'd render the actual
// tile
canvas_.DrawRect(canvas_x, canvas_y, 16, 16,
gui::ConvertSnesColorToImVec4(color));
}
}
void DungeonEditor::DrawRoomGraphics() {
const auto height = 0x40;
room_gfx_canvas_.DrawBackground();
@@ -543,14 +821,26 @@ void DungeonEditor::DrawObjectRenderer() {
TableNextColumn();
BeginChild("DungeonObjectButtons", ImVec2(250, 0), true);
int selected_object = 0;
static int selected_object = 0;
int i = 0;
for (const auto object_name : zelda3::Type1RoomObjectNames) {
if (ImGui::Selectable(object_name.data(), selected_object == i)) {
selected_object = i;
// object_renderer_.LoadObject(i,
// rooms_[current_room_id_].mutable_blocks());
// object_loaded_ = true;
// Create a test object and render it
auto test_object = zelda3::RoomObject(i, 0, 0, 0x12, 0);
test_object.set_rom(rom_);
test_object.EnsureTilesLoaded();
// Get current palette
auto palette =
rom()->palette_group().dungeon_main[current_palette_group_id_];
// Render object preview
auto result = object_renderer_.GetObjectPreview(test_object, palette);
if (result.ok()) {
object_loaded_ = true;
}
}
i += 1;
}
@@ -572,10 +862,8 @@ void DungeonEditor::DrawObjectRenderer() {
}
if (object_loaded_) {
ImGui::Begin("Memory Viewer", &object_loaded_, 0);
static MemoryEditor mem_edit;
// mem_edit.DrawContents(object_renderer_.mutable_memory()->data(),
// object_renderer_.mutable_memory()->size());
ImGui::Begin("Object Preview", &object_loaded_, 0);
ImGui::Text("Object rendered successfully using improved renderer!");
ImGui::End();
}
}

View File

@@ -8,6 +8,7 @@
#include "app/gui/canvas.h"
#include "app/rom.h"
#include "imgui/imgui.h"
#include "zelda3/dungeon/object_renderer.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_entrance.h"
#include "zelda3/dungeon/room_object.h"
@@ -40,7 +41,8 @@ constexpr ImGuiTableFlags kDungeonTableFlags =
*/
class DungeonEditor : public Editor {
public:
explicit DungeonEditor(Rom* rom = nullptr) : rom_(rom) {
explicit DungeonEditor(Rom* rom = nullptr)
: rom_(rom), object_renderer_(rom) {
type_ = EditorType::kDungeon;
}
@@ -78,6 +80,26 @@ class DungeonEditor : public Editor {
void DrawTileSelector();
void DrawObjectRenderer();
// Object rendering methods
void RenderObjectInCanvas(const zelda3::RoomObject& object,
const gfx::SnesPalette& palette);
void DisplayObjectInfo(const zelda3::RoomObject& object, int canvas_x,
int canvas_y);
void RenderLayoutObjects(const zelda3::RoomLayout& layout,
const gfx::SnesPalette& palette);
// Object rendering cache to avoid re-rendering the same objects
struct ObjectRenderCache {
int object_id;
int object_x, object_y, object_size;
uint64_t palette_hash;
gfx::Bitmap rendered_bitmap;
bool is_valid;
};
std::vector<ObjectRenderCache> object_render_cache_;
uint64_t last_palette_hash_ = 0;
void CalculateUsageStats();
void DrawUsageStats();
void DrawUsageGrid();
@@ -123,7 +145,7 @@ class DungeonEditor : public Editor {
std::array<zelda3::Room, 0x128> rooms_ = {};
std::array<zelda3::RoomEntrance, 0x8C> entrances_ = {};
// zelda3::DungeonObjectRenderer object_renderer_;
zelda3::ObjectRenderer object_renderer_;
absl::flat_hash_map<uint16_t, int> spriteset_usage_;
absl::flat_hash_map<uint16_t, int> blockset_usage_;

View File

@@ -0,0 +1,207 @@
#include "object_parser.h"
#include <algorithm>
#include <cstring>
#include "absl/strings/str_format.h"
#include "app/zelda3/dungeon/room_object.h"
namespace yaze {
namespace zelda3 {
absl::StatusOr<std::vector<gfx::Tile16>> ObjectParser::ParseObject(int16_t object_id) {
if (rom_ == nullptr) {
return absl::InvalidArgumentError("ROM is null");
}
int subtype = DetermineSubtype(object_id);
switch (subtype) {
case 1:
return ParseSubtype1(object_id);
case 2:
return ParseSubtype2(object_id);
case 3:
return ParseSubtype3(object_id);
default:
return absl::InvalidArgumentError(
absl::StrFormat("Invalid object subtype for ID: %#04x", object_id));
}
}
absl::StatusOr<ObjectRoutineInfo> ObjectParser::ParseObjectRoutine(int16_t object_id) {
if (rom_ == nullptr) {
return absl::InvalidArgumentError("ROM is null");
}
auto subtype_info = GetObjectSubtype(object_id);
if (!subtype_info.ok()) {
return subtype_info.status();
}
ObjectRoutineInfo routine_info;
routine_info.routine_ptr = subtype_info->routine_ptr;
routine_info.tile_ptr = subtype_info->subtype_ptr;
routine_info.tile_count = subtype_info->max_tile_count;
routine_info.is_repeatable = true;
routine_info.is_orientation_dependent = true;
return routine_info;
}
absl::StatusOr<ObjectSubtypeInfo> ObjectParser::GetObjectSubtype(int16_t object_id) {
ObjectSubtypeInfo info;
info.subtype = DetermineSubtype(object_id);
switch (info.subtype) {
case 1: {
int index = object_id & 0xFF;
info.subtype_ptr = kRoomObjectSubtype1 + (index * 2);
info.routine_ptr = kRoomObjectSubtype1 + 0x200 + (index * 2);
info.max_tile_count = 8; // Most subtype 1 objects use 8 tiles
break;
}
case 2: {
int index = object_id & 0x7F;
info.subtype_ptr = kRoomObjectSubtype2 + (index * 2);
info.routine_ptr = kRoomObjectSubtype2 + 0x80 + (index * 2);
info.max_tile_count = 8;
break;
}
case 3: {
int index = object_id & 0xFF;
info.subtype_ptr = kRoomObjectSubtype3 + (index * 2);
info.routine_ptr = kRoomObjectSubtype3 + 0x100 + (index * 2);
info.max_tile_count = 8;
break;
}
default:
return absl::InvalidArgumentError(
absl::StrFormat("Invalid object subtype for ID: %#04x", object_id));
}
return info;
}
absl::StatusOr<ObjectSizeInfo> ObjectParser::ParseObjectSize(int16_t object_id, uint8_t size_byte) {
ObjectSizeInfo info;
// Extract size bits (0-3 for X, 4-7 for Y)
int size_x = size_byte & 0x03;
int size_y = (size_byte >> 2) & 0x03;
info.width_tiles = (size_x + 1) * 2; // Convert to tile count
info.height_tiles = (size_y + 1) * 2;
// Determine orientation based on object ID and size
// This is a heuristic based on the object naming patterns
if (object_id >= 0x80 && object_id <= 0xFF) {
// Objects 0x80-0xFF are typically vertical
info.is_horizontal = false;
} else {
// Objects 0x00-0x7F are typically horizontal
info.is_horizontal = true;
}
// Determine if object is repeatable
info.is_repeatable = (size_byte != 0);
info.repeat_count = size_byte == 0 ? 32 : size_byte;
return info;
}
absl::StatusOr<std::vector<gfx::Tile16>> ObjectParser::ParseSubtype1(int16_t object_id) {
int index = object_id & 0xFF;
int tile_ptr = kRoomObjectSubtype1 + (index * 2);
if (tile_ptr + 1 >= (int)rom_->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Tile pointer out of range: %#06x", tile_ptr));
}
// Read tile data pointer
uint8_t low = rom_->data()[tile_ptr];
uint8_t high = rom_->data()[tile_ptr + 1];
int tile_data_ptr = kRoomObjectTileAddress + ((high << 8) | low);
// Read 8 tiles (most subtype 1 objects use 8 tiles)
return ReadTileData(tile_data_ptr, 8);
}
absl::StatusOr<std::vector<gfx::Tile16>> ObjectParser::ParseSubtype2(int16_t object_id) {
int index = object_id & 0x7F;
int tile_ptr = kRoomObjectSubtype2 + (index * 2);
if (tile_ptr + 1 >= (int)rom_->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Tile pointer out of range: %#06x", tile_ptr));
}
// Read tile data pointer
uint8_t low = rom_->data()[tile_ptr];
uint8_t high = rom_->data()[tile_ptr + 1];
int tile_data_ptr = kRoomObjectTileAddress + ((high << 8) | low);
// Read 8 tiles
return ReadTileData(tile_data_ptr, 8);
}
absl::StatusOr<std::vector<gfx::Tile16>> ObjectParser::ParseSubtype3(int16_t object_id) {
int index = object_id & 0xFF;
int tile_ptr = kRoomObjectSubtype3 + (index * 2);
if (tile_ptr + 1 >= (int)rom_->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Tile pointer out of range: %#06x", tile_ptr));
}
// Read tile data pointer
uint8_t low = rom_->data()[tile_ptr];
uint8_t high = rom_->data()[tile_ptr + 1];
int tile_data_ptr = kRoomObjectTileAddress + ((high << 8) | low);
// Read 8 tiles
return ReadTileData(tile_data_ptr, 8);
}
absl::StatusOr<std::vector<gfx::Tile16>> ObjectParser::ReadTileData(int address, int tile_count) {
if (address < 0 || address + (tile_count * 8) >= (int)rom_->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Tile data address out of range: %#06x", address));
}
std::vector<gfx::Tile16> tiles;
tiles.reserve(tile_count);
for (int i = 0; i < tile_count; i++) {
int tile_offset = address + (i * 8);
// Read 4 words (8 bytes) per tile
uint16_t w0 = rom_->data()[tile_offset] | (rom_->data()[tile_offset + 1] << 8);
uint16_t w1 = rom_->data()[tile_offset + 2] | (rom_->data()[tile_offset + 3] << 8);
uint16_t w2 = rom_->data()[tile_offset + 4] | (rom_->data()[tile_offset + 5] << 8);
uint16_t w3 = rom_->data()[tile_offset + 6] | (rom_->data()[tile_offset + 7] << 8);
tiles.emplace_back(
gfx::WordToTileInfo(w0),
gfx::WordToTileInfo(w1),
gfx::WordToTileInfo(w2),
gfx::WordToTileInfo(w3)
);
}
return tiles;
}
int ObjectParser::DetermineSubtype(int16_t object_id) const {
if (object_id >= 0x200) {
return 3;
} else if (object_id >= 0x100) {
return 2;
} else {
return 1;
}
}
} // namespace zelda3
} // namespace yaze

View File

@@ -0,0 +1,145 @@
#ifndef YAZE_APP_ZELDA3_DUNGEON_OBJECT_PARSER_H
#define YAZE_APP_ZELDA3_DUNGEON_OBJECT_PARSER_H
#include <cstdint>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/gfx/snes_tile.h"
#include "app/rom.h"
namespace yaze {
namespace zelda3 {
/**
* @brief Object routine information
*/
struct ObjectRoutineInfo {
uint32_t routine_ptr;
uint32_t tile_ptr;
int tile_count;
bool is_repeatable;
bool is_orientation_dependent;
ObjectRoutineInfo()
: routine_ptr(0),
tile_ptr(0),
tile_count(0),
is_repeatable(false),
is_orientation_dependent(false) {}
};
/**
* @brief Object subtype information
*/
struct ObjectSubtypeInfo {
int subtype;
uint32_t subtype_ptr;
uint32_t routine_ptr;
int max_tile_count;
ObjectSubtypeInfo()
: subtype(0), subtype_ptr(0), routine_ptr(0), max_tile_count(0) {}
};
/**
* @brief Object size and orientation information
*/
struct ObjectSizeInfo {
int width_tiles;
int height_tiles;
bool is_horizontal;
bool is_repeatable;
int repeat_count;
ObjectSizeInfo()
: width_tiles(0),
height_tiles(0),
is_horizontal(true),
is_repeatable(false),
repeat_count(1) {}
};
/**
* @brief Direct ROM parser for dungeon objects
*
* This class replaces the SNES emulation approach with direct ROM parsing,
* providing better performance and reliability for object rendering.
*/
class ObjectParser {
public:
explicit ObjectParser(Rom* rom) : rom_(rom) {}
/**
* @brief Parse object data directly from ROM
*
* @param object_id The object ID to parse
* @return StatusOr containing the parsed tile data
*/
absl::StatusOr<std::vector<gfx::Tile16>> ParseObject(int16_t object_id);
/**
* @brief Parse object routine data
*
* @param object_id The object ID
* @return StatusOr containing routine information
*/
absl::StatusOr<ObjectRoutineInfo> ParseObjectRoutine(int16_t object_id);
/**
* @brief Get object subtype information
*
* @param object_id The object ID
* @return StatusOr containing subtype information
*/
absl::StatusOr<ObjectSubtypeInfo> GetObjectSubtype(int16_t object_id);
/**
* @brief Parse object size and orientation
*
* @param object_id The object ID
* @param size_byte The size byte from object data
* @return StatusOr containing size and orientation info
*/
absl::StatusOr<ObjectSizeInfo> ParseObjectSize(int16_t object_id,
uint8_t size_byte);
/**
* @brief Determine object subtype from ID
*/
int DetermineSubtype(int16_t object_id) const;
private:
/**
* @brief Parse subtype 1 objects (0x00-0xFF)
*/
absl::StatusOr<std::vector<gfx::Tile16>> ParseSubtype1(int16_t object_id);
/**
* @brief Parse subtype 2 objects (0x100-0x1FF)
*/
absl::StatusOr<std::vector<gfx::Tile16>> ParseSubtype2(int16_t object_id);
/**
* @brief Parse subtype 3 objects (0x200+)
*/
absl::StatusOr<std::vector<gfx::Tile16>> ParseSubtype3(int16_t object_id);
/**
* @brief Read tile data from ROM
*
* @param address The address to read from
* @param tile_count Number of tiles to read
* @return StatusOr containing tile data
*/
absl::StatusOr<std::vector<gfx::Tile16>> ReadTileData(int address,
int tile_count);
Rom* rom_;
};
} // namespace zelda3
} // namespace yaze
#endif // YAZE_APP_ZELDA3_DUNGEON_OBJECT_PARSER_H

View File

@@ -1,145 +1,259 @@
#include "app/zelda3/dungeon/object_renderer.h"
#include "object_renderer.h"
#include <algorithm>
#include <cstring>
#include "absl/strings/str_format.h"
#include "app/gfx/arena.h"
namespace yaze {
namespace zelda3 {
void DungeonObjectRenderer::LoadObject(uint32_t routine_ptr,
std::array<uint8_t, 16>& sheet_ids) {
vram_.sheets = sheet_ids;
absl::StatusOr<gfx::Bitmap> ObjectRenderer::RenderObject(
const RoomObject& object, const gfx::SnesPalette& palette) {
// Ensure object has tiles loaded
if (object.tiles().empty()) {
return absl::FailedPreconditionError("Object has no tiles loaded");
}
rom_data_ = rom()->vector();
snes_.memory().Initialize(rom_data_);
// Create bitmap for the object
gfx::Bitmap bitmap = CreateBitmap(32, 32); // Default 32x32 pixels
// Configure the object based on the fetched information
ConfigureObject();
// Render each tile
for (size_t i = 0; i < object.tiles().size(); ++i) {
int tile_x = (i % 2) * 16; // 2 tiles per row
int tile_y = (i / 2) * 16;
auto status = RenderTile(object.tiles()[i], bitmap, tile_x, tile_y, palette);
if (!status.ok()) {
return status;
}
}
// Run the CPU emulation for the object's draw routines
RenderObject(routine_ptr);
return bitmap;
}
void DungeonObjectRenderer::ConfigureObject() {
snes_.cpu().A = 0x03D8;
snes_.cpu().X = 0x03D8;
snes_.cpu().DB = 0x7E;
// VRAM target destinations
snes_.cpu().WriteLong(0xBF, 0x7E2000);
snes_.cpu().WriteLong(0xCB, 0x7E2080);
snes_.cpu().WriteLong(0xC2, 0x7E2002);
snes_.cpu().WriteLong(0xCE, 0x7E2082);
snes_.cpu().SetAccumulatorSize(false);
snes_.cpu().SetIndexSize(false);
}
absl::StatusOr<gfx::Bitmap> ObjectRenderer::RenderObjects(
const std::vector<RoomObject>& objects, const gfx::SnesPalette& palette,
int width, int height) {
gfx::Bitmap bitmap = CreateBitmap(width, height);
/**
* Example:
* the STA $BF, $CD, $C2, $CE are the location of the object in the room
* $B2 is used for size loop
* so if object size is setted on 07 that draw code will be repeated 7 times
* and since Y is increasing by 4 it makes the object draw from left to right
RoomDraw_Rightwards2x2_1to15or32:
#_018B89: JSR RoomDraw_GetSize_1to15or32
.next
#_018B8C: JSR RoomDraw_Rightwards2x2
#_018B8F: DEC.b $B2
#_018B91: BNE .next
#_018B93: RTS
RoomDraw_Rightwards2x2:
#_019895: LDA.w RoomDrawObjectData+0,X
#_019898: STA.b [$BF],Y
#_01989A: LDA.w RoomDrawObjectData+2,X
#_01989D: STA.b [$CB],Y
#_01989F: LDA.w RoomDrawObjectData+4,X
#_0198A2: STA.b [$C2],Y
#_0198A4: LDA.w RoomDrawObjectData+6,X
#_0198A7: STA.b [$CE],Y
#_0198A9: INY #4
#_0198AD: RTS
*/
void DungeonObjectRenderer::RenderObject(uint32_t routine_ptr) {
snes_.cpu().PB = 0x01;
snes_.cpu().PC = routine_ptr;
// Set up initial state for object drawing
snes_.cpu().Y = 0; // Start at the beginning of the tilemap
snes_.cpu().D = 0x7E; // Direct page register for memory access
// Push return address to stack
snes_.cpu().PushLong(0x01 << 16 | 0xFFFF); // Push a dummy return address
// Set up a maximum instruction count to prevent infinite loops
const int MAX_INSTRUCTIONS = 10000;
int instruction_count = 0;
// Execute instructions until we hit a return instruction or max count
while (instruction_count < MAX_INSTRUCTIONS) {
uint8_t opcode =
snes_.cpu().ReadByte(snes_.cpu().PB << 16 | snes_.cpu().PC);
// Check for RTS (Return from Subroutine) instruction
if (opcode == 0x60) {
// Execute the RTS instruction
snes_.cpu().ExecuteInstruction(opcode);
break; // Exit the loop after RTS
for (const auto& object : objects) {
if (object.tiles().empty()) {
continue; // Skip objects without tiles
}
// Execute the instruction
snes_.cpu().ExecuteInstruction(opcode);
instruction_count++;
// Calculate object position in the bitmap
int obj_x = object.x_ * 16; // Convert room coordinates to pixel coordinates
int obj_y = object.y_ * 16;
// Render each tile of the object
for (size_t i = 0; i < object.tiles().size(); ++i) {
int tile_x = obj_x + (i % 2) * 16;
int tile_y = obj_y + (i / 2) * 16;
// Check bounds
if (tile_x >= 0 && tile_x < width && tile_y >= 0 && tile_y < height) {
auto status = RenderTile(object.tiles()[i], bitmap, tile_x, tile_y, palette);
if (!status.ok()) {
return status;
}
}
}
}
// If we hit the max instruction count, log a warning
if (instruction_count >= MAX_INSTRUCTIONS) {
std::cerr << "Warning: Object rendering hit maximum instruction count"
<< std::endl;
}
UpdateObjectBitmap();
return bitmap;
}
// In the underworld, this holds a copy of the entire BG tilemap for
// Layer 1 (BG2) in TILEMAPA
// Layer 2 (BG1) in TILEMAPB
void DungeonObjectRenderer::UpdateObjectBitmap() {
// Initialize the tilemap with zeros
tilemap_.resize(0x2000, 0);
absl::StatusOr<gfx::Bitmap> ObjectRenderer::RenderObjectWithSize(
const RoomObject& object, const gfx::SnesPalette& palette,
const ObjectSizeInfo& size_info) {
if (object.tiles().empty()) {
return absl::FailedPreconditionError("Object has no tiles loaded");
}
// Iterate over tilemap in memory to read tile IDs
for (int tile_index = 0; tile_index < 512; tile_index++) {
// Read the tile ID from memory
uint16_t tile_id = snes_.memory().ReadWord(0x7E2000 + tile_index * 2);
// Calculate bitmap size based on object size
int bitmap_width = size_info.width_tiles * 16;
int bitmap_height = size_info.height_tiles * 16;
gfx::Bitmap bitmap = CreateBitmap(bitmap_width, bitmap_height);
// Skip empty tiles (0x0000)
if (tile_id == 0) continue;
// Render tiles based on orientation
if (size_info.is_horizontal) {
// Horizontal rendering
for (int repeat = 0; repeat < size_info.repeat_count; ++repeat) {
for (size_t i = 0; i < object.tiles().size(); ++i) {
int tile_x = (repeat * 2) + (i % 2);
int tile_y = i / 2;
if (tile_x < size_info.width_tiles && tile_y < size_info.height_tiles) {
auto status = RenderTile(object.tiles()[i], bitmap,
tile_x * 16, tile_y * 16, palette);
if (!status.ok()) {
return status;
}
}
}
}
} else {
// Vertical rendering
for (int repeat = 0; repeat < size_info.repeat_count; ++repeat) {
for (size_t i = 0; i < object.tiles().size(); ++i) {
int tile_x = i % 2;
int tile_y = (repeat * 2) + (i / 2);
if (tile_x < size_info.width_tiles && tile_y < size_info.height_tiles) {
auto status = RenderTile(object.tiles()[i], bitmap,
tile_x * 16, tile_y * 16, palette);
if (!status.ok()) {
return status;
}
}
}
}
}
// Calculate sheet number (each sheet contains 32 tiles)
int sheet_number = tile_id / 32;
return bitmap;
}
// Ensure sheet number is valid
if (sheet_number >= vram_.sheets.size()) {
std::cerr << "Warning: Invalid sheet number " << sheet_number
<< std::endl;
absl::StatusOr<gfx::Bitmap> ObjectRenderer::GetObjectPreview(
const RoomObject& object, const gfx::SnesPalette& palette) {
if (object.tiles().empty()) {
return absl::FailedPreconditionError("Object has no tiles loaded");
}
// Create a smaller preview bitmap (16x16 pixels)
gfx::Bitmap bitmap = CreateBitmap(16, 16);
// Render only the first tile as a preview
auto status = RenderTile(object.tiles()[0], bitmap, 0, 0, palette);
if (!status.ok()) {
return status;
}
return bitmap;
}
absl::Status ObjectRenderer::RenderTile(const gfx::Tile16& tile,
gfx::Bitmap& bitmap,
int x, int y,
const gfx::SnesPalette& palette) {
// Check if bitmap is valid
if (!bitmap.is_active() || bitmap.surface() == nullptr) {
return absl::FailedPreconditionError("Bitmap is not properly initialized");
}
// Get the graphics sheet from Arena - this contains the actual pixel data
auto& arena = gfx::Arena::Get();
// Render the 4 sub-tiles of the Tile16
std::array<gfx::TileInfo, 4> sub_tiles = {
tile.tile0_, tile.tile1_, tile.tile2_, tile.tile3_
};
for (int i = 0; i < 4; ++i) {
const auto& tile_info = sub_tiles[i];
int sub_x = x + (i % 2) * 8;
int sub_y = y + (i / 2) * 8;
// Get the graphics sheet that contains this tile
// Tile IDs are typically organized in sheets of 256 tiles each
int sheet_index = tile_info.id_ / 256;
if (sheet_index >= 223) { // Arena has 223 graphics sheets
sheet_index = 0; // Fallback to first sheet
}
auto graphics_sheet = arena.gfx_sheet(sheet_index);
if (!graphics_sheet.is_active()) {
// If graphics sheet is not loaded, create a simple pattern
RenderTilePattern(bitmap, sub_x, sub_y, tile_info, palette);
continue;
}
// Calculate tile position within the graphics sheet
int tile_x = (tile_info.id_ % 16) * 8; // 16 tiles per row, 8 pixels per tile
int tile_y = ((tile_info.id_ % 256) / 16) * 8; // 16 rows per sheet
// Render the 8x8 tile from the graphics sheet
for (int py = 0; py < 8; ++py) {
for (int px = 0; px < 8; ++px) {
if (sub_x + px < bitmap.width() && sub_y + py < bitmap.height()) {
// Get pixel from graphics sheet
int src_x = tile_x + px;
int src_y = tile_y + py;
if (src_x < graphics_sheet.width() && src_y < graphics_sheet.height()) {
int pixel_index = src_y * graphics_sheet.width() + src_x;
if (pixel_index < (int)graphics_sheet.size()) {
uint8_t color_index = graphics_sheet.at(pixel_index);
// Apply palette
if (color_index < palette.size()) {
// Apply mirroring if needed
int final_x = sub_x + px;
int final_y = sub_y + py;
if (tile_info.horizontal_mirror_) {
final_x = sub_x + (7 - px);
}
if (tile_info.vertical_mirror_) {
final_y = sub_y + (7 - py);
}
if (final_x < bitmap.width() && final_y < bitmap.height()) {
bitmap.SetPixel(final_x, final_y, palette[color_index]);
}
}
}
}
}
}
}
}
// Calculate position in the tilemap
int tile_x = (tile_index % 32) * 8;
int tile_y = (tile_index / 32) * 8;
return absl::OkStatus();
}
// Get the graphics sheet
auto& sheet =
gfx::Arena::Get().mutable_gfx_sheets()->at(vram_.sheets[sheet_number]);
absl::Status ObjectRenderer::ApplyObjectSize(gfx::Bitmap& bitmap,
const ObjectSizeInfo& size_info) {
// This method would apply size and orientation transformations
// For now, it's a placeholder
return absl::OkStatus();
}
// Calculate the offset in the tilemap
int tilemap_offset = tile_y * 256 + tile_x;
gfx::Bitmap ObjectRenderer::CreateBitmap(int width, int height) {
// Create a bitmap with proper initialization
std::vector<uint8_t> data(width * height, 0); // Initialize with zeros
gfx::Bitmap bitmap(width, height, 8, data); // 8-bit depth
return bitmap;
}
// Copy the tile from the graphics sheet to the tilemap
sheet.Get8x8Tile(tile_id % 32, 0, 0, tilemap_, tilemap_offset);
void ObjectRenderer::RenderTilePattern(gfx::Bitmap& bitmap, int x, int y,
const gfx::TileInfo& tile_info,
const gfx::SnesPalette& palette) {
// Create a simple pattern based on tile ID and palette
// This is used when the graphics sheet is not available
for (int py = 0; py < 8; ++py) {
for (int px = 0; px < 8; ++px) {
if (x + px < bitmap.width() && y + py < bitmap.height()) {
// Create a simple pattern based on tile ID
int pattern_value = (tile_info.id_ + px + py) % 16;
// Use different colors based on the pattern
int color_index = pattern_value % palette.size();
if (color_index > 0) { // Skip transparent color (index 0)
bitmap.SetPixel(x + px, y + py, palette[color_index]);
}
}
}
}
}
} // namespace zelda3
} // namespace yaze
} // namespace yaze

View File

@@ -1,77 +1,105 @@
#ifndef YAZE_APP_ZELDA3_DUNGEON_OBJECT_RENDERER_H
#define YAZE_APP_ZELDA3_DUNGEON_OBJECT_RENDERER_H
#include <cstdint>
#include <vector>
#include "app/emu/snes.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/gfx/bitmap.h"
#include "app/gfx/snes_palette.h"
#include "app/rom.h"
#include "app/zelda3/dungeon/object_parser.h"
#include "app/zelda3/dungeon/room_object.h"
namespace yaze {
namespace zelda3 {
/**
* @struct PseudoVram
* @brief Simulates the SNES VRAM for object rendering
* @brief Dungeon object renderer using direct ROM parsing
*
* This structure holds the sheet IDs and palettes needed for rendering
* dungeon objects in Link to the Past.
* This class provides high-performance object rendering using direct ROM
* parsing, providing better performance, reliability, and maintainability.
*/
struct PseudoVram {
std::array<uint8_t, 16> sheets = {0};
std::vector<gfx::SnesPalette> palettes;
};
/**
* @class DungeonObjectRenderer
* @brief Renders dungeon objects from Link to the Past
*
* This class uses the emulator subsystem to simulate the SNES CPU
* drawing routines for dungeon objects. It captures the tile data
* written to memory and renders it to a bitmap.
*/
class DungeonObjectRenderer {
class ObjectRenderer {
public:
DungeonObjectRenderer() = default;
explicit ObjectRenderer(Rom* rom) : rom_(rom), parser_(rom) {}
/**
* @brief Loads and renders a dungeon object
* @brief Render a single object to a bitmap
*
* @param routine_ptr Pointer to the drawing routine in ROM
* @param sheet_ids Array of graphics sheet IDs used by the object
* @param object The room object to render
* @param palette The palette to use for rendering
* @return StatusOr containing the rendered bitmap
*/
void LoadObject(uint32_t routine_ptr, std::array<uint8_t, 16>& sheet_ids);
absl::StatusOr<gfx::Bitmap> RenderObject(const RoomObject& object,
const gfx::SnesPalette& palette);
/**
* @brief Configures the CPU state for object rendering
*/
void ConfigureObject();
/**
* @brief Executes the object drawing routine
* @brief Render multiple objects to a single bitmap
*
* @param routine_ptr Pointer to the drawing routine in ROM
* @param objects Vector of room objects to render
* @param palette The palette to use for rendering
* @param width Width of the output bitmap
* @param height Height of the output bitmap
* @return StatusOr containing the rendered bitmap
*/
void RenderObject(uint32_t routine_ptr);
absl::StatusOr<gfx::Bitmap> RenderObjects(
const std::vector<RoomObject>& objects, const gfx::SnesPalette& palette,
int width = 256, int height = 256);
/**
* @brief Updates the bitmap with the rendered object
* @brief Render object with size and orientation
*
* @param object The room object to render
* @param palette The palette to use for rendering
* @param size_info Size and orientation information
* @return StatusOr containing the rendered bitmap
*/
void UpdateObjectBitmap();
absl::StatusOr<gfx::Bitmap> RenderObjectWithSize(
const RoomObject& object, const gfx::SnesPalette& palette,
const ObjectSizeInfo& size_info);
auto mutable_memory() { return &tilemap_; }
auto rom() { return rom_; }
auto mutable_rom() { return &rom_; }
/**
* @brief Get object preview (smaller version for UI)
*
* @param object The room object to preview
* @param palette The palette to use
* @return StatusOr containing the preview bitmap
*/
absl::StatusOr<gfx::Bitmap> GetObjectPreview(const RoomObject& object,
const gfx::SnesPalette& palette);
private:
std::vector<uint8_t> tilemap_;
std::vector<uint8_t> rom_data_;
/**
* @brief Render a single tile to the bitmap
*/
absl::Status RenderTile(const gfx::Tile16& tile, gfx::Bitmap& bitmap, int x,
int y, const gfx::SnesPalette& palette);
PseudoVram vram_;
/**
* @brief Apply object size and orientation
*/
absl::Status ApplyObjectSize(gfx::Bitmap& bitmap,
const ObjectSizeInfo& size_info);
/**
* @brief Create a bitmap with the specified dimensions
*/
gfx::Bitmap CreateBitmap(int width, int height);
/**
* @brief Render a simple pattern when graphics sheet is not available
*/
void RenderTilePattern(gfx::Bitmap& bitmap, int x, int y,
const gfx::TileInfo& tile_info,
const gfx::SnesPalette& palette);
Rom* rom_;
emu::Snes snes_;
gfx::Bitmap bitmap_;
ObjectParser parser_;
};
} // namespace zelda3
} // namespace yaze
#endif // YAZE_APP_ZELDA3_DUNGEON_OBJECT_RENDERER_H

View File

@@ -187,6 +187,15 @@ Room LoadRoomFromRom(Rom *rom, int room_id) {
int spr_ptr = 0x040000 | rooms_sprite_pointer;
int sprite_address = SnesToPc(dungeon_spr_ptrs | spr_ptr + (room_id * 2));
// Load room layout
room.LoadRoomLayout();
// Load additional room features
room.LoadDoors();
room.LoadTorches();
room.LoadBlocks();
room.LoadPits();
return room;
}
@@ -389,6 +398,7 @@ void Room::LoadObjects() {
}
RoomObject r(oid, posX, posY, sizeXY, static_cast<uint8_t>(layer));
r.set_rom(rom_);
tile_objects_.push_back(r);
for (short stair : stairsObjects) {
@@ -507,5 +517,63 @@ void Room::LoadChests() {
}
}
void Room::LoadRoomLayout() {
// Use the new RoomLayout system to load walls, floors, and structural elements
auto status = layout_.LoadLayout(room_id_);
if (!status.ok()) {
// Log error but don't fail - some rooms might not have layout data
util::logf("Failed to load room layout for room %d: %s",
room_id_, status.message().data());
return;
}
// Store the layout ID for compatibility with existing code
layout = static_cast<uint8_t>(room_id_ & 0xFF);
util::logf("Loaded room layout for room %d with %zu objects",
room_id_, layout_.GetObjects().size());
}
void Room::LoadDoors() {
auto rom_data = rom()->vector();
// Load door graphics and positions
// Door graphics are stored at door_gfx_* addresses
// Door positions are stored at door_pos_* addresses
// For now, create placeholder door objects
// TODO: Implement full door loading from ROM data
}
void Room::LoadTorches() {
auto rom_data = rom()->vector();
// Load torch data from torch_data address
int torch_count = rom_data[torches_length_pointer + 1] << 8 | rom_data[torches_length_pointer];
// For now, create placeholder torch objects
// TODO: Implement full torch loading from ROM data
}
void Room::LoadBlocks() {
auto rom_data = rom()->vector();
// Load block data from blocks_* addresses
int block_count = rom_data[blocks_length + 1] << 8 | rom_data[blocks_length];
// For now, create placeholder block objects
// TODO: Implement full block loading from ROM data
}
void Room::LoadPits() {
auto rom_data = rom()->vector();
// Load pit data from pit_pointer
int pit_count = rom_data[pit_count + 1] << 8 | rom_data[pit_count];
// For now, create placeholder pit objects
// TODO: Implement full pit loading from ROM data
}
} // namespace zelda3
} // namespace yaze

View File

@@ -9,6 +9,7 @@
#include "app/rom.h"
#include "app/zelda3/dungeon/room_object.h"
#include "app/zelda3/dungeon/room_layout.h"
#include "app/zelda3/sprite/sprite.h"
namespace yaze {
@@ -200,15 +201,22 @@ enum TagKey {
class Room {
public:
Room() = default;
Room(int room_id, Rom *rom) : room_id_(room_id), rom_(rom) {}
Room(int room_id, Rom *rom) : room_id_(room_id), rom_(rom), layout_(rom) {}
void LoadRoomGraphics(uint8_t entrance_blockset = 0xFF);
void CopyRoomGraphicsToBuffer();
void RenderRoomGraphics();
void RenderObjectsToBackground();
void LoadAnimatedGraphics();
void LoadObjects();
void LoadSprites();
void LoadChests();
void LoadRoomLayout();
void LoadDoors();
void LoadTorches();
void LoadBlocks();
void LoadPits();
const RoomLayout& GetLayout() const { return layout_; }
auto blocks() const { return blocks_; }
auto &mutable_blocks() { return blocks_; }
@@ -251,9 +259,13 @@ class Room {
std::array<chest, 16> chest_list_;
std::vector<RoomObject> tile_objects_;
// TODO: add separate door objects list when door section (F0 FF) is parsed
std::vector<zelda3::Sprite> sprites_;
std::vector<staircase> z3_staircases_;
std::vector<chest_data> chests_in_room_;
// Room layout system for walls, floors, and structural elements
RoomLayout layout_;
LayerMergeType layer_merging_;
CollisionKey collision_;

View File

@@ -0,0 +1,212 @@
#include "room_layout.h"
#include "absl/strings/str_format.h"
#include "app/zelda3/dungeon/room.h"
#include "app/snes.h"
namespace yaze {
namespace zelda3 {
absl::StatusOr<gfx::Tile16> RoomLayoutObject::GetTile() const {
// This would typically look up the actual tile data from the graphics sheets
// For now, we'll create a placeholder tile based on the object type
gfx::TileInfo tile_info;
tile_info.id_ = static_cast<uint16_t>(id_);
tile_info.palette_ = 0; // Default palette
tile_info.vertical_mirror_ = false;
tile_info.horizontal_mirror_ = false;
tile_info.over_ = false;
// Create a 16x16 tile with the same tile info for all 4 sub-tiles
return gfx::Tile16(tile_info, tile_info, tile_info, tile_info);
}
std::string RoomLayoutObject::GetTypeName() const {
switch (type_) {
case Type::kWall:
return "Wall";
case Type::kFloor:
return "Floor";
case Type::kCeiling:
return "Ceiling";
case Type::kPit:
return "Pit";
case Type::kWater:
return "Water";
case Type::kStairs:
return "Stairs";
case Type::kDoor:
return "Door";
case Type::kUnknown:
default:
return "Unknown";
}
}
absl::Status RoomLayout::LoadLayout(int room_id) {
if (rom_ == nullptr) {
return absl::InvalidArgumentError("ROM is null");
}
auto rom_data = rom_->vector();
// Load room layout from room_object_layout_pointer
// This follows the same pattern as the room object loading
int layout_pointer = (rom_data[room_object_layout_pointer + 2] << 16) +
(rom_data[room_object_layout_pointer + 1] << 8) +
(rom_data[room_object_layout_pointer]);
layout_pointer = SnesToPc(layout_pointer);
// Get the layout address for this room
int layout_address = layout_pointer + (room_id * 3);
int layout_location = SnesToPc(layout_address);
if (layout_location < 0 || layout_location + 2 >= (int)rom_->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Layout address out of range: %#06x", layout_location));
}
// Read the layout data (3 bytes: bank, high, low)
uint8_t bank = rom_data[layout_location + 2];
uint8_t high = rom_data[layout_location + 1];
uint8_t low = rom_data[layout_location];
// Construct the layout data address
int layout_data_address = SnesToPc((bank << 16) | (high << 8) | low);
if (layout_data_address < 0 || layout_data_address >= (int)rom_->size()) {
return absl::OutOfRangeError(absl::StrFormat(
"Layout data address out of range: %#06x", layout_data_address));
}
// Read layout data - this contains the room's wall/floor structure
// The format varies by room type, but typically contains tile IDs for each
// position
std::vector<uint8_t> layout_data;
layout_data.reserve(width_ * height_);
// Read the layout data (assuming 1 byte per tile position)
for (int i = 0; i < width_ * height_; ++i) {
if (layout_data_address + i < (int)rom_->size()) {
layout_data.push_back(rom_data[layout_data_address + i]);
} else {
layout_data.push_back(0); // Default to empty space
}
}
return ParseLayoutData(layout_data);
}
absl::Status RoomLayout::ParseLayoutData(const std::vector<uint8_t>& data) {
objects_.clear();
objects_.reserve(width_ * height_);
// Parse the layout data to create layout objects
// This is a simplified implementation - in reality, the format is more
// complex
for (int y = 0; y < height_; ++y) {
for (int x = 0; x < width_; ++x) {
int index = y * width_ + x;
if (index >= (int)data.size()) continue;
uint8_t tile_id = data[index];
// Determine object type based on tile ID
RoomLayoutObject::Type type = RoomLayoutObject::Type::kUnknown;
if (tile_id == 0) {
// Empty space - skip
continue;
} else if (tile_id >= 0x01 && tile_id <= 0x20) {
// Wall tiles
type = RoomLayoutObject::Type::kWall;
} else if (tile_id >= 0x21 && tile_id <= 0x40) {
// Floor tiles
type = RoomLayoutObject::Type::kFloor;
} else if (tile_id >= 0x41 && tile_id <= 0x60) {
// Ceiling tiles
type = RoomLayoutObject::Type::kCeiling;
} else if (tile_id >= 0x61 && tile_id <= 0x80) {
// Water tiles
type = RoomLayoutObject::Type::kWater;
} else if (tile_id >= 0x81 && tile_id <= 0xA0) {
// Stairs
type = RoomLayoutObject::Type::kStairs;
} else if (tile_id >= 0xA1 && tile_id <= 0xC0) {
// Doors
type = RoomLayoutObject::Type::kDoor;
}
// Create layout object
objects_.emplace_back(tile_id, x, y, type, 0);
}
}
return absl::OkStatus();
}
RoomLayoutObject RoomLayout::CreateLayoutObject(int16_t tile_id, uint8_t x,
uint8_t y, uint8_t layer) {
// Determine type based on tile ID
RoomLayoutObject::Type type = RoomLayoutObject::Type::kUnknown;
if (tile_id >= 0x01 && tile_id <= 0x20) {
type = RoomLayoutObject::Type::kWall;
} else if (tile_id >= 0x21 && tile_id <= 0x40) {
type = RoomLayoutObject::Type::kFloor;
} else if (tile_id >= 0x41 && tile_id <= 0x60) {
type = RoomLayoutObject::Type::kCeiling;
} else if (tile_id >= 0x61 && tile_id <= 0x80) {
type = RoomLayoutObject::Type::kWater;
} else if (tile_id >= 0x81 && tile_id <= 0xA0) {
type = RoomLayoutObject::Type::kStairs;
} else if (tile_id >= 0xA1 && tile_id <= 0xC0) {
type = RoomLayoutObject::Type::kDoor;
}
return RoomLayoutObject(tile_id, x, y, type, layer);
}
std::vector<RoomLayoutObject> RoomLayout::GetObjectsByType(
RoomLayoutObject::Type type) const {
std::vector<RoomLayoutObject> result;
for (const auto& obj : objects_) {
if (obj.type() == type) {
result.push_back(obj);
}
}
return result;
}
absl::StatusOr<RoomLayoutObject> RoomLayout::GetObjectAt(uint8_t x, uint8_t y,
uint8_t layer) const {
for (const auto& obj : objects_) {
if (obj.x() == x && obj.y() == y && obj.layer() == layer) {
return obj;
}
}
return absl::NotFoundError(
absl::StrFormat("No object found at position (%d, %d, %d)", x, y, layer));
}
bool RoomLayout::HasWall(uint8_t x, uint8_t y, uint8_t layer) const {
for (const auto& obj : objects_) {
if (obj.x() == x && obj.y() == y && obj.layer() == layer &&
obj.type() == RoomLayoutObject::Type::kWall) {
return true;
}
}
return false;
}
bool RoomLayout::HasFloor(uint8_t x, uint8_t y, uint8_t layer) const {
for (const auto& obj : objects_) {
if (obj.x() == x && obj.y() == y && obj.layer() == layer &&
obj.type() == RoomLayoutObject::Type::kFloor) {
return true;
}
}
return false;
}
} // namespace zelda3
} // namespace yaze

View File

@@ -0,0 +1,116 @@
#ifndef YAZE_APP_ZELDA3_DUNGEON_ROOM_LAYOUT_H
#define YAZE_APP_ZELDA3_DUNGEON_ROOM_LAYOUT_H
#include <cstdint>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/gfx/snes_tile.h"
#include "app/rom.h"
namespace yaze {
namespace zelda3 {
/**
* @brief Represents a room layout object (wall, floor, etc.)
*
* Room layout objects are the basic building blocks of dungeon rooms.
* They include walls, floors, ceilings, and other structural elements.
* Unlike regular room objects, these are loaded from the room layout data
* and represent the fundamental geometry of the room.
*/
class RoomLayoutObject {
public:
enum class Type {
kWall = 0,
kFloor = 1,
kCeiling = 2,
kPit = 3,
kWater = 4,
kStairs = 5,
kDoor = 6,
kUnknown = 7
};
RoomLayoutObject(int16_t id, uint8_t x, uint8_t y, Type type, uint8_t layer = 0)
: id_(id), x_(x), y_(y), type_(type), layer_(layer) {}
// Getters
int16_t id() const { return id_; }
uint8_t x() const { return x_; }
uint8_t y() const { return y_; }
Type type() const { return type_; }
uint8_t layer() const { return layer_; }
// Setters
void set_id(int16_t id) { id_ = id; }
void set_x(uint8_t x) { x_ = x; }
void set_y(uint8_t y) { y_ = y; }
void set_type(Type type) { type_ = type; }
void set_layer(uint8_t layer) { layer_ = layer; }
// Get tile data for this layout object
absl::StatusOr<gfx::Tile16> GetTile() const;
// Get the name/description of this layout object type
std::string GetTypeName() const;
private:
int16_t id_;
uint8_t x_;
uint8_t y_;
Type type_;
uint8_t layer_;
};
/**
* @brief Manages room layout data and objects
*
* This class handles loading and managing room layout objects from ROM data.
* It provides efficient access to wall, floor, and other layout elements
* without copying large amounts of data.
*/
class RoomLayout {
public:
RoomLayout() = default;
explicit RoomLayout(Rom* rom) : rom_(rom) {}
// Load layout data from ROM for a specific room
absl::Status LoadLayout(int room_id);
// Get all layout objects of a specific type
std::vector<RoomLayoutObject> GetObjectsByType(RoomLayoutObject::Type type) const;
// Get layout object at specific coordinates
absl::StatusOr<RoomLayoutObject> GetObjectAt(uint8_t x, uint8_t y, uint8_t layer = 0) const;
// Get all layout objects
const std::vector<RoomLayoutObject>& GetObjects() const { return objects_; }
// Check if a position has a wall
bool HasWall(uint8_t x, uint8_t y, uint8_t layer = 0) const;
// Check if a position has a floor
bool HasFloor(uint8_t x, uint8_t y, uint8_t layer = 0) const;
// Get room dimensions
std::pair<uint8_t, uint8_t> GetDimensions() const { return {width_, height_}; }
private:
Rom* rom_ = nullptr;
std::vector<RoomLayoutObject> objects_;
uint8_t width_ = 16; // Default room width in tiles
uint8_t height_ = 11; // Default room height in tiles
// Parse layout data from ROM
absl::Status ParseLayoutData(const std::vector<uint8_t>& data);
// Create layout object from tile data
RoomLayoutObject CreateLayoutObject(int16_t tile_id, uint8_t x, uint8_t y, uint8_t layer);
};
} // namespace zelda3
} // namespace yaze
#endif // YAZE_APP_ZELDA3_DUNGEON_ROOM_LAYOUT_H

View File

@@ -1,8 +1,31 @@
#include "room_object.h"
#include "absl/status/status.h"
#include "app/zelda3/dungeon/object_parser.h"
namespace yaze {
namespace zelda3 {
namespace {
struct SubtypeTableInfo {
int base_ptr; // base address of subtype table in ROM (PC)
int index_mask; // mask to apply to object id for index
SubtypeTableInfo(int base, int mask) : base_ptr(base), index_mask(mask) {}
};
SubtypeTableInfo GetSubtypeTable(int object_id) {
// Heuristic: 0x00-0xFF => subtype1, 0x100-0x1FF => subtype2, >=0x200 => subtype3
if (object_id >= 0x200) {
return SubtypeTableInfo(kRoomObjectSubtype3, 0xFF);
} else if (object_id >= 0x100) {
return SubtypeTableInfo(kRoomObjectSubtype2, 0x7F);
} else {
return SubtypeTableInfo(kRoomObjectSubtype1, 0xFF);
}
}
} // namespace
ObjectOption operator|(ObjectOption lhs, ObjectOption rhs) {
return static_cast<ObjectOption>(static_cast<int>(lhs) |
static_cast<int>(rhs));
@@ -129,5 +152,91 @@ void RoomObject::DrawTile(gfx::Tile16 t, int xx, int yy,
}
}
void RoomObject::EnsureTilesLoaded() {
if (tiles_loaded_) return;
if (rom_ == nullptr) return;
// Try the new parser first - this is more efficient and accurate
if (LoadTilesWithParser().ok()) {
tiles_loaded_ = true;
return;
}
// Fallback to old method for compatibility
auto rom_data = rom_->data();
// Determine which subtype table to use and compute the tile data offset.
SubtypeTableInfo sti = GetSubtypeTable(id_);
int index = (id_ & sti.index_mask);
int tile_ptr = sti.base_ptr + (index * 2);
if (tile_ptr < 0 || tile_ptr + 1 >= (int)rom_->size()) return;
int tile_rel = (int16_t)((rom_data[tile_ptr + 1] << 8) + rom_data[tile_ptr]);
int pos = kRoomObjectTileAddress + tile_rel;
tile_data_ptr_ = pos;
// Read one 16x16 (4 words) worth of tile info as a preview.
if (pos < 0 || pos + 7 >= (int)rom_->size()) return;
uint16_t w0 = (uint16_t)(rom_data[pos] | (rom_data[pos + 1] << 8));
uint16_t w1 = (uint16_t)(rom_data[pos + 2] | (rom_data[pos + 3] << 8));
uint16_t w2 = (uint16_t)(rom_data[pos + 4] | (rom_data[pos + 5] << 8));
uint16_t w3 = (uint16_t)(rom_data[pos + 6] | (rom_data[pos + 7] << 8));
tiles_.clear();
tiles_.push_back(gfx::Tile16(gfx::WordToTileInfo(w0), gfx::WordToTileInfo(w1),
gfx::WordToTileInfo(w2), gfx::WordToTileInfo(w3)));
tile_count_ = 1;
tiles_loaded_ = true;
}
absl::Status RoomObject::LoadTilesWithParser() {
if (rom_ == nullptr) {
return absl::InvalidArgumentError("ROM is null");
}
ObjectParser parser(rom_);
auto result = parser.ParseObject(id_);
if (!result.ok()) {
return result.status();
}
tiles_ = std::move(result.value());
tile_count_ = tiles_.size();
return absl::OkStatus();
}
absl::StatusOr<std::span<const gfx::Tile16>> RoomObject::GetTiles() const {
if (!tiles_loaded_) {
const_cast<RoomObject*>(this)->EnsureTilesLoaded();
}
if (tiles_.empty()) {
return absl::FailedPreconditionError("No tiles loaded for object");
}
return std::span<const gfx::Tile16>(tiles_.data(), tiles_.size());
}
absl::StatusOr<const gfx::Tile16*> RoomObject::GetTile(int index) const {
if (!tiles_loaded_) {
const_cast<RoomObject*>(this)->EnsureTilesLoaded();
}
if (index < 0 || index >= static_cast<int>(tiles_.size())) {
return absl::OutOfRangeError(
absl::StrFormat("Tile index %d out of range (0-%d)", index, tiles_.size() - 1));
}
return &tiles_[index];
}
int RoomObject::GetTileCount() const {
if (!tiles_loaded_) {
const_cast<RoomObject*>(this)->EnsureTilesLoaded();
}
return tile_count_;
}
} // namespace zelda3
} // namespace yaze

View File

@@ -8,7 +8,7 @@
#include "absl/strings/string_view.h"
#include "app/gfx/snes_tile.h"
#include "app/rom.h"
#include "app/zelda3/dungeon/object_renderer.h"
#include "app/zelda3/dungeon/object_parser.h"
namespace yaze {
namespace zelda3 {
@@ -69,15 +69,45 @@ class RoomObject {
ox_(x),
oy_(y),
width_(16),
height_(16) {}
height_(16),
rom_(nullptr) {}
void set_rom(Rom* rom) { rom_ = rom; }
auto rom() { return rom_; }
auto mutable_rom() { return rom_; }
// Ensures tiles_ is populated with a basic set based on ROM tables so we can
// preview/draw objects without needing full emulator execution.
void EnsureTilesLoaded();
// Load tiles using the new ObjectParser
absl::Status LoadTilesWithParser();
// Getter for tiles
const std::vector<gfx::Tile16>& tiles() const { return tiles_; }
std::vector<gfx::Tile16>& mutable_tiles() { return tiles_; }
// Get tile data through Arena system - returns references, not copies
absl::StatusOr<std::span<const gfx::Tile16>> GetTiles() const;
// Get individual tile by index - uses Arena lookup
absl::StatusOr<const gfx::Tile16*> GetTile(int index) const;
// Get tile count without loading all tiles
int GetTileCount() const;
void AddTiles(int nbr, int pos) {
// Reads nbr Tile16 entries from ROM object data starting at pos (8 bytes per Tile16)
for (int i = 0; i < nbr; i++) {
ASSIGN_OR_LOG_ERROR(auto tile, rom()->ReadTile16(pos + (i * 2)));
int tpos = pos + (i * 8);
auto rom_data = rom()->data();
if (tpos + 7 >= (int)rom()->size()) break;
uint16_t w0 = (uint16_t)(rom_data[tpos] | (rom_data[tpos + 1] << 8));
uint16_t w1 = (uint16_t)(rom_data[tpos + 2] | (rom_data[tpos + 3] << 8));
uint16_t w2 = (uint16_t)(rom_data[tpos + 4] | (rom_data[tpos + 5] << 8));
uint16_t w3 = (uint16_t)(rom_data[tpos + 6] | (rom_data[tpos + 7] << 8));
gfx::Tile16 tile(gfx::WordToTileInfo(w0), gfx::WordToTileInfo(w1),
gfx::WordToTileInfo(w2), gfx::WordToTileInfo(w3));
tiles_.push_back(tile);
}
}
@@ -104,6 +134,10 @@ class RoomObject {
uint8_t oy_;
uint8_t z_ = 0;
uint8_t previous_size_ = 0;
// Size nibble bits captured from object encoding (0..3 each) for heuristic
// orientation and sizing decisions.
uint8_t size_x_bits_ = 0;
uint8_t size_y_bits_ = 0;
int width_;
int height_;
@@ -113,7 +147,13 @@ class RoomObject {
std::string name_;
std::vector<uint8_t> preview_object_data_;
std::vector<gfx::Tile16> tiles_;
// Tile data storage - using Arena system for efficient memory management
// Instead of copying Tile16 vectors, we store references to Arena-managed data
mutable std::vector<gfx::Tile16> tiles_; // Fallback for compatibility
mutable bool tiles_loaded_ = false;
mutable int tile_count_ = 0;
mutable int tile_data_ptr_ = -1; // Pointer to tile data in ROM
LayerType layer_;
ObjectOption options_ = ObjectOption::Nothing;