feat: Enhance DungeonObjectEditor with visual feedback and object manipulation features, including drag-and-drop support and property panel integration
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#include "app/core/window.h"
|
||||
#include "app/gfx/arena.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
@@ -100,10 +101,8 @@ absl::Status DungeonObjectEditor::SaveRoom() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement actual room saving to ROM
|
||||
// This would involve writing the room data back to the ROM file
|
||||
|
||||
return absl::OkStatus();
|
||||
// Save room objects back to ROM (Phase 1, Task 1.3)
|
||||
return current_room_->SaveObjects();
|
||||
}
|
||||
|
||||
absl::Status DungeonObjectEditor::ClearRoom() {
|
||||
@@ -175,8 +174,11 @@ absl::Status DungeonObjectEditor::InsertObject(int x, int y, int object_type, in
|
||||
}
|
||||
}
|
||||
|
||||
// Add object to room
|
||||
current_room_->AddTileObject(new_object);
|
||||
// Add object to room using new method (Phase 3)
|
||||
auto add_status = current_room_->AddObject(new_object);
|
||||
if (!add_status.ok()) {
|
||||
return add_status;
|
||||
}
|
||||
|
||||
// Select the new object
|
||||
ClearSelection();
|
||||
@@ -213,8 +215,11 @@ absl::Status DungeonObjectEditor::DeleteObject(size_t object_index) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Remove object from room
|
||||
current_room_->RemoveTileObject(object_index);
|
||||
// Remove object from room using new method (Phase 3)
|
||||
auto remove_status = current_room_->RemoveObject(object_index);
|
||||
if (!remove_status.ok()) {
|
||||
return remove_status;
|
||||
}
|
||||
|
||||
// Update selection indices
|
||||
for (auto& selected_index : selection_state_.selected_objects) {
|
||||
@@ -531,26 +536,35 @@ absl::Status DungeonObjectEditor::HandleMouseDrag(int start_x, int start_y, int
|
||||
return absl::FailedPreconditionError("No room loaded");
|
||||
}
|
||||
|
||||
// Convert screen coordinates to room coordinates
|
||||
auto [start_room_x, start_room_y] = ScreenToRoomCoordinates(start_x, start_y);
|
||||
auto [current_room_x, current_room_y] = ScreenToRoomCoordinates(current_x, current_y);
|
||||
// Enable dragging if not already (Phase 4)
|
||||
if (!selection_state_.is_dragging && !selection_state_.selected_objects.empty()) {
|
||||
selection_state_.is_dragging = true;
|
||||
selection_state_.drag_start_x = start_x;
|
||||
selection_state_.drag_start_y = start_y;
|
||||
|
||||
// Create undo point before drag
|
||||
auto undo_status = CreateUndoPoint();
|
||||
if (!undo_status.ok()) {
|
||||
return undo_status;
|
||||
}
|
||||
}
|
||||
|
||||
if (editing_state_.current_mode == Mode::kSelect && !selection_state_.selected_objects.empty()) {
|
||||
// Move selected objects
|
||||
for (size_t object_index : selection_state_.selected_objects) {
|
||||
if (object_index < current_room_->GetTileObjectCount()) {
|
||||
auto& object = current_room_->GetTileObject(object_index);
|
||||
|
||||
// Calculate offset from start position
|
||||
int offset_x = current_room_x - start_room_x;
|
||||
int offset_y = current_room_y - start_room_y;
|
||||
|
||||
// Move object
|
||||
auto status = MoveObject(object_index, object.x_ + offset_x, object.y_ + offset_y);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
// Handle the drag operation (Phase 4)
|
||||
return HandleDragOperation(current_x, current_y);
|
||||
}
|
||||
|
||||
absl::Status DungeonObjectEditor::HandleMouseRelease(int x, int y) {
|
||||
if (current_room_ == nullptr) {
|
||||
return absl::FailedPreconditionError("No room loaded");
|
||||
}
|
||||
|
||||
// End dragging operation (Phase 4)
|
||||
if (selection_state_.is_dragging) {
|
||||
selection_state_.is_dragging = false;
|
||||
|
||||
// Notify callbacks about the final positions
|
||||
if (room_changed_callback_) {
|
||||
room_changed_callback_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +877,292 @@ void DungeonObjectEditor::ClearHistory() {
|
||||
redo_history_.clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Phase 4: Visual Feedback and GUI Methods
|
||||
// ============================================================================
|
||||
|
||||
// Helper for color blending
|
||||
static uint32_t BlendColors(uint32_t base, uint32_t tint) {
|
||||
uint8_t a_tint = (tint >> 24) & 0xFF;
|
||||
if (a_tint == 0) return base;
|
||||
|
||||
uint8_t r_base = (base >> 16) & 0xFF;
|
||||
uint8_t g_base = (base >> 8) & 0xFF;
|
||||
uint8_t b_base = base & 0xFF;
|
||||
|
||||
uint8_t r_tint = (tint >> 16) & 0xFF;
|
||||
uint8_t g_tint = (tint >> 8) & 0xFF;
|
||||
uint8_t b_tint = tint & 0xFF;
|
||||
|
||||
float alpha = a_tint / 255.0f;
|
||||
uint8_t r = r_base * (1.0f - alpha) + r_tint * alpha;
|
||||
uint8_t g = g_base * (1.0f - alpha) + g_tint * alpha;
|
||||
uint8_t b = b_base * (1.0f - alpha) + b_tint * alpha;
|
||||
|
||||
return 0xFF000000 | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
void DungeonObjectEditor::RenderSelectionHighlight(gfx::Bitmap& canvas) {
|
||||
if (!config_.show_selection_highlight || selection_state_.selected_objects.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw highlight rectangles around selected objects
|
||||
for (size_t obj_idx : selection_state_.selected_objects) {
|
||||
if (obj_idx >= current_room_->GetTileObjectCount()) continue;
|
||||
|
||||
const auto& obj = current_room_->GetTileObject(obj_idx);
|
||||
int x = obj.x() * 16;
|
||||
int y = obj.y() * 16;
|
||||
int w = 16 + (obj.size() * 4); // Approximate width
|
||||
int h = 16 + (obj.size() * 4); // Approximate height
|
||||
|
||||
// Draw yellow selection box (2px border) - using SetPixel
|
||||
uint8_t r = (config_.selection_color >> 16) & 0xFF;
|
||||
uint8_t g = (config_.selection_color >> 8) & 0xFF;
|
||||
uint8_t b = config_.selection_color & 0xFF;
|
||||
gfx::SnesColor sel_color(r, g, b);
|
||||
|
||||
for (int py = y; py < y + h; py++) {
|
||||
for (int px = x; px < x + w; px++) {
|
||||
if (px < canvas.width() && py < canvas.height() &&
|
||||
(px < x + 2 || px >= x + w - 2 || py < y + 2 || py >= y + h - 2)) {
|
||||
canvas.SetPixel(px, py, sel_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DungeonObjectEditor::RenderLayerVisualization(gfx::Bitmap& canvas) {
|
||||
if (!config_.show_layer_colors || !current_room_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply subtle color tints based on layer (simplified - just mark with colored border)
|
||||
for (const auto& obj : current_room_->GetTileObjects()) {
|
||||
int x = obj.x() * 16;
|
||||
int y = obj.y() * 16;
|
||||
int w = 16;
|
||||
int h = 16;
|
||||
|
||||
uint32_t tint_color = 0xFF000000;
|
||||
switch (obj.GetLayerValue()) {
|
||||
case 0: tint_color = config_.layer0_color; break;
|
||||
case 1: tint_color = config_.layer1_color; break;
|
||||
case 2: tint_color = config_.layer2_color; break;
|
||||
}
|
||||
|
||||
// Draw 1px border in layer color
|
||||
uint8_t r = (tint_color >> 16) & 0xFF;
|
||||
uint8_t g = (tint_color >> 8) & 0xFF;
|
||||
uint8_t b = tint_color & 0xFF;
|
||||
gfx::SnesColor layer_color(r, g, b);
|
||||
|
||||
for (int py = y; py < y + h && py < canvas.height(); py++) {
|
||||
for (int px = x; px < x + w && px < canvas.width(); px++) {
|
||||
if (px == x || px == x + w - 1 || py == y || py == y + h - 1) {
|
||||
canvas.SetPixel(px, py, layer_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DungeonObjectEditor::RenderObjectPropertyPanel() {
|
||||
if (!config_.show_property_panel || selection_state_.selected_objects.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Begin("Object Properties", &config_.show_property_panel);
|
||||
|
||||
if (selection_state_.selected_objects.size() == 1) {
|
||||
size_t obj_idx = selection_state_.selected_objects[0];
|
||||
if (obj_idx < current_room_->GetTileObjectCount()) {
|
||||
auto& obj = current_room_->GetTileObject(obj_idx);
|
||||
|
||||
ImGui::Text("Object #%zu", obj_idx);
|
||||
ImGui::Separator();
|
||||
|
||||
// ID (hex)
|
||||
int id = obj.id_;
|
||||
if (ImGui::InputInt("ID (0x)", &id, 1, 16, ImGuiInputTextFlags_CharsHexadecimal)) {
|
||||
if (id >= 0 && id <= 0xFFF) {
|
||||
obj.id_ = id;
|
||||
if (object_changed_callback_) {
|
||||
object_changed_callback_(obj_idx, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Position
|
||||
int x = obj.x();
|
||||
int y = obj.y();
|
||||
if (ImGui::InputInt("X Position", &x, 1, 4)) {
|
||||
if (x >= 0 && x < 64) {
|
||||
obj.set_x(x);
|
||||
if (object_changed_callback_) {
|
||||
object_changed_callback_(obj_idx, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ImGui::InputInt("Y Position", &y, 1, 4)) {
|
||||
if (y >= 0 && y < 64) {
|
||||
obj.set_y(y);
|
||||
if (object_changed_callback_) {
|
||||
object_changed_callback_(obj_idx, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size (for Type 1 objects only)
|
||||
if (obj.id_ < 0x100) {
|
||||
int size = obj.size();
|
||||
if (ImGui::SliderInt("Size", &size, 0, 15)) {
|
||||
obj.set_size(size);
|
||||
if (object_changed_callback_) {
|
||||
object_changed_callback_(obj_idx, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Layer
|
||||
int layer = obj.GetLayerValue();
|
||||
if (ImGui::Combo("Layer", &layer, "Layer 0\0Layer 1\0Layer 2\0")) {
|
||||
obj.layer_ = static_cast<RoomObject::LayerType>(layer);
|
||||
if (object_changed_callback_) {
|
||||
object_changed_callback_(obj_idx, obj);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Action buttons
|
||||
if (ImGui::Button("Delete Object")) {
|
||||
auto status = DeleteObject(obj_idx);
|
||||
(void)status; // Ignore return value for now
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Duplicate")) {
|
||||
RoomObject duplicate = obj;
|
||||
duplicate.set_x(obj.x() + 1);
|
||||
auto status = current_room_->AddObject(duplicate);
|
||||
(void)status; // Ignore return value for now
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Multiple objects selected
|
||||
ImGui::Text("%zu objects selected", selection_state_.selected_objects.size());
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("Delete All Selected")) {
|
||||
auto status = DeleteSelectedObjects();
|
||||
(void)status; // Ignore return value for now
|
||||
}
|
||||
|
||||
if (ImGui::Button("Clear Selection")) {
|
||||
auto status = ClearSelection();
|
||||
(void)status; // Ignore return value for now
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void DungeonObjectEditor::RenderLayerControls() {
|
||||
ImGui::Begin("Layer Controls");
|
||||
|
||||
// Current layer selection
|
||||
ImGui::Text("Current Layer:");
|
||||
ImGui::RadioButton("Layer 0", &editing_state_.current_layer, 0);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Layer 1", &editing_state_.current_layer, 1);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Layer 2", &editing_state_.current_layer, 2);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Layer visibility toggles
|
||||
static bool layer_visible[3] = {true, true, true};
|
||||
ImGui::Text("Layer Visibility:");
|
||||
ImGui::Checkbox("Show Layer 0", &layer_visible[0]);
|
||||
ImGui::Checkbox("Show Layer 1", &layer_visible[1]);
|
||||
ImGui::Checkbox("Show Layer 2", &layer_visible[2]);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Layer colors
|
||||
ImGui::Checkbox("Show Layer Colors", &config_.show_layer_colors);
|
||||
if (config_.show_layer_colors) {
|
||||
ImGui::ColorEdit4("Layer 0 Tint", (float*)&config_.layer0_color);
|
||||
ImGui::ColorEdit4("Layer 1 Tint", (float*)&config_.layer1_color);
|
||||
ImGui::ColorEdit4("Layer 2 Tint", (float*)&config_.layer2_color);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Object counts per layer
|
||||
if (current_room_) {
|
||||
int count0 = 0, count1 = 0, count2 = 0;
|
||||
for (const auto& obj : current_room_->GetTileObjects()) {
|
||||
switch (obj.GetLayerValue()) {
|
||||
case 0: count0++; break;
|
||||
case 1: count1++; break;
|
||||
case 2: count2++; break;
|
||||
}
|
||||
}
|
||||
ImGui::Text("Layer 0: %d objects", count0);
|
||||
ImGui::Text("Layer 1: %d objects", count1);
|
||||
ImGui::Text("Layer 2: %d objects", count2);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
absl::Status DungeonObjectEditor::HandleDragOperation(int current_x, int current_y) {
|
||||
if (!selection_state_.is_dragging || selection_state_.selected_objects.empty()) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Calculate delta from drag start
|
||||
int dx = current_x - selection_state_.drag_start_x;
|
||||
int dy = current_y - selection_state_.drag_start_y;
|
||||
|
||||
// Convert pixel delta to grid delta
|
||||
int grid_dx = dx / config_.grid_size;
|
||||
int grid_dy = dy / config_.grid_size;
|
||||
|
||||
if (grid_dx == 0 && grid_dy == 0) {
|
||||
return absl::OkStatus(); // No meaningful movement yet
|
||||
}
|
||||
|
||||
// Move all selected objects
|
||||
for (size_t obj_idx : selection_state_.selected_objects) {
|
||||
if (obj_idx >= current_room_->GetTileObjectCount()) continue;
|
||||
|
||||
auto& obj = current_room_->GetTileObject(obj_idx);
|
||||
int new_x = obj.x() + grid_dx;
|
||||
int new_y = obj.y() + grid_dy;
|
||||
|
||||
// Clamp to valid range
|
||||
new_x = std::max(0, std::min(63, new_x));
|
||||
new_y = std::max(0, std::min(63, new_y));
|
||||
|
||||
obj.set_x(new_x);
|
||||
obj.set_y(new_y);
|
||||
|
||||
if (object_changed_callback_) {
|
||||
object_changed_callback_(obj_idx, obj);
|
||||
}
|
||||
}
|
||||
|
||||
// Update drag start position
|
||||
selection_state_.drag_start_x = current_x;
|
||||
selection_state_.drag_start_y = current_y;
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<gfx::Bitmap> DungeonObjectEditor::RenderRoom() {
|
||||
if (current_room_ == nullptr) {
|
||||
return absl::FailedPreconditionError("No room loaded");
|
||||
|
||||
@@ -77,6 +77,15 @@ class DungeonObjectEditor {
|
||||
int auto_save_interval = 300; // 5 minutes
|
||||
bool validate_objects = true;
|
||||
bool show_collision_bounds = false;
|
||||
|
||||
// Phase 4: Visual feedback settings
|
||||
bool show_selection_highlight = true;
|
||||
bool show_layer_colors = true;
|
||||
bool show_property_panel = true;
|
||||
uint32_t selection_color = 0xFFFFFF00; // Yellow
|
||||
uint32_t layer0_color = 0xFFFF0000; // Red tint
|
||||
uint32_t layer1_color = 0xFF00FF00; // Green tint
|
||||
uint32_t layer2_color = 0xFF0000FF; // Blue tint
|
||||
};
|
||||
|
||||
// Undo/Redo system
|
||||
@@ -117,7 +126,7 @@ class DungeonObjectEditor {
|
||||
bool right_button, bool shift_pressed);
|
||||
absl::Status HandleMouseDrag(int start_x, int start_y, int current_x,
|
||||
int current_y);
|
||||
absl::Status HandleMouseRelease(int x, int y);
|
||||
absl::Status HandleMouseRelease(int x, int y); // Phase 4: End drag operations
|
||||
absl::Status HandleScrollWheel(int delta, int x, int y, bool ctrl_pressed);
|
||||
absl::Status HandleKeyPress(int key_code, bool ctrl_pressed,
|
||||
bool shift_pressed);
|
||||
@@ -145,6 +154,13 @@ class DungeonObjectEditor {
|
||||
absl::StatusOr<gfx::Bitmap> RenderPreview(int x, int y);
|
||||
void SetPreviewPosition(int x, int y);
|
||||
void UpdatePreview();
|
||||
|
||||
// Phase 4: Visual feedback and GUI
|
||||
void RenderSelectionHighlight(gfx::Bitmap& canvas);
|
||||
void RenderLayerVisualization(gfx::Bitmap& canvas);
|
||||
void RenderObjectPropertyPanel(); // ImGui panel
|
||||
void RenderLayerControls(); // ImGui controls
|
||||
absl::Status HandleDragOperation(int current_x, int current_y);
|
||||
|
||||
// Undo/Redo functionality
|
||||
absl::Status Undo();
|
||||
|
||||
@@ -481,37 +481,16 @@ void Room::ParseObjectsFromLocation(int objects_location) {
|
||||
}
|
||||
|
||||
if (!door) {
|
||||
// Parse object with enhanced validation
|
||||
if (b3 >= 0xF8) {
|
||||
oid = static_cast<short>((b3 << 4) |
|
||||
0x80 + (((b2 & 0x03) << 2) + ((b1 & 0x03))));
|
||||
posX = static_cast<uint8_t>((b1 & 0xFC) >> 2);
|
||||
posY = static_cast<uint8_t>((b2 & 0xFC) >> 2);
|
||||
sizeXY = static_cast<uint8_t>((((b1 & 0x03) << 2) + (b2 & 0x03)));
|
||||
} else {
|
||||
oid = b3;
|
||||
posX = static_cast<uint8_t>((b1 & 0xFC) >> 2);
|
||||
posY = static_cast<uint8_t>((b2 & 0xFC) >> 2);
|
||||
sizeX = static_cast<uint8_t>((b1 & 0x03));
|
||||
sizeY = static_cast<uint8_t>((b2 & 0x03));
|
||||
sizeXY = static_cast<uint8_t>(((sizeX << 2) + sizeY));
|
||||
}
|
||||
|
||||
if (b1 >= 0xFC) {
|
||||
oid = static_cast<short>((b3 & 0x3F) + 0x100);
|
||||
posX = static_cast<uint8_t>(((b2 & 0xF0) >> 4) + ((b1 & 0x3) << 4));
|
||||
posY = static_cast<uint8_t>(((b2 & 0x0F) << 2) + ((b3 & 0xC0) >> 6));
|
||||
sizeXY = 0;
|
||||
}
|
||||
|
||||
// Validate object ID before creating object
|
||||
if (oid >= 0 && oid <= 0x3FF) {
|
||||
RoomObject r(oid, posX, posY, sizeXY, static_cast<uint8_t>(layer));
|
||||
// Use the refactored encoding/decoding functions (Phase 1, Task 1.2)
|
||||
RoomObject r = RoomObject::DecodeObjectFromBytes(b1, b2, b3, static_cast<uint8_t>(layer));
|
||||
|
||||
// Validate object ID before adding to the room
|
||||
if (r.id_ >= 0 && r.id_ <= 0x3FF) {
|
||||
r.set_rom(rom_);
|
||||
tile_objects_.push_back(r);
|
||||
|
||||
// Handle special object types
|
||||
HandleSpecialObjects(oid, posX, posY, nbr_of_staircase);
|
||||
// Handle special object types (staircases, chests, etc.)
|
||||
HandleSpecialObjects(r.id_, r.x(), r.y(), nbr_of_staircase);
|
||||
}
|
||||
} else {
|
||||
// Handle door objects (placeholder for future implementation)
|
||||
@@ -521,6 +500,170 @@ void Room::ParseObjectsFromLocation(int objects_location) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Object Saving Implementation (Phase 1, Task 1.3)
|
||||
// ============================================================================
|
||||
|
||||
std::vector<uint8_t> Room::EncodeObjects() const {
|
||||
std::vector<uint8_t> bytes;
|
||||
|
||||
// Organize objects by layer
|
||||
std::vector<RoomObject> layer0_objects;
|
||||
std::vector<RoomObject> layer1_objects;
|
||||
std::vector<RoomObject> layer2_objects;
|
||||
|
||||
for (const auto& obj : tile_objects_) {
|
||||
switch (obj.GetLayerValue()) {
|
||||
case 0: layer0_objects.push_back(obj); break;
|
||||
case 1: layer1_objects.push_back(obj); break;
|
||||
case 2: layer2_objects.push_back(obj); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Encode Layer 1 (BG2)
|
||||
for (const auto& obj : layer0_objects) {
|
||||
auto encoded = obj.EncodeObjectToBytes();
|
||||
bytes.push_back(encoded.b1);
|
||||
bytes.push_back(encoded.b2);
|
||||
bytes.push_back(encoded.b3);
|
||||
}
|
||||
bytes.push_back(0xFF);
|
||||
bytes.push_back(0xFF);
|
||||
|
||||
// Encode Layer 2 (BG1)
|
||||
for (const auto& obj : layer1_objects) {
|
||||
auto encoded = obj.EncodeObjectToBytes();
|
||||
bytes.push_back(encoded.b1);
|
||||
bytes.push_back(encoded.b2);
|
||||
bytes.push_back(encoded.b3);
|
||||
}
|
||||
bytes.push_back(0xFF);
|
||||
bytes.push_back(0xFF);
|
||||
|
||||
// Encode Layer 3
|
||||
for (const auto& obj : layer2_objects) {
|
||||
auto encoded = obj.EncodeObjectToBytes();
|
||||
bytes.push_back(encoded.b1);
|
||||
bytes.push_back(encoded.b2);
|
||||
bytes.push_back(encoded.b3);
|
||||
}
|
||||
|
||||
// Final terminator
|
||||
bytes.push_back(0xFF);
|
||||
bytes.push_back(0xFF);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
absl::Status Room::SaveObjects() {
|
||||
if (rom_ == nullptr) {
|
||||
return absl::InvalidArgumentError("ROM pointer is null");
|
||||
}
|
||||
|
||||
auto rom_data = rom()->vector();
|
||||
|
||||
// Get object pointer
|
||||
int object_pointer = (rom_data[room_object_pointer + 2] << 16) +
|
||||
(rom_data[room_object_pointer + 1] << 8) +
|
||||
(rom_data[room_object_pointer]);
|
||||
object_pointer = SnesToPc(object_pointer);
|
||||
|
||||
if (object_pointer < 0 || object_pointer >= (int)rom_->size()) {
|
||||
return absl::OutOfRangeError("Object pointer out of range");
|
||||
}
|
||||
|
||||
int room_address = object_pointer + (room_id_ * 3);
|
||||
|
||||
if (room_address < 0 || room_address + 2 >= (int)rom_->size()) {
|
||||
return absl::OutOfRangeError("Room address out of range");
|
||||
}
|
||||
|
||||
int tile_address = (rom_data[room_address + 2] << 16) +
|
||||
(rom_data[room_address + 1] << 8) + rom_data[room_address];
|
||||
|
||||
int objects_location = SnesToPc(tile_address);
|
||||
|
||||
if (objects_location < 0 || objects_location >= (int)rom_->size()) {
|
||||
return absl::OutOfRangeError("Objects location out of range");
|
||||
}
|
||||
|
||||
// Skip graphics/layout header (2 bytes)
|
||||
int write_pos = objects_location + 2;
|
||||
|
||||
// Encode all objects
|
||||
auto encoded_bytes = EncodeObjects();
|
||||
|
||||
// Write encoded bytes to ROM using WriteVector
|
||||
return rom_->WriteVector(write_pos, encoded_bytes);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Object Manipulation Methods (Phase 3)
|
||||
// ============================================================================
|
||||
|
||||
absl::Status Room::AddObject(const RoomObject& object) {
|
||||
// Validate object
|
||||
if (!ValidateObject(object)) {
|
||||
return absl::InvalidArgumentError("Invalid object parameters");
|
||||
}
|
||||
|
||||
// Add to internal list
|
||||
tile_objects_.push_back(object);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status Room::RemoveObject(size_t index) {
|
||||
if (index >= tile_objects_.size()) {
|
||||
return absl::OutOfRangeError("Object index out of range");
|
||||
}
|
||||
|
||||
tile_objects_.erase(tile_objects_.begin() + index);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status Room::UpdateObject(size_t index, const RoomObject& object) {
|
||||
if (index >= tile_objects_.size()) {
|
||||
return absl::OutOfRangeError("Object index out of range");
|
||||
}
|
||||
|
||||
if (!ValidateObject(object)) {
|
||||
return absl::InvalidArgumentError("Invalid object parameters");
|
||||
}
|
||||
|
||||
tile_objects_[index] = object;
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<size_t> Room::FindObjectAt(int x, int y, int layer) const {
|
||||
for (size_t i = 0; i < tile_objects_.size(); i++) {
|
||||
const auto& obj = tile_objects_[i];
|
||||
if (obj.x() == x && obj.y() == y && obj.GetLayerValue() == layer) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return absl::NotFoundError("No object found at position");
|
||||
}
|
||||
|
||||
bool Room::ValidateObject(const RoomObject& object) const {
|
||||
// Validate position (0-63 for both X and Y)
|
||||
if (object.x() < 0 || object.x() > 63) return false;
|
||||
if (object.y() < 0 || object.y() > 63) return false;
|
||||
|
||||
// Validate layer (0-2)
|
||||
if (object.GetLayerValue() < 0 || object.GetLayerValue() > 2) return false;
|
||||
|
||||
// Validate object ID range
|
||||
if (object.id_ < 0 || object.id_ > 0xFFF) return false;
|
||||
|
||||
// Validate size for Type 1 objects
|
||||
if (object.id_ < 0x100 && object.size() > 15) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Room::HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY, int& nbr_of_staircase) {
|
||||
// Handle staircase objects
|
||||
for (short stair : stairsObjects) {
|
||||
|
||||
@@ -244,6 +244,13 @@ class Room {
|
||||
void AddTileObject(const RoomObject& object) {
|
||||
tile_objects_.push_back(object);
|
||||
}
|
||||
|
||||
// Enhanced object manipulation (Phase 3)
|
||||
absl::Status AddObject(const RoomObject& object);
|
||||
absl::Status RemoveObject(size_t index);
|
||||
absl::Status UpdateObject(size_t index, const RoomObject& object);
|
||||
absl::StatusOr<size_t> FindObjectAt(int x, int y, int layer) const;
|
||||
bool ValidateObject(const RoomObject& object) const;
|
||||
void RemoveTileObject(size_t index) {
|
||||
if (index < tile_objects_.size()) {
|
||||
tile_objects_.erase(tile_objects_.begin() + index);
|
||||
@@ -318,6 +325,10 @@ class Room {
|
||||
void ParseObjectsFromLocation(int objects_location);
|
||||
void HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY,
|
||||
int& nbr_of_staircase);
|
||||
|
||||
// Object saving (Phase 1, Task 1.3)
|
||||
absl::Status SaveObjects();
|
||||
std::vector<uint8_t> EncodeObjects() const;
|
||||
|
||||
auto blocks() const { return blocks_; }
|
||||
auto& mutable_blocks() { return blocks_; }
|
||||
|
||||
@@ -43,6 +43,8 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF")
|
||||
unit/zelda3/test_dungeon_objects.cc
|
||||
unit/zelda3/dungeon_component_unit_test.cc
|
||||
zelda3/dungeon/room_object_encoding_test.cc
|
||||
zelda3/dungeon/room_integration_test.cc
|
||||
zelda3/dungeon/room_manipulation_test.cc
|
||||
|
||||
# CLI Services (for catalog serialization tests)
|
||||
../src/cli/service/resources/resource_catalog.cc
|
||||
@@ -98,6 +100,8 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF")
|
||||
unit/zelda3/test_dungeon_objects.cc
|
||||
unit/zelda3/dungeon_component_unit_test.cc
|
||||
zelda3/dungeon/room_object_encoding_test.cc
|
||||
zelda3/dungeon/room_integration_test.cc
|
||||
zelda3/dungeon/room_manipulation_test.cc
|
||||
|
||||
# CLI Services (for catalog serialization tests)
|
||||
../src/cli/service/resources/resource_catalog.cc
|
||||
|
||||
327
test/zelda3/dungeon/room_integration_test.cc
Normal file
327
test/zelda3/dungeon/room_integration_test.cc
Normal file
@@ -0,0 +1,327 @@
|
||||
// Integration tests for Room object load/save cycle with real ROM data
|
||||
// Phase 1, Task 2.1: Full round-trip verification
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "app/zelda3/dungeon/room.h"
|
||||
#include "app/zelda3/dungeon/room_object.h"
|
||||
|
||||
// Helper function for SNES to PC address conversion
|
||||
inline int SnesToPc(int addr) {
|
||||
int temp = (addr & 0x7FFF) + ((addr / 2) & 0xFF8000);
|
||||
return (temp + 0x0);
|
||||
}
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
namespace test {
|
||||
|
||||
class RoomIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Load the ROM file
|
||||
rom_ = std::make_unique<Rom>();
|
||||
|
||||
// Check if ROM file exists
|
||||
const char* rom_path = std::getenv("YAZE_TEST_ROM_PATH");
|
||||
if (!rom_path) {
|
||||
rom_path = "zelda3.sfc";
|
||||
}
|
||||
|
||||
auto status = rom_->LoadFromFile(rom_path);
|
||||
if (!status.ok()) {
|
||||
GTEST_SKIP() << "ROM file not available: " << status.message();
|
||||
}
|
||||
|
||||
// Create backup of ROM data for restoration after tests
|
||||
original_rom_data_ = rom_->vector();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Restore original ROM data
|
||||
if (rom_ && !original_rom_data_.empty()) {
|
||||
for (size_t i = 0; i < original_rom_data_.size(); i++) {
|
||||
rom_->WriteByte(i, original_rom_data_[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::vector<uint8_t> original_rom_data_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Test 1: Basic Load/Save Round-Trip
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(RoomIntegrationTest, BasicLoadSaveRoundTrip) {
|
||||
// Load room 0 (Hyrule Castle Entrance)
|
||||
Room room1(0x00, rom_.get());
|
||||
|
||||
// Get original object count
|
||||
size_t original_count = room1.GetTileObjects().size();
|
||||
ASSERT_GT(original_count, 0) << "Room should have objects";
|
||||
|
||||
// Store original objects
|
||||
auto original_objects = room1.GetTileObjects();
|
||||
|
||||
// Save the room (should write same data back)
|
||||
auto save_status = room1.SaveObjects();
|
||||
ASSERT_TRUE(save_status.ok()) << save_status.message();
|
||||
|
||||
// Load the room again
|
||||
Room room2(0x00, rom_.get());
|
||||
|
||||
// Verify object count matches
|
||||
EXPECT_EQ(room2.GetTileObjects().size(), original_count);
|
||||
|
||||
// Verify each object matches
|
||||
auto reloaded_objects = room2.GetTileObjects();
|
||||
ASSERT_EQ(reloaded_objects.size(), original_objects.size());
|
||||
|
||||
for (size_t i = 0; i < original_objects.size(); i++) {
|
||||
SCOPED_TRACE("Object " + std::to_string(i));
|
||||
|
||||
const auto& orig = original_objects[i];
|
||||
const auto& reload = reloaded_objects[i];
|
||||
|
||||
EXPECT_EQ(reload.id_, orig.id_) << "ID mismatch";
|
||||
EXPECT_EQ(reload.x(), orig.x()) << "X position mismatch";
|
||||
EXPECT_EQ(reload.y(), orig.y()) << "Y position mismatch";
|
||||
EXPECT_EQ(reload.size(), orig.size()) << "Size mismatch";
|
||||
EXPECT_EQ(reload.GetLayerValue(), orig.GetLayerValue()) << "Layer mismatch";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 2: Multi-Room Verification
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(RoomIntegrationTest, MultiRoomLoadSaveRoundTrip) {
|
||||
// Test several different rooms to ensure broad coverage
|
||||
std::vector<int> test_rooms = {0x00, 0x01, 0x02, 0x10, 0x20};
|
||||
|
||||
for (int room_id : test_rooms) {
|
||||
SCOPED_TRACE("Room " + std::to_string(room_id));
|
||||
|
||||
// Load room
|
||||
Room room1(room_id, rom_.get());
|
||||
auto original_objects = room1.GetTileObjects();
|
||||
|
||||
if (original_objects.empty()) {
|
||||
continue; // Skip empty rooms
|
||||
}
|
||||
|
||||
// Save objects
|
||||
auto save_status = room1.SaveObjects();
|
||||
ASSERT_TRUE(save_status.ok()) << save_status.message();
|
||||
|
||||
// Reload and verify
|
||||
Room room2(room_id, rom_.get());
|
||||
auto reloaded_objects = room2.GetTileObjects();
|
||||
|
||||
EXPECT_EQ(reloaded_objects.size(), original_objects.size());
|
||||
|
||||
// Verify objects match
|
||||
for (size_t i = 0; i < std::min(original_objects.size(), reloaded_objects.size()); i++) {
|
||||
const auto& orig = original_objects[i];
|
||||
const auto& reload = reloaded_objects[i];
|
||||
|
||||
EXPECT_EQ(reload.id_, orig.id_);
|
||||
EXPECT_EQ(reload.x(), orig.x());
|
||||
EXPECT_EQ(reload.y(), orig.y());
|
||||
EXPECT_EQ(reload.size(), orig.size());
|
||||
EXPECT_EQ(reload.GetLayerValue(), orig.GetLayerValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 3: Layer Verification
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(RoomIntegrationTest, LayerPreservation) {
|
||||
// Load a room known to have multiple layers
|
||||
Room room(0x01, rom_.get());
|
||||
|
||||
auto objects = room.GetTileObjects();
|
||||
ASSERT_GT(objects.size(), 0);
|
||||
|
||||
// Count objects per layer
|
||||
int layer0_count = 0, layer1_count = 0, layer2_count = 0;
|
||||
for (const auto& obj : objects) {
|
||||
switch (obj.GetLayerValue()) {
|
||||
case 0: layer0_count++; break;
|
||||
case 1: layer1_count++; break;
|
||||
case 2: layer2_count++; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Save and reload
|
||||
ASSERT_TRUE(room.SaveObjects().ok());
|
||||
|
||||
Room room2(0x01, rom_.get());
|
||||
auto reloaded = room2.GetTileObjects();
|
||||
|
||||
// Verify layer counts match
|
||||
int reload_layer0 = 0, reload_layer1 = 0, reload_layer2 = 0;
|
||||
for (const auto& obj : reloaded) {
|
||||
switch (obj.GetLayerValue()) {
|
||||
case 0: reload_layer0++; break;
|
||||
case 1: reload_layer1++; break;
|
||||
case 2: reload_layer2++; break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(reload_layer0, layer0_count);
|
||||
EXPECT_EQ(reload_layer1, layer1_count);
|
||||
EXPECT_EQ(reload_layer2, layer2_count);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 4: Object Type Distribution
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(RoomIntegrationTest, ObjectTypeDistribution) {
|
||||
Room room(0x00, rom_.get());
|
||||
|
||||
auto objects = room.GetTileObjects();
|
||||
ASSERT_GT(objects.size(), 0);
|
||||
|
||||
// Count object types
|
||||
int type1_count = 0; // ID < 0x100
|
||||
int type2_count = 0; // ID 0x100-0x13F
|
||||
int type3_count = 0; // ID >= 0xF00
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
if (obj.id_ >= 0xF00) {
|
||||
type3_count++;
|
||||
} else if (obj.id_ >= 0x100) {
|
||||
type2_count++;
|
||||
} else {
|
||||
type1_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Save and reload
|
||||
ASSERT_TRUE(room.SaveObjects().ok());
|
||||
|
||||
Room room2(0x00, rom_.get());
|
||||
auto reloaded = room2.GetTileObjects();
|
||||
|
||||
// Verify type distribution matches
|
||||
int reload_type1 = 0, reload_type2 = 0, reload_type3 = 0;
|
||||
for (const auto& obj : reloaded) {
|
||||
if (obj.id_ >= 0xF00) {
|
||||
reload_type3++;
|
||||
} else if (obj.id_ >= 0x100) {
|
||||
reload_type2++;
|
||||
} else {
|
||||
reload_type1++;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(reload_type1, type1_count);
|
||||
EXPECT_EQ(reload_type2, type2_count);
|
||||
EXPECT_EQ(reload_type3, type3_count);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 5: Binary Data Verification
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(RoomIntegrationTest, BinaryDataExactMatch) {
|
||||
// This test verifies that saving doesn't change ROM data
|
||||
// when no modifications are made
|
||||
|
||||
Room room(0x02, rom_.get());
|
||||
|
||||
// Get the ROM location where objects are stored
|
||||
auto rom_data = rom_->vector();
|
||||
int object_pointer = (rom_data[0x874C + 2] << 16) +
|
||||
(rom_data[0x874C + 1] << 8) +
|
||||
(rom_data[0x874C]);
|
||||
object_pointer = SnesToPc(object_pointer);
|
||||
|
||||
int room_address = object_pointer + (0x02 * 3);
|
||||
int tile_address = (rom_data[room_address + 2] << 16) +
|
||||
(rom_data[room_address + 1] << 8) +
|
||||
rom_data[room_address];
|
||||
int objects_location = SnesToPc(tile_address);
|
||||
|
||||
// Read original bytes (up to 500 bytes should cover most rooms)
|
||||
std::vector<uint8_t> original_bytes;
|
||||
for (int i = 0; i < 500 && objects_location + i < (int)rom_data.size(); i++) {
|
||||
original_bytes.push_back(rom_data[objects_location + i]);
|
||||
// Stop at final terminator
|
||||
if (i > 0 && original_bytes[i] == 0xFF && original_bytes[i-1] == 0xFF) {
|
||||
// Check if this is the final terminator (3rd layer end)
|
||||
bool might_be_final = true;
|
||||
for (int j = i - 10; j < i - 1; j += 2) {
|
||||
if (j >= 0 && original_bytes[j] == 0xFF && original_bytes[j+1] == 0xFF) {
|
||||
// Found another FF FF marker, keep going
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (might_be_final) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Save objects (should write identical data)
|
||||
ASSERT_TRUE(room.SaveObjects().ok());
|
||||
|
||||
// Read bytes after save
|
||||
rom_data = rom_->vector();
|
||||
std::vector<uint8_t> saved_bytes;
|
||||
for (size_t i = 0; i < original_bytes.size() && objects_location + i < rom_data.size(); i++) {
|
||||
saved_bytes.push_back(rom_data[objects_location + i]);
|
||||
}
|
||||
|
||||
// Verify binary match
|
||||
ASSERT_EQ(saved_bytes.size(), original_bytes.size());
|
||||
for (size_t i = 0; i < original_bytes.size(); i++) {
|
||||
EXPECT_EQ(saved_bytes[i], original_bytes[i])
|
||||
<< "Byte mismatch at offset " << i;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test 6: Known Room Data Verification
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(RoomIntegrationTest, KnownRoomData) {
|
||||
// Room 0x00 (Hyrule Castle Entrance) - verify known objects exist
|
||||
Room room(0x00, rom_.get());
|
||||
|
||||
auto objects = room.GetTileObjects();
|
||||
ASSERT_GT(objects.size(), 0) << "Room 0x00 should have objects";
|
||||
|
||||
// Verify we can find common object types
|
||||
bool found_type1 = false;
|
||||
bool found_layer0 = false;
|
||||
bool found_layer1 = false;
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
if (obj.id_ < 0x100) found_type1 = true;
|
||||
if (obj.GetLayerValue() == 0) found_layer0 = true;
|
||||
if (obj.GetLayerValue() == 1) found_layer1 = true;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(found_type1) << "Should have Type 1 objects";
|
||||
EXPECT_TRUE(found_layer0) << "Should have Layer 0 objects";
|
||||
|
||||
// Verify coordinates are in valid range (0-63)
|
||||
for (const auto& obj : objects) {
|
||||
EXPECT_GE(obj.x(), 0);
|
||||
EXPECT_LE(obj.x(), 63);
|
||||
EXPECT_GE(obj.y(), 0);
|
||||
EXPECT_LE(obj.y(), 63);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
169
test/zelda3/dungeon/room_manipulation_test.cc
Normal file
169
test/zelda3/dungeon/room_manipulation_test.cc
Normal file
@@ -0,0 +1,169 @@
|
||||
// Tests for Room object manipulation methods (Phase 3)
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "app/rom.h"
|
||||
#include "app/zelda3/dungeon/room.h"
|
||||
#include "app/zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
namespace test {
|
||||
|
||||
class RoomManipulationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
rom_ = std::make_unique<Rom>();
|
||||
// Create a minimal ROM for testing
|
||||
std::vector<uint8_t> dummy_data(0x200000, 0);
|
||||
rom_->LoadFromData(dummy_data, false);
|
||||
|
||||
room_ = std::make_unique<Room>(0, rom_.get());
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<Room> room_;
|
||||
};
|
||||
|
||||
TEST_F(RoomManipulationTest, AddObject) {
|
||||
RoomObject obj(0x10, 10, 20, 3, 0);
|
||||
|
||||
auto status = room_->AddObject(obj);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects.size(), 1);
|
||||
EXPECT_EQ(objects[0].id_, 0x10);
|
||||
EXPECT_EQ(objects[0].x(), 10);
|
||||
EXPECT_EQ(objects[0].y(), 20);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, AddInvalidObject) {
|
||||
// Invalid X position (> 63)
|
||||
RoomObject obj(0x10, 100, 20, 3, 0);
|
||||
|
||||
auto status = room_->AddObject(obj);
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, RemoveObject) {
|
||||
RoomObject obj1(0x10, 10, 20, 3, 0);
|
||||
RoomObject obj2(0x20, 15, 25, 2, 1);
|
||||
|
||||
room_->AddObject(obj1);
|
||||
room_->AddObject(obj2);
|
||||
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 2);
|
||||
|
||||
auto status = room_->RemoveObject(0);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects.size(), 1);
|
||||
EXPECT_EQ(objects[0].id_, 0x20);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, RemoveInvalidIndex) {
|
||||
auto status = room_->RemoveObject(0);
|
||||
EXPECT_FALSE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, UpdateObject) {
|
||||
RoomObject obj(0x10, 10, 20, 3, 0);
|
||||
room_->AddObject(obj);
|
||||
|
||||
RoomObject updated(0x20, 15, 25, 5, 1);
|
||||
auto status = room_->UpdateObject(0, updated);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects[0].id_, 0x20);
|
||||
EXPECT_EQ(objects[0].x(), 15);
|
||||
EXPECT_EQ(objects[0].y(), 25);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, FindObjectAt) {
|
||||
RoomObject obj1(0x10, 10, 20, 3, 0);
|
||||
RoomObject obj2(0x20, 15, 25, 2, 1);
|
||||
|
||||
room_->AddObject(obj1);
|
||||
room_->AddObject(obj2);
|
||||
|
||||
auto result = room_->FindObjectAt(15, 25, 1);
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_EQ(result.value(), 1);
|
||||
|
||||
auto not_found = room_->FindObjectAt(99, 99, 0);
|
||||
EXPECT_FALSE(not_found.ok());
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, ValidateObject) {
|
||||
// Valid Type 1 object
|
||||
RoomObject valid1(0x10, 10, 20, 3, 0);
|
||||
EXPECT_TRUE(room_->ValidateObject(valid1));
|
||||
|
||||
// Valid Type 2 object
|
||||
RoomObject valid2(0x110, 30, 40, 0, 1);
|
||||
EXPECT_TRUE(room_->ValidateObject(valid2));
|
||||
|
||||
// Invalid X (> 63)
|
||||
RoomObject invalid_x(0x10, 100, 20, 3, 0);
|
||||
EXPECT_FALSE(room_->ValidateObject(invalid_x));
|
||||
|
||||
// Invalid layer (> 2)
|
||||
RoomObject invalid_layer(0x10, 10, 20, 3, 5);
|
||||
EXPECT_FALSE(room_->ValidateObject(invalid_layer));
|
||||
|
||||
// Invalid size for Type 1 (> 15)
|
||||
RoomObject invalid_size(0x10, 10, 20, 20, 0);
|
||||
EXPECT_FALSE(room_->ValidateObject(invalid_size));
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, MultipleOperations) {
|
||||
// Add several objects
|
||||
for (int i = 0; i < 5; i++) {
|
||||
RoomObject obj(0x10 + i, i * 5, i * 6, i, 0);
|
||||
ASSERT_TRUE(room_->AddObject(obj).ok());
|
||||
}
|
||||
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 5);
|
||||
|
||||
// Update middle object
|
||||
RoomObject updated(0x99, 30, 35, 7, 1);
|
||||
ASSERT_TRUE(room_->UpdateObject(2, updated).ok());
|
||||
|
||||
// Verify update
|
||||
auto objects = room_->GetTileObjects();
|
||||
EXPECT_EQ(objects[2].id_, 0x99);
|
||||
|
||||
// Remove first object
|
||||
ASSERT_TRUE(room_->RemoveObject(0).ok());
|
||||
EXPECT_EQ(room_->GetTileObjects().size(), 4);
|
||||
|
||||
// Verify first object is now what was second
|
||||
EXPECT_EQ(room_->GetTileObjects()[0].id_, 0x11);
|
||||
}
|
||||
|
||||
TEST_F(RoomManipulationTest, LayerOrganization) {
|
||||
// Add objects to different layers
|
||||
RoomObject layer0_obj(0x10, 10, 10, 2, 0);
|
||||
RoomObject layer1_obj(0x20, 20, 20, 3, 1);
|
||||
RoomObject layer2_obj(0x30, 30, 30, 4, 2);
|
||||
|
||||
room_->AddObject(layer0_obj);
|
||||
room_->AddObject(layer1_obj);
|
||||
room_->AddObject(layer2_obj);
|
||||
|
||||
// Verify can find by layer
|
||||
EXPECT_TRUE(room_->FindObjectAt(10, 10, 0).ok());
|
||||
EXPECT_TRUE(room_->FindObjectAt(20, 20, 1).ok());
|
||||
EXPECT_TRUE(room_->FindObjectAt(30, 30, 2).ok());
|
||||
|
||||
// Wrong layer should not find
|
||||
EXPECT_FALSE(room_->FindObjectAt(10, 10, 1).ok());
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
|
||||
Reference in New Issue
Block a user