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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
207
src/app/zelda3/dungeon/object_parser.cc
Normal file
207
src/app/zelda3/dungeon/object_parser.cc
Normal 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
|
||||
145
src/app/zelda3/dungeon/object_parser.h
Normal file
145
src/app/zelda3/dungeon/object_parser.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
212
src/app/zelda3/dungeon/room_layout.cc
Normal file
212
src/app/zelda3/dungeon/room_layout.cc
Normal 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
|
||||
116
src/app/zelda3/dungeon/room_layout.h
Normal file
116
src/app/zelda3/dungeon/room_layout.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user