backend-infra-engineer: Release v0.3.2 snapshot
This commit is contained in:
334
test/integration/ai/ai_gui_controller_test.cc
Normal file
334
test/integration/ai/ai_gui_controller_test.cc
Normal file
@@ -0,0 +1,334 @@
|
||||
// Integration tests for AIGUIController
|
||||
// Tests the gRPC GUI automation with vision feedback
|
||||
|
||||
#include "cli/service/ai/ai_gui_controller.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "cli/service/ai/gemini_ai_service.h"
|
||||
#include "cli/service/gui/gui_automation_client.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace ai {
|
||||
namespace {
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::Return;
|
||||
|
||||
// Mock GuiAutomationClient for testing without actual GUI
|
||||
class MockGuiAutomationClient : public GuiAutomationClient {
|
||||
public:
|
||||
MockGuiAutomationClient() : GuiAutomationClient("localhost:50052") {}
|
||||
|
||||
MOCK_METHOD(absl::Status, Connect, ());
|
||||
MOCK_METHOD(absl::StatusOr<AutomationResult>, Ping, (const std::string&));
|
||||
MOCK_METHOD(absl::StatusOr<AutomationResult>, Click,
|
||||
(const std::string&, ClickType));
|
||||
MOCK_METHOD(absl::StatusOr<AutomationResult>, Type,
|
||||
(const std::string&, const std::string&, bool));
|
||||
MOCK_METHOD(absl::StatusOr<AutomationResult>, Wait,
|
||||
(const std::string&, int, int));
|
||||
MOCK_METHOD(absl::StatusOr<AutomationResult>, Assert,
|
||||
(const std::string&));
|
||||
};
|
||||
|
||||
class AIGUIControllerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create mock services
|
||||
GeminiConfig config;
|
||||
config.api_key = "test_key";
|
||||
config.model = "gemini-2.5-flash";
|
||||
gemini_service_ = std::make_unique<GeminiAIService>(config);
|
||||
|
||||
gui_client_ = std::make_unique<MockGuiAutomationClient>();
|
||||
|
||||
controller_ = std::make_unique<AIGUIController>(
|
||||
gemini_service_.get(), gui_client_.get());
|
||||
|
||||
ControlLoopConfig loop_config;
|
||||
loop_config.max_iterations = 5;
|
||||
loop_config.enable_vision_verification = false; // Disable for unit tests
|
||||
loop_config.enable_iterative_refinement = false;
|
||||
controller_->Initialize(loop_config);
|
||||
}
|
||||
|
||||
std::unique_ptr<GeminiAIService> gemini_service_;
|
||||
std::unique_ptr<MockGuiAutomationClient> gui_client_;
|
||||
std::unique_ptr<AIGUIController> controller_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Basic Action Execution Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteClickAction_Success) {
|
||||
AIAction action(AIActionType::kClickButton);
|
||||
action.parameters["target"] = "button:Test";
|
||||
action.parameters["click_type"] = "left";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = true;
|
||||
result.message = "Click successful";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Click("button:Test", ClickType::kLeft))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
ASSERT_TRUE(status.ok()) << status.status().message();
|
||||
EXPECT_TRUE(status->action_successful);
|
||||
}
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteClickAction_Failure) {
|
||||
AIAction action(AIActionType::kClickButton);
|
||||
action.parameters["target"] = "button:NonExistent";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = false;
|
||||
result.message = "Button not found";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Click("button:NonExistent", ClickType::kLeft))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_THAT(status.status().message(),
|
||||
::testing::HasSubstr("Click action failed"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type Action Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteTypeAction_Success) {
|
||||
AIAction action(AIActionType::kSelectTile); // Using SelectTile as a type action
|
||||
action.parameters["target"] = "input:TileID";
|
||||
action.parameters["text"] = "0x42";
|
||||
action.parameters["clear_first"] = "true";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = true;
|
||||
result.message = "Text entered";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Type("input:TileID", "0x42", true))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
ASSERT_TRUE(status.ok());
|
||||
EXPECT_TRUE(status->action_successful);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wait Action Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteWaitAction_Success) {
|
||||
AIAction action(AIActionType::kWait);
|
||||
action.parameters["condition"] = "window:OverworldEditor";
|
||||
action.parameters["timeout_ms"] = "2000";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = true;
|
||||
result.message = "Condition met";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Wait("window:OverworldEditor", 2000, 100))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
ASSERT_TRUE(status.ok());
|
||||
EXPECT_TRUE(status->action_successful);
|
||||
}
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteWaitAction_Timeout) {
|
||||
AIAction action(AIActionType::kWait);
|
||||
action.parameters["condition"] = "window:NonExistentWindow";
|
||||
action.parameters["timeout_ms"] = "100";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = false;
|
||||
result.message = "Timeout waiting for condition";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Wait("window:NonExistentWindow", 100, 100))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Verify/Assert Action Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteVerifyAction_Success) {
|
||||
AIAction action(AIActionType::kVerifyTile);
|
||||
action.parameters["condition"] = "tile_placed";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = true;
|
||||
result.message = "Assertion passed";
|
||||
result.expected_value = "0x42";
|
||||
result.actual_value = "0x42";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Assert("tile_placed"))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
ASSERT_TRUE(status.ok());
|
||||
EXPECT_TRUE(status->action_successful);
|
||||
}
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteVerifyAction_Failure) {
|
||||
AIAction action(AIActionType::kVerifyTile);
|
||||
action.parameters["condition"] = "tile_placed";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = false;
|
||||
result.message = "Assertion failed";
|
||||
result.expected_value = "0x42";
|
||||
result.actual_value = "0x00";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Assert("tile_placed"))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_THAT(status.status().message(),
|
||||
::testing::HasSubstr("Assert action failed"));
|
||||
EXPECT_THAT(status.status().message(),
|
||||
::testing::HasSubstr("expected: 0x42"));
|
||||
EXPECT_THAT(status.status().message(),
|
||||
::testing::HasSubstr("actual: 0x00"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Complex Tile Placement Action Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecutePlaceTileAction_CompleteFlow) {
|
||||
AIAction action(AIActionType::kPlaceTile);
|
||||
action.parameters["map_id"] = "5";
|
||||
action.parameters["x"] = "10";
|
||||
action.parameters["y"] = "20";
|
||||
action.parameters["tile"] = "0x42";
|
||||
|
||||
AutomationResult result;
|
||||
result.success = true;
|
||||
|
||||
// Expect sequence: open menu, wait for window, set map ID, click position
|
||||
testing::InSequence seq;
|
||||
|
||||
EXPECT_CALL(*gui_client_, Click("menu:Overworld", ClickType::kLeft))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
EXPECT_CALL(*gui_client_, Wait("window:Overworld Editor", 2000, 100))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
EXPECT_CALL(*gui_client_, Type("input:Map ID", "5", true))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
EXPECT_CALL(*gui_client_, Click(::testing::_, ClickType::kLeft))
|
||||
.WillOnce(Return(result));
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
ASSERT_TRUE(status.ok()) << status.status().message();
|
||||
EXPECT_TRUE(status->action_successful);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Multiple Actions Execution Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteActions_MultipleActionsSuccess) {
|
||||
std::vector<AIAction> actions;
|
||||
|
||||
AIAction action1(AIActionType::kClickButton);
|
||||
action1.parameters["target"] = "button:Overworld";
|
||||
actions.push_back(action1);
|
||||
|
||||
AIAction action2(AIActionType::kWait);
|
||||
action2.parameters["condition"] = "window:OverworldEditor";
|
||||
actions.push_back(action2);
|
||||
|
||||
AutomationResult success_result;
|
||||
success_result.success = true;
|
||||
|
||||
EXPECT_CALL(*gui_client_, Click("button:Overworld", ClickType::kLeft))
|
||||
.WillOnce(Return(success_result));
|
||||
|
||||
EXPECT_CALL(*gui_client_, Wait("window:OverworldEditor", 5000, 100))
|
||||
.WillOnce(Return(success_result));
|
||||
|
||||
auto result = controller_->ExecuteActions(actions);
|
||||
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
EXPECT_TRUE(result->success);
|
||||
EXPECT_EQ(result->actions_executed.size(), 2);
|
||||
}
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteActions_StopsOnFirstFailure) {
|
||||
std::vector<AIAction> actions;
|
||||
|
||||
AIAction action1(AIActionType::kClickButton);
|
||||
action1.parameters["target"] = "button:Test";
|
||||
actions.push_back(action1);
|
||||
|
||||
AIAction action2(AIActionType::kClickButton);
|
||||
action2.parameters["target"] = "button:NeverReached";
|
||||
actions.push_back(action2);
|
||||
|
||||
AutomationResult failure_result;
|
||||
failure_result.success = false;
|
||||
failure_result.message = "First action failed";
|
||||
|
||||
EXPECT_CALL(*gui_client_, Click("button:Test", ClickType::kLeft))
|
||||
.WillOnce(Return(failure_result));
|
||||
|
||||
// Second action should never be called
|
||||
EXPECT_CALL(*gui_client_, Click("button:NeverReached", _))
|
||||
.Times(0);
|
||||
|
||||
auto result = controller_->ExecuteActions(actions);
|
||||
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result->actions_executed.size(), 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Error Handling Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecuteAction_InvalidActionType) {
|
||||
AIAction action(AIActionType::kInvalidAction);
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_THAT(status.status().message(),
|
||||
::testing::HasSubstr("Action type not implemented"));
|
||||
}
|
||||
|
||||
TEST_F(AIGUIControllerTest, ExecutePlaceTileAction_MissingParameters) {
|
||||
AIAction action(AIActionType::kPlaceTile);
|
||||
// Missing required parameters
|
||||
|
||||
auto status = controller_->ExecuteSingleAction(action, false);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_THAT(status.status().message(),
|
||||
::testing::HasSubstr("requires map_id, x, y, and tile"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace ai
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
211
test/integration/ai/test_ai_tile_placement.cc
Normal file
211
test/integration/ai/test_ai_tile_placement.cc
Normal file
@@ -0,0 +1,211 @@
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "cli/service/ai/ai_action_parser.h"
|
||||
#include "cli/service/ai/vision_action_refiner.h"
|
||||
#include "cli/service/ai/ai_gui_controller.h"
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
#include "cli/service/gui/gui_automation_client.h"
|
||||
#include "cli/service/ai/gemini_ai_service.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Integration tests for AI-controlled tile placement
|
||||
*
|
||||
* These tests verify the complete pipeline:
|
||||
* 1. Parse natural language commands
|
||||
* 2. Execute actions via gRPC
|
||||
* 3. Verify success with vision analysis
|
||||
* 4. Refine and retry on failure
|
||||
*/
|
||||
class AITilePlacementTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// These tests require YAZE GUI to be running with gRPC test harness
|
||||
// Skip if not available
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AITilePlacementTest, ParsePlaceTileCommand) {
|
||||
using namespace cli::ai;
|
||||
|
||||
// Test basic tile placement command
|
||||
auto result = AIActionParser::ParseCommand(
|
||||
"Place tile 0x42 at overworld position (5, 7)");
|
||||
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
EXPECT_EQ(result->size(), 3); // Select, Place, Save
|
||||
|
||||
// Check first action (Select)
|
||||
EXPECT_EQ(result->at(0).type, AIActionType::kSelectTile);
|
||||
EXPECT_EQ(result->at(0).parameters.at("tile_id"), "66"); // 0x42 = 66
|
||||
|
||||
// Check second action (Place)
|
||||
EXPECT_EQ(result->at(1).type, AIActionType::kPlaceTile);
|
||||
EXPECT_EQ(result->at(1).parameters.at("x"), "5");
|
||||
EXPECT_EQ(result->at(1).parameters.at("y"), "7");
|
||||
EXPECT_EQ(result->at(1).parameters.at("map_id"), "0");
|
||||
|
||||
// Check third action (Save)
|
||||
EXPECT_EQ(result->at(2).type, AIActionType::kSaveTile);
|
||||
}
|
||||
|
||||
TEST_F(AITilePlacementTest, ParseSelectTileCommand) {
|
||||
using namespace cli::ai;
|
||||
|
||||
auto result = AIActionParser::ParseCommand("Select tile 100");
|
||||
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_EQ(result->size(), 1);
|
||||
EXPECT_EQ(result->at(0).type, AIActionType::kSelectTile);
|
||||
EXPECT_EQ(result->at(0).parameters.at("tile_id"), "100");
|
||||
}
|
||||
|
||||
TEST_F(AITilePlacementTest, ParseOpenEditorCommand) {
|
||||
using namespace cli::ai;
|
||||
|
||||
auto result = AIActionParser::ParseCommand("Open the overworld editor");
|
||||
|
||||
ASSERT_TRUE(result.ok());
|
||||
EXPECT_EQ(result->size(), 1);
|
||||
EXPECT_EQ(result->at(0).type, AIActionType::kOpenEditor);
|
||||
EXPECT_EQ(result->at(0).parameters.at("editor"), "overworld");
|
||||
}
|
||||
|
||||
TEST_F(AITilePlacementTest, ActionToStringRoundtrip) {
|
||||
using namespace cli::ai;
|
||||
|
||||
AIAction action(AIActionType::kPlaceTile, {
|
||||
{"x", "5"},
|
||||
{"y", "7"},
|
||||
{"tile_id", "42"}
|
||||
});
|
||||
|
||||
std::string str = AIActionParser::ActionToString(action);
|
||||
EXPECT_FALSE(str.empty());
|
||||
EXPECT_TRUE(str.find("5") != std::string::npos);
|
||||
EXPECT_TRUE(str.find("7") != std::string::npos);
|
||||
}
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
TEST_F(AITilePlacementTest, DISABLED_VisionAnalysisBasic) {
|
||||
// This test requires Gemini API key
|
||||
const char* api_key = std::getenv("GEMINI_API_KEY");
|
||||
if (!api_key || std::string(api_key).empty()) {
|
||||
GTEST_SKIP() << "GEMINI_API_KEY not set";
|
||||
}
|
||||
|
||||
cli::GeminiConfig config;
|
||||
config.api_key = api_key;
|
||||
config.model = "gemini-2.5-flash";
|
||||
|
||||
cli::GeminiAIService gemini_service(config);
|
||||
cli::ai::VisionActionRefiner refiner(&gemini_service);
|
||||
|
||||
// Would need actual screenshots for real test
|
||||
// This is a structure test
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
TEST_F(AITilePlacementTest, DISABLED_FullAIControlLoop) {
|
||||
// This test requires:
|
||||
// 1. YAZE GUI running with gRPC test harness
|
||||
// 2. Gemini API key for vision
|
||||
// 3. Test ROM loaded
|
||||
|
||||
const char* api_key = std::getenv("GEMINI_API_KEY");
|
||||
if (!api_key || std::string(api_key).empty()) {
|
||||
GTEST_SKIP() << "GEMINI_API_KEY not set";
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
cli::GeminiConfig gemini_config;
|
||||
gemini_config.api_key = api_key;
|
||||
cli::GeminiAIService gemini_service(gemini_config);
|
||||
|
||||
cli::GuiAutomationClient gui_client("localhost:50051");
|
||||
auto connect_status = gui_client.Connect();
|
||||
if (!connect_status.ok()) {
|
||||
GTEST_SKIP() << "GUI test harness not available: "
|
||||
<< connect_status.message();
|
||||
}
|
||||
|
||||
// Create AI controller
|
||||
cli::ai::AIGUIController controller(&gemini_service, &gui_client);
|
||||
cli::ai::ControlLoopConfig config;
|
||||
config.max_iterations = 5;
|
||||
config.enable_vision_verification = true;
|
||||
controller.Initialize(config);
|
||||
|
||||
// Execute command
|
||||
auto result = controller.ExecuteCommand(
|
||||
"Place tile 0x42 at overworld position (5, 7)");
|
||||
|
||||
if (result.ok()) {
|
||||
EXPECT_TRUE(result->success);
|
||||
EXPECT_GT(result->iterations_performed, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
|
||||
TEST_F(AITilePlacementTest, ActionRefinement) {
|
||||
using namespace cli::ai;
|
||||
|
||||
// Test refinement logic with a failed action
|
||||
VisionAnalysisResult analysis;
|
||||
analysis.action_successful = false;
|
||||
analysis.error_message = "Element not found";
|
||||
|
||||
AIAction original_action(AIActionType::kClickButton, {
|
||||
{"button", "save"}
|
||||
});
|
||||
|
||||
// Would need VisionActionRefiner for real test
|
||||
// This verifies the structure compiles
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
TEST_F(AITilePlacementTest, MultipleCommandsParsing) {
|
||||
using namespace cli::ai;
|
||||
|
||||
// Test that we can parse multiple commands in sequence
|
||||
std::vector<std::string> commands = {
|
||||
"Open overworld editor",
|
||||
"Select tile 0x42",
|
||||
"Place tile at position (5, 7)",
|
||||
"Save changes"
|
||||
};
|
||||
|
||||
for (const auto& cmd : commands) {
|
||||
auto result = AIActionParser::ParseCommand(cmd);
|
||||
// At least some should parse successfully
|
||||
if (result.ok()) {
|
||||
EXPECT_FALSE(result->empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AITilePlacementTest, HexAndDecimalParsing) {
|
||||
using namespace cli::ai;
|
||||
|
||||
// Test hex notation
|
||||
auto hex_result = AIActionParser::ParseCommand("Select tile 0xFF");
|
||||
if (hex_result.ok() && !hex_result->empty()) {
|
||||
EXPECT_EQ(hex_result->at(0).parameters.at("tile_id"), "255");
|
||||
}
|
||||
|
||||
// Test decimal notation
|
||||
auto dec_result = AIActionParser::ParseCommand("Select tile 255");
|
||||
if (dec_result.ok() && !dec_result->empty()) {
|
||||
EXPECT_EQ(dec_result->at(0).parameters.at("tile_id"), "255");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
243
test/integration/ai/test_gemini_vision.cc
Normal file
243
test/integration/ai/test_gemini_vision.cc
Normal file
@@ -0,0 +1,243 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "cli/service/ai/gemini_ai_service.h"
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
#include "app/service/screenshot_utils.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
class GeminiVisionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Check if GEMINI_API_KEY is set
|
||||
const char* api_key = std::getenv("GEMINI_API_KEY");
|
||||
if (!api_key || std::string(api_key).empty()) {
|
||||
GTEST_SKIP() << "GEMINI_API_KEY not set. Skipping multimodal tests.";
|
||||
}
|
||||
|
||||
api_key_ = api_key;
|
||||
|
||||
// Create test data directory
|
||||
test_dir_ = std::filesystem::temp_directory_path() / "yaze_multimodal_test";
|
||||
std::filesystem::create_directories(test_dir_);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up test directory
|
||||
if (std::filesystem::exists(test_dir_)) {
|
||||
std::filesystem::remove_all(test_dir_);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Create a simple test image (16x16 PNG)
|
||||
std::filesystem::path CreateTestImage() {
|
||||
auto image_path = test_dir_ / "test_image.png";
|
||||
|
||||
// Create a minimal PNG file (16x16 red square)
|
||||
// PNG signature + IHDR + IDAT + IEND
|
||||
const unsigned char png_data[] = {
|
||||
// PNG signature
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
// IHDR chunk
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x91, 0x68,
|
||||
0x36,
|
||||
// IDAT chunk (minimal data)
|
||||
0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54,
|
||||
0x08, 0x99, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00,
|
||||
0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB4,
|
||||
// IEND chunk
|
||||
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
|
||||
0xAE, 0x42, 0x60, 0x82
|
||||
};
|
||||
|
||||
std::ofstream file(image_path, std::ios::binary);
|
||||
file.write(reinterpret_cast<const char*>(png_data), sizeof(png_data));
|
||||
file.close();
|
||||
|
||||
return image_path;
|
||||
}
|
||||
|
||||
std::string api_key_;
|
||||
std::filesystem::path test_dir_;
|
||||
};
|
||||
|
||||
TEST_F(GeminiVisionTest, BasicImageAnalysis) {
|
||||
cli::GeminiConfig config;
|
||||
config.api_key = api_key_;
|
||||
config.model = "gemini-2.5-flash"; // Vision-capable model
|
||||
config.verbose = false;
|
||||
|
||||
cli::GeminiAIService service(config);
|
||||
|
||||
// Create test image
|
||||
auto image_path = CreateTestImage();
|
||||
ASSERT_TRUE(std::filesystem::exists(image_path));
|
||||
|
||||
// Send multimodal request
|
||||
auto response = service.GenerateMultimodalResponse(
|
||||
image_path.string(),
|
||||
"Describe this image in one sentence."
|
||||
);
|
||||
|
||||
ASSERT_TRUE(response.ok()) << response.status().message();
|
||||
EXPECT_FALSE(response->text_response.empty());
|
||||
|
||||
std::cout << "Vision API response: " << response->text_response << std::endl;
|
||||
}
|
||||
|
||||
TEST_F(GeminiVisionTest, ImageWithSpecificPrompt) {
|
||||
cli::GeminiConfig config;
|
||||
config.api_key = api_key_;
|
||||
config.model = "gemini-2.5-flash";
|
||||
config.verbose = false;
|
||||
|
||||
cli::GeminiAIService service(config);
|
||||
|
||||
auto image_path = CreateTestImage();
|
||||
|
||||
// Ask specific question about the image
|
||||
auto response = service.GenerateMultimodalResponse(
|
||||
image_path.string(),
|
||||
"What color is the dominant color in this image? Answer with just the color name."
|
||||
);
|
||||
|
||||
ASSERT_TRUE(response.ok()) << response.status().message();
|
||||
EXPECT_FALSE(response->text_response.empty());
|
||||
|
||||
// Response should mention "red" since we created a red square
|
||||
std::string response_lower = response->text_response;
|
||||
std::transform(response_lower.begin(), response_lower.end(),
|
||||
response_lower.begin(), ::tolower);
|
||||
EXPECT_TRUE(response_lower.find("red") != std::string::npos ||
|
||||
response_lower.find("pink") != std::string::npos)
|
||||
<< "Expected color 'red' or 'pink' in response: " << response->text_response;
|
||||
}
|
||||
|
||||
TEST_F(GeminiVisionTest, InvalidImagePath) {
|
||||
cli::GeminiConfig config;
|
||||
config.api_key = api_key_;
|
||||
config.model = "gemini-2.5-flash";
|
||||
|
||||
cli::GeminiAIService service(config);
|
||||
|
||||
// Try with non-existent image
|
||||
auto response = service.GenerateMultimodalResponse(
|
||||
"/nonexistent/image.png",
|
||||
"Describe this image."
|
||||
);
|
||||
|
||||
EXPECT_FALSE(response.ok());
|
||||
EXPECT_TRUE(absl::IsNotFound(response.status()) ||
|
||||
absl::IsInternal(response.status()));
|
||||
}
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
// Integration test with screenshot capture
|
||||
TEST_F(GeminiVisionTest, ScreenshotCaptureIntegration) {
|
||||
// Note: This test requires a running YAZE instance with gRPC test harness
|
||||
// Skip if we can't connect
|
||||
|
||||
cli::GeminiConfig config;
|
||||
config.api_key = api_key_;
|
||||
config.model = "gemini-2.5-flash";
|
||||
config.verbose = false;
|
||||
|
||||
cli::GeminiAIService service(config);
|
||||
|
||||
// Attempt to capture a screenshot
|
||||
auto screenshot_result = yaze::test::CaptureHarnessScreenshot(
|
||||
(test_dir_ / "screenshot.png").string());
|
||||
|
||||
if (!screenshot_result.ok()) {
|
||||
GTEST_SKIP() << "Screenshot capture failed (YAZE may not be running): "
|
||||
<< screenshot_result.status().message();
|
||||
}
|
||||
|
||||
// Analyze the captured screenshot
|
||||
auto response = service.GenerateMultimodalResponse(
|
||||
screenshot_result->file_path,
|
||||
"What UI elements are visible in this screenshot? List them."
|
||||
);
|
||||
|
||||
ASSERT_TRUE(response.ok()) << response.status().message();
|
||||
EXPECT_FALSE(response->text_response.empty());
|
||||
|
||||
std::cout << "Screenshot analysis: " << response->text_response << std::endl;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Performance test
|
||||
TEST_F(GeminiVisionTest, MultipleRequestsSequential) {
|
||||
cli::GeminiConfig config;
|
||||
config.api_key = api_key_;
|
||||
config.model = "gemini-2.5-flash";
|
||||
config.verbose = false;
|
||||
|
||||
cli::GeminiAIService service(config);
|
||||
|
||||
auto image_path = CreateTestImage();
|
||||
|
||||
// Make 3 sequential requests
|
||||
const int num_requests = 3;
|
||||
for (int i = 0; i < num_requests; ++i) {
|
||||
auto response = service.GenerateMultimodalResponse(
|
||||
image_path.string(),
|
||||
absl::StrCat("Request ", i + 1, ": Describe this image briefly.")
|
||||
);
|
||||
|
||||
ASSERT_TRUE(response.ok()) << "Request " << i + 1 << " failed: "
|
||||
<< response.status().message();
|
||||
EXPECT_FALSE(response->text_response.empty());
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting test (should handle gracefully)
|
||||
TEST_F(GeminiVisionTest, RateLimitHandling) {
|
||||
cli::GeminiConfig config;
|
||||
config.api_key = api_key_;
|
||||
config.model = "gemini-2.5-flash";
|
||||
config.verbose = false;
|
||||
|
||||
cli::GeminiAIService service(config);
|
||||
|
||||
auto image_path = CreateTestImage();
|
||||
|
||||
// Make many rapid requests (may hit rate limit)
|
||||
int successful = 0;
|
||||
int rate_limited = 0;
|
||||
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
auto response = service.GenerateMultimodalResponse(
|
||||
image_path.string(),
|
||||
"Describe this image."
|
||||
);
|
||||
|
||||
if (response.ok()) {
|
||||
successful++;
|
||||
} else if (absl::IsResourceExhausted(response.status()) ||
|
||||
response.status().message().find("429") != std::string::npos) {
|
||||
rate_limited++;
|
||||
}
|
||||
}
|
||||
|
||||
// At least some requests should succeed
|
||||
EXPECT_GT(successful, 0) << "No successful requests out of 10";
|
||||
|
||||
// If we hit rate limits, that's expected behavior (not a failure)
|
||||
if (rate_limited > 0) {
|
||||
std::cout << "Note: Hit rate limit on " << rate_limited << " out of 10 requests (expected)" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
// Note: main() is provided by yaze_test.cc for the unified test runner
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "app/core/asar_wrapper.h"
|
||||
#include "core/asar_wrapper.h"
|
||||
#include "app/rom.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "testing.h"
|
||||
@@ -17,7 +17,7 @@ namespace integration {
|
||||
class AsarIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
wrapper_ = std::make_unique<app::core::AsarWrapper>();
|
||||
wrapper_ = std::make_unique<core::AsarWrapper>();
|
||||
|
||||
// Create test directory
|
||||
test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_integration";
|
||||
@@ -322,7 +322,7 @@ error_test:
|
||||
err_file.close();
|
||||
}
|
||||
|
||||
std::unique_ptr<app::core::AsarWrapper> wrapper_;
|
||||
std::unique_ptr<core::AsarWrapper> wrapper_;
|
||||
std::filesystem::path test_dir_;
|
||||
std::filesystem::path comprehensive_asm_path_;
|
||||
std::filesystem::path advanced_asm_path_;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// Must define before any ImGui includes
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "app/core/asar_wrapper.h"
|
||||
#include "core/asar_wrapper.h"
|
||||
#include "app/rom.h"
|
||||
#include "test_utils.h"
|
||||
#include "testing.h"
|
||||
@@ -19,14 +24,14 @@ class AsarRomIntegrationTest : public RomDependentTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
RomDependentTest::SetUp();
|
||||
|
||||
wrapper_ = std::make_unique<app::core::AsarWrapper>();
|
||||
|
||||
wrapper_ = std::make_unique<core::AsarWrapper>();
|
||||
ASSERT_OK(wrapper_->Initialize());
|
||||
|
||||
|
||||
// Create test directory
|
||||
test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_rom_test";
|
||||
std::filesystem::create_directories(test_dir_);
|
||||
|
||||
|
||||
CreateTestPatches();
|
||||
}
|
||||
|
||||
@@ -226,7 +231,7 @@ enemy_shell:
|
||||
symbols_file.close();
|
||||
}
|
||||
|
||||
std::unique_ptr<app::core::AsarWrapper> wrapper_;
|
||||
std::unique_ptr<core::AsarWrapper> wrapper_;
|
||||
std::filesystem::path test_dir_;
|
||||
std::filesystem::path simple_patch_path_;
|
||||
std::filesystem::path gameplay_patch_path_;
|
||||
@@ -239,15 +244,16 @@ TEST_F(AsarRomIntegrationTest, SimplePatchOnRealRom) {
|
||||
size_t original_size = rom_copy.size();
|
||||
|
||||
// Apply simple patch
|
||||
auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
|
||||
auto patch_result =
|
||||
wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(patch_result.status());
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success) << "Patch failed: "
|
||||
<< testing::PrintToString(result.errors);
|
||||
EXPECT_TRUE(result.success)
|
||||
<< "Patch failed: " << testing::PrintToString(result.errors);
|
||||
|
||||
// Verify ROM was modified
|
||||
EXPECT_NE(rom_copy, test_rom_); // Should be different
|
||||
EXPECT_NE(rom_copy, test_rom_); // Should be different
|
||||
EXPECT_GE(rom_copy.size(), original_size); // Size may have grown
|
||||
|
||||
// Check for expected symbols
|
||||
@@ -277,17 +283,16 @@ TEST_F(AsarRomIntegrationTest, SymbolExtractionFromRealRom) {
|
||||
|
||||
// Check for specific symbols we expect
|
||||
std::vector<std::string> expected_symbols = {
|
||||
"main_routine", "init_player", "game_loop", "update_player",
|
||||
"update_enemies", "update_graphics", "multiply_by_two", "divide_by_two"
|
||||
};
|
||||
"main_routine", "init_player", "game_loop", "update_player",
|
||||
"update_enemies", "update_graphics", "multiply_by_two", "divide_by_two"};
|
||||
|
||||
for (const auto& expected_symbol : expected_symbols) {
|
||||
bool found = false;
|
||||
for (const auto& symbol : symbols) {
|
||||
if (symbol.name == expected_symbol) {
|
||||
found = true;
|
||||
EXPECT_GT(symbol.address, 0) << "Symbol " << expected_symbol
|
||||
<< " has invalid address";
|
||||
EXPECT_GT(symbol.address, 0)
|
||||
<< "Symbol " << expected_symbol << " has invalid address";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -311,19 +316,20 @@ TEST_F(AsarRomIntegrationTest, GameplayModificationPatch) {
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
|
||||
// Apply gameplay modification patch
|
||||
auto patch_result = wrapper_->ApplyPatch(gameplay_patch_path_.string(), rom_copy);
|
||||
auto patch_result =
|
||||
wrapper_->ApplyPatch(gameplay_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(patch_result.status());
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
EXPECT_TRUE(result.success) << "Gameplay patch failed: "
|
||||
<< testing::PrintToString(result.errors);
|
||||
EXPECT_TRUE(result.success)
|
||||
<< "Gameplay patch failed: " << testing::PrintToString(result.errors);
|
||||
|
||||
// Verify specific memory locations were modified
|
||||
// Note: These addresses are based on the patch content
|
||||
|
||||
|
||||
// Check health modification at 0x7EF36C -> ROM offset would need calculation
|
||||
// For a proper test, we'd need to convert SNES addresses to ROM offsets
|
||||
|
||||
|
||||
// Check if custom routine was inserted at 0xC000 -> ROM offset 0x18000 (in LoROM)
|
||||
const uint32_t rom_offset = 0x18000; // Bank $00:C000 in LoROM
|
||||
if (rom_offset < rom_copy.size()) {
|
||||
@@ -369,40 +375,43 @@ broken_routine:
|
||||
broken_file.close();
|
||||
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(broken_patch_path.string(), rom_copy);
|
||||
auto patch_result =
|
||||
wrapper_->ApplyPatch(broken_patch_path.string(), rom_copy);
|
||||
|
||||
// Should fail with proper error messages
|
||||
EXPECT_FALSE(patch_result.ok());
|
||||
EXPECT_THAT(patch_result.status().message(),
|
||||
testing::AnyOf(
|
||||
testing::HasSubstr("invalid"),
|
||||
testing::HasSubstr("unknown"),
|
||||
testing::HasSubstr("error")));
|
||||
EXPECT_THAT(patch_result.status().message(),
|
||||
testing::AnyOf(testing::HasSubstr("invalid"),
|
||||
testing::HasSubstr("unknown"),
|
||||
testing::HasSubstr("error")));
|
||||
}
|
||||
|
||||
TEST_F(AsarRomIntegrationTest, PatchValidationWorkflow) {
|
||||
// Test the complete workflow: validate -> patch -> verify
|
||||
|
||||
|
||||
// Step 1: Validate assembly
|
||||
auto validation_result = wrapper_->ValidateAssembly(simple_patch_path_.string());
|
||||
auto validation_result =
|
||||
wrapper_->ValidateAssembly(simple_patch_path_.string());
|
||||
EXPECT_OK(validation_result);
|
||||
|
||||
// Step 2: Apply patch
|
||||
std::vector<uint8_t> rom_copy = test_rom_;
|
||||
auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
|
||||
auto patch_result =
|
||||
wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy);
|
||||
ASSERT_OK(patch_result.status());
|
||||
EXPECT_TRUE(patch_result->success);
|
||||
|
||||
// Step 3: Verify results
|
||||
EXPECT_GT(patch_result->symbols.size(), 0);
|
||||
EXPECT_GT(patch_result->rom_size, 0);
|
||||
|
||||
|
||||
// Step 4: Test symbol operations
|
||||
auto entry_symbol = wrapper_->FindSymbol("yaze_test_entry");
|
||||
EXPECT_TRUE(entry_symbol.has_value());
|
||||
|
||||
|
||||
if (entry_symbol) {
|
||||
auto symbols_at_address = wrapper_->GetSymbolsAtAddress(entry_symbol->address);
|
||||
auto symbols_at_address =
|
||||
wrapper_->GetSymbolsAtAddress(entry_symbol->address);
|
||||
EXPECT_GT(symbols_at_address.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,233 +1,231 @@
|
||||
#include "integration/dungeon_editor_test.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/zelda3/dungeon/room.h"
|
||||
#include "app/zelda3/dungeon/room_object.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
void DungeonEditorIntegrationTest::SetUp() {
|
||||
ASSERT_TRUE(CreateMockRom().ok());
|
||||
ASSERT_TRUE(LoadTestRoomData().ok());
|
||||
|
||||
dungeon_editor_ = std::make_unique<editor::DungeonEditor>(mock_rom_.get());
|
||||
dungeon_editor_->Initialize();
|
||||
}
|
||||
using namespace yaze::zelda3;
|
||||
|
||||
void DungeonEditorIntegrationTest::TearDown() {
|
||||
dungeon_editor_.reset();
|
||||
mock_rom_.reset();
|
||||
}
|
||||
// ============================================================================
|
||||
// Basic Room Loading Tests
|
||||
// ============================================================================
|
||||
|
||||
absl::Status DungeonEditorIntegrationTest::CreateMockRom() {
|
||||
mock_rom_ = std::make_unique<MockRom>();
|
||||
|
||||
// Generate mock ROM data
|
||||
std::vector<uint8_t> mock_data(kMockRomSize, 0x00);
|
||||
|
||||
// Set up basic ROM structure
|
||||
// Header at 0x7FC0
|
||||
std::string title = "ZELDA3 TEST ROM";
|
||||
std::memcpy(&mock_data[0x7FC0], title.c_str(), std::min(title.length(), size_t(21)));
|
||||
|
||||
// Set ROM size and type
|
||||
mock_data[0x7FD7] = 0x21; // 2MB ROM
|
||||
mock_data[0x7FD8] = 0x00; // SRAM size
|
||||
mock_data[0x7FD9] = 0x00; // Country code (NTSC)
|
||||
mock_data[0x7FDA] = 0x00; // License code
|
||||
mock_data[0x7FDB] = 0x00; // Version
|
||||
|
||||
// Set up room header pointers
|
||||
mock_data[0xB5DD] = 0x00; // Room header pointer low
|
||||
mock_data[0xB5DE] = 0x00; // Room header pointer mid
|
||||
mock_data[0xB5DF] = 0x00; // Room header pointer high
|
||||
|
||||
// Set up object pointers
|
||||
mock_data[0x874C] = 0x00; // Object pointer low
|
||||
mock_data[0x874D] = 0x00; // Object pointer mid
|
||||
mock_data[0x874E] = 0x00; // Object pointer high
|
||||
|
||||
static_cast<MockRom*>(mock_rom_.get())->SetMockData(mock_data);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status DungeonEditorIntegrationTest::LoadTestRoomData() {
|
||||
// Generate test room data
|
||||
auto room_header = GenerateMockRoomHeader(kTestRoomId);
|
||||
auto object_data = GenerateMockObjectData();
|
||||
auto graphics_data = GenerateMockGraphicsData();
|
||||
|
||||
static_cast<MockRom*>(mock_rom_.get())->SetMockRoomData(kTestRoomId, room_header);
|
||||
static_cast<MockRom*>(mock_rom_.get())->SetMockObjectData(kTestObjectId, object_data);
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status DungeonEditorIntegrationTest::TestObjectParsing() {
|
||||
// Test object parsing without SNES emulation
|
||||
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
|
||||
|
||||
// Verify room was loaded correctly
|
||||
TEST_F(DungeonEditorIntegrationTest, LoadRoomFromRealRom) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
EXPECT_NE(room.rom(), nullptr);
|
||||
// Note: room_id_ is private, so we can't directly access it in tests
|
||||
|
||||
// Test object loading
|
||||
room.LoadObjects();
|
||||
EXPECT_FALSE(room.GetTileObjects().empty());
|
||||
|
||||
// Verify object properties
|
||||
for (const auto& obj : room.GetTileObjects()) {
|
||||
// Note: id_ is private, so we can't directly access it in tests
|
||||
EXPECT_LE(obj.x_, 31); // Room width limit
|
||||
EXPECT_LE(obj.y_, 31); // Room height limit
|
||||
// Note: rom() method is not const, so we can't call it on const objects
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status DungeonEditorIntegrationTest::TestObjectRendering() {
|
||||
// Test object rendering without SNES emulation
|
||||
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
|
||||
TEST_F(DungeonEditorIntegrationTest, LoadMultipleRooms) {
|
||||
// Test loading several different rooms
|
||||
for (int room_id : {0x00, 0x01, 0x02, 0x10, 0x20}) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), room_id);
|
||||
EXPECT_NE(room.rom(), nullptr) << "Failed to load room " << std::hex << room_id;
|
||||
room.LoadObjects();
|
||||
// Some rooms may be empty, but loading should not fail
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, DungeonEditorInitialization) {
|
||||
// Initialize the editor before loading
|
||||
dungeon_editor_->Initialize();
|
||||
|
||||
// Now load should succeed
|
||||
auto status = dungeon_editor_->Load();
|
||||
ASSERT_TRUE(status.ok()) << "Load failed: " << status.message();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Object Encoding/Decoding Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, ObjectEncodingRoundTrip) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
room.LoadObjects();
|
||||
|
||||
// Test tile loading for objects
|
||||
for (auto& obj : room.GetTileObjects()) {
|
||||
obj.EnsureTilesLoaded();
|
||||
EXPECT_FALSE(obj.tiles_.empty());
|
||||
auto encoded = room.EncodeObjects();
|
||||
EXPECT_FALSE(encoded.empty());
|
||||
EXPECT_EQ(encoded[encoded.size()-1], 0xFF); // Terminator
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, EncodeType1Object) {
|
||||
// Type 1: xxxxxxss yyyyyyss iiiiiiii (ID < 0x100)
|
||||
zelda3::RoomObject obj(0x10, 5, 7, 0x12, 0); // id, x, y, size, layer
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
|
||||
// Verify encoding format
|
||||
EXPECT_EQ((bytes.b1 >> 2), 5) << "X coordinate should be in upper 6 bits of b1";
|
||||
EXPECT_EQ((bytes.b2 >> 2), 7) << "Y coordinate should be in upper 6 bits of b2";
|
||||
EXPECT_EQ(bytes.b3, 0x10) << "Object ID should be in b3";
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, EncodeType2Object) {
|
||||
// Type 2: 111111xx xxxxyyyy yyiiiiii (ID >= 0x100 && < 0x200)
|
||||
zelda3::RoomObject obj(0x150, 12, 8, 0, 0);
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
|
||||
// Verify Type 2 marker
|
||||
EXPECT_EQ((bytes.b1 & 0xFC), 0xFC) << "Type 2 objects should have 111111 prefix";
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, EncodeType3Object) {
|
||||
// Type 3: xxxxxxii yyyyyyii 11111iii (ID >= 0xF00)
|
||||
zelda3::RoomObject obj(0xF23, 3, 4, 0, 0);
|
||||
auto bytes = obj.EncodeObjectToBytes();
|
||||
|
||||
// Verify Type 3 encoding: bytes.b3 = (id_ >> 4) & 0xFF
|
||||
// For ID 0xF23: (0xF23 >> 4) = 0xF2
|
||||
EXPECT_EQ(bytes.b3, 0xF2) << "Type 3: (ID >> 4) should be in b3";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Object Manipulation Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, AddObjectToRoom) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
room.LoadObjects();
|
||||
|
||||
size_t initial_count = room.GetTileObjects().size();
|
||||
|
||||
// Add a new object (Type 1, so size must be <= 15)
|
||||
zelda3::RoomObject new_obj(0x20, 10, 10, 5, 0);
|
||||
new_obj.set_rom(rom_.get());
|
||||
auto status = room.AddObject(new_obj);
|
||||
|
||||
EXPECT_TRUE(status.ok()) << "Failed to add object: " << status.message();
|
||||
EXPECT_EQ(room.GetTileObjects().size(), initial_count + 1);
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, RemoveObjectFromRoom) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
room.LoadObjects();
|
||||
|
||||
size_t initial_count = room.GetTileObjects().size();
|
||||
ASSERT_GT(initial_count, 0) << "Room should have at least one object";
|
||||
|
||||
// Remove first object
|
||||
auto status = room.RemoveObject(0);
|
||||
|
||||
EXPECT_TRUE(status.ok()) << "Failed to remove object: " << status.message();
|
||||
EXPECT_EQ(room.GetTileObjects().size(), initial_count - 1);
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, UpdateObjectInRoom) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
room.LoadObjects();
|
||||
|
||||
ASSERT_FALSE(room.GetTileObjects().empty());
|
||||
|
||||
// Update first object's position
|
||||
zelda3::RoomObject updated_obj = room.GetTileObjects()[0];
|
||||
updated_obj.x_ = 15;
|
||||
updated_obj.y_ = 15;
|
||||
|
||||
auto status = room.UpdateObject(0, updated_obj);
|
||||
|
||||
EXPECT_TRUE(status.ok()) << "Failed to update object: " << status.message();
|
||||
EXPECT_EQ(room.GetTileObjects()[0].x_, 15);
|
||||
EXPECT_EQ(room.GetTileObjects()[0].y_, 15);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Object Validation Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, ValidateObjectBounds) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
|
||||
// Test objects within valid bounds (0-63 for x and y)
|
||||
zelda3::RoomObject valid_obj(0x10, 0, 0, 0, 0);
|
||||
EXPECT_TRUE(room.ValidateObject(valid_obj));
|
||||
|
||||
zelda3::RoomObject valid_obj2(0x10, 31, 31, 0, 0);
|
||||
EXPECT_TRUE(room.ValidateObject(valid_obj2));
|
||||
|
||||
zelda3::RoomObject valid_obj3(0x10, 63, 63, 0, 0);
|
||||
EXPECT_TRUE(room.ValidateObject(valid_obj3));
|
||||
|
||||
// Test objects outside bounds (> 63)
|
||||
zelda3::RoomObject invalid_obj(0x10, 64, 64, 0, 0);
|
||||
EXPECT_FALSE(room.ValidateObject(invalid_obj));
|
||||
|
||||
zelda3::RoomObject invalid_obj2(0x10, 100, 100, 0, 0);
|
||||
EXPECT_FALSE(room.ValidateObject(invalid_obj2));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Save/Load Round-Trip Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, SaveAndReloadRoom) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
room.LoadObjects();
|
||||
|
||||
size_t original_count = room.GetTileObjects().size();
|
||||
|
||||
// Encode objects
|
||||
auto encoded = room.EncodeObjects();
|
||||
EXPECT_FALSE(encoded.empty());
|
||||
|
||||
// Create a new room and decode
|
||||
auto room2 = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
room2.LoadObjects();
|
||||
|
||||
// Verify object count matches
|
||||
EXPECT_EQ(room2.GetTileObjects().size(), original_count);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Object Rendering Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, RenderObjectWithTiles) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
room.LoadObjects();
|
||||
|
||||
ASSERT_FALSE(room.GetTileObjects().empty());
|
||||
|
||||
// Ensure tiles are loaded for first object
|
||||
auto& obj = room.GetTileObjects()[0];
|
||||
const_cast<zelda3::RoomObject&>(obj).set_rom(rom_.get());
|
||||
const_cast<zelda3::RoomObject&>(obj).EnsureTilesLoaded();
|
||||
|
||||
EXPECT_FALSE(obj.tiles_.empty()) << "Object should have tiles after loading";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Multi-Layer Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, ObjectsOnDifferentLayers) {
|
||||
auto room = zelda3::LoadRoomFromRom(rom_.get(), kTestRoomId);
|
||||
|
||||
// Add objects on different layers
|
||||
zelda3::RoomObject obj_bg1(0x10, 5, 5, 0, 0); // Layer 0 (BG2)
|
||||
zelda3::RoomObject obj_bg2(0x11, 6, 6, 0, 1); // Layer 1 (BG1)
|
||||
zelda3::RoomObject obj_bg3(0x12, 7, 7, 0, 2); // Layer 2 (BG3)
|
||||
|
||||
room.AddObject(obj_bg1);
|
||||
room.AddObject(obj_bg2);
|
||||
room.AddObject(obj_bg3);
|
||||
|
||||
// Encode and verify layer separation
|
||||
auto encoded = room.EncodeObjects();
|
||||
|
||||
// Should have layer terminators (0xFF 0xFF between layers)
|
||||
int terminator_count = 0;
|
||||
for (size_t i = 0; i < encoded.size() - 1; i++) {
|
||||
if (encoded[i] == 0xFF && encoded[i+1] == 0xFF) {
|
||||
terminator_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Test room graphics rendering
|
||||
room.LoadRoomGraphics();
|
||||
room.RenderRoomGraphics();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status DungeonEditorIntegrationTest::TestRoomGraphics() {
|
||||
// Test room graphics loading and rendering
|
||||
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
|
||||
|
||||
// Test graphics loading
|
||||
room.LoadRoomGraphics();
|
||||
EXPECT_FALSE(room.blocks().empty());
|
||||
|
||||
// Test graphics rendering
|
||||
room.RenderRoomGraphics();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status DungeonEditorIntegrationTest::TestPaletteHandling() {
|
||||
// Test palette loading and application
|
||||
auto room = zelda3::LoadRoomFromRom(mock_rom_.get(), kTestRoomId);
|
||||
|
||||
// Verify palette is set
|
||||
EXPECT_GE(room.palette, 0);
|
||||
EXPECT_LE(room.palette, 0x47); // Max palette index
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DungeonEditorIntegrationTest::GenerateMockRoomHeader(int room_id) {
|
||||
std::vector<uint8_t> header(32, 0x00);
|
||||
|
||||
// Basic room properties
|
||||
header[0] = 0x00; // Background type, collision, light
|
||||
header[1] = 0x00; // Palette
|
||||
header[2] = 0x01; // Blockset
|
||||
header[3] = 0x01; // Spriteset
|
||||
header[4] = 0x00; // Effect
|
||||
header[5] = 0x00; // Tag1
|
||||
header[6] = 0x00; // Tag2
|
||||
header[7] = 0x00; // Staircase planes
|
||||
header[8] = 0x00; // Staircase planes continued
|
||||
header[9] = 0x00; // Hole warp
|
||||
header[10] = 0x00; // Staircase rooms
|
||||
header[11] = 0x00;
|
||||
header[12] = 0x00;
|
||||
header[13] = 0x00;
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DungeonEditorIntegrationTest::GenerateMockObjectData() {
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
// Add a simple wall object
|
||||
data.push_back(0x08); // X position (2 tiles)
|
||||
data.push_back(0x08); // Y position (2 tiles)
|
||||
data.push_back(0x01); // Object ID (wall)
|
||||
|
||||
// Add layer separator
|
||||
data.push_back(0xFF);
|
||||
data.push_back(0xFF);
|
||||
|
||||
// Add door section
|
||||
data.push_back(0xF0);
|
||||
data.push_back(0xFF);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DungeonEditorIntegrationTest::GenerateMockGraphicsData() {
|
||||
std::vector<uint8_t> data(0x4000, 0x00);
|
||||
|
||||
// Generate basic tile data
|
||||
for (size_t i = 0; i < data.size(); i += 2) {
|
||||
data[i] = 0x00; // Tile low byte
|
||||
data[i + 1] = 0x00; // Tile high byte
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void MockRom::SetMockData(const std::vector<uint8_t>& data) {
|
||||
mock_data_ = data;
|
||||
}
|
||||
|
||||
void MockRom::SetMockRoomData(int room_id, const std::vector<uint8_t>& data) {
|
||||
mock_room_data_[room_id] = data;
|
||||
}
|
||||
|
||||
void MockRom::SetMockObjectData(int object_id, const std::vector<uint8_t>& data) {
|
||||
mock_object_data_[object_id] = data;
|
||||
}
|
||||
|
||||
bool MockRom::ValidateRoomData(int room_id) const {
|
||||
return mock_room_data_.find(room_id) != mock_room_data_.end();
|
||||
}
|
||||
|
||||
bool MockRom::ValidateObjectData(int object_id) const {
|
||||
return mock_object_data_.find(object_id) != mock_object_data_.end();
|
||||
}
|
||||
|
||||
// Test cases
|
||||
TEST_F(DungeonEditorIntegrationTest, ObjectParsingTest) {
|
||||
EXPECT_TRUE(TestObjectParsing().ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, ObjectRenderingTest) {
|
||||
EXPECT_TRUE(TestObjectRendering().ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, RoomGraphicsTest) {
|
||||
EXPECT_TRUE(TestRoomGraphics().ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, PaletteHandlingTest) {
|
||||
EXPECT_TRUE(TestPaletteHandling().ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorIntegrationTest, MockRomValidation) {
|
||||
EXPECT_TRUE(static_cast<MockRom*>(mock_rom_.get())->ValidateRoomData(kTestRoomId));
|
||||
EXPECT_TRUE(static_cast<MockRom*>(mock_rom_.get())->ValidateObjectData(kTestObjectId));
|
||||
EXPECT_GE(terminator_count, 2) << "Should have at least 2 layer terminators";
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
} // namespace yaze
|
||||
|
||||
@@ -4,72 +4,56 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/editor/dungeon/dungeon_editor.h"
|
||||
#include "app/editor/dungeon/dungeon_editor_v2.h"
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Integration test framework for dungeon editor components
|
||||
* @brief Integration test framework using real ROM data
|
||||
*
|
||||
* This class provides a comprehensive testing framework for the dungeon editor,
|
||||
* allowing modular testing of individual components and their interactions.
|
||||
* Updated for DungeonEditorV2 with card-based architecture
|
||||
*/
|
||||
class DungeonEditorIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override;
|
||||
void TearDown() override;
|
||||
void SetUp() override {
|
||||
// Use the real ROM (try multiple locations)
|
||||
rom_ = std::make_unique<Rom>();
|
||||
auto status = rom_->LoadFromFile("assets/zelda3.sfc");
|
||||
if (!status.ok()) {
|
||||
status = rom_->LoadFromFile("build/bin/zelda3.sfc");
|
||||
}
|
||||
if (!status.ok()) {
|
||||
status = rom_->LoadFromFile("zelda3.sfc");
|
||||
}
|
||||
ASSERT_TRUE(status.ok()) << "Could not load zelda3.sfc from any location";
|
||||
ASSERT_TRUE(rom_->InitializeForTesting().ok());
|
||||
|
||||
// Initialize DungeonEditorV2 with ROM
|
||||
dungeon_editor_ = std::make_unique<editor::DungeonEditorV2>();
|
||||
dungeon_editor_->set_rom(rom_.get());
|
||||
|
||||
// Load editor data
|
||||
auto load_status = dungeon_editor_->Load();
|
||||
ASSERT_TRUE(load_status.ok()) << "Failed to load dungeon editor: "
|
||||
<< load_status.message();
|
||||
}
|
||||
|
||||
// Test data setup
|
||||
absl::Status CreateMockRom();
|
||||
absl::Status LoadTestRoomData();
|
||||
|
||||
// Component testing helpers
|
||||
absl::Status TestObjectParsing();
|
||||
absl::Status TestObjectRendering();
|
||||
absl::Status TestRoomGraphics();
|
||||
absl::Status TestPaletteHandling();
|
||||
|
||||
// Mock data generators
|
||||
std::vector<uint8_t> GenerateMockRoomHeader(int room_id);
|
||||
std::vector<uint8_t> GenerateMockObjectData();
|
||||
std::vector<uint8_t> GenerateMockGraphicsData();
|
||||
void TearDown() override {
|
||||
dungeon_editor_.reset();
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> mock_rom_;
|
||||
std::unique_ptr<editor::DungeonEditor> dungeon_editor_;
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<editor::DungeonEditorV2> dungeon_editor_;
|
||||
|
||||
// Test constants
|
||||
static constexpr int kTestRoomId = 0x01;
|
||||
static constexpr int kTestObjectId = 0x10;
|
||||
static constexpr size_t kMockRomSize = 0x200000; // 2MB mock ROM
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Mock ROM class for testing without real ROM files
|
||||
*/
|
||||
class MockRom : public Rom {
|
||||
public:
|
||||
MockRom() = default;
|
||||
|
||||
// Test data injection
|
||||
void SetMockData(const std::vector<uint8_t>& data);
|
||||
void SetMockRoomData(int room_id, const std::vector<uint8_t>& data);
|
||||
void SetMockObjectData(int object_id, const std::vector<uint8_t>& data);
|
||||
|
||||
// Validation helpers
|
||||
bool ValidateRoomData(int room_id) const;
|
||||
bool ValidateObjectData(int object_id) const;
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> mock_data_;
|
||||
std::map<int, std::vector<uint8_t>> mock_room_data_;
|
||||
std::map<int, std::vector<uint8_t>> mock_object_data_;
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_TEST_H
|
||||
#endif // YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_TEST_H
|
||||
|
||||
250
test/integration/dungeon_editor_v2_test.cc
Normal file
250
test/integration/dungeon_editor_v2_test.cc
Normal file
@@ -0,0 +1,250 @@
|
||||
#include "integration/dungeon_editor_v2_test.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
// ============================================================================
|
||||
// Basic Initialization Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, EditorInitialization) {
|
||||
// Initialize should not fail
|
||||
dungeon_editor_v2_->Initialize();
|
||||
EXPECT_TRUE(dungeon_editor_v2_->rom() != nullptr);
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, RomLoadStatus) {
|
||||
EXPECT_TRUE(dungeon_editor_v2_->IsRomLoaded());
|
||||
std::string status = dungeon_editor_v2_->GetRomStatus();
|
||||
EXPECT_FALSE(status.empty());
|
||||
EXPECT_NE(status, "No ROM loaded");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Load Tests - Component Delegation
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, LoadAllRooms) {
|
||||
// Test that Load() properly delegates to room_loader_
|
||||
dungeon_editor_v2_->Initialize();
|
||||
auto status = dungeon_editor_v2_->Load();
|
||||
ASSERT_TRUE(status.ok()) << "Load failed: " << status.message();
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, LoadWithoutRom) {
|
||||
// Test error handling when ROM is not available
|
||||
editor::DungeonEditorV2 editor(nullptr);
|
||||
auto status = editor.Load();
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, LoadSequence) {
|
||||
// Test the full initialization sequence
|
||||
dungeon_editor_v2_->Initialize();
|
||||
|
||||
auto load_status = dungeon_editor_v2_->Load();
|
||||
ASSERT_TRUE(load_status.ok());
|
||||
|
||||
// After loading, Update() should work
|
||||
(void)dungeon_editor_v2_->Update();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Update Tests - UI Coordination
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, UpdateBeforeLoad) {
|
||||
// Update before Load should show loading message but not crash
|
||||
auto status = dungeon_editor_v2_->Update();
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, UpdateAfterLoad) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
(void)dungeon_editor_v2_->Load();
|
||||
|
||||
// Update should delegate to components
|
||||
auto status = dungeon_editor_v2_->Update();
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Save Tests - Component Delegation
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, SaveWithoutRom) {
|
||||
// Test error handling when ROM is not available
|
||||
editor::DungeonEditorV2 editor(nullptr);
|
||||
auto status = editor.Save();
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, SaveAfterLoad) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
auto load_status = dungeon_editor_v2_->Load();
|
||||
ASSERT_TRUE(load_status.ok());
|
||||
|
||||
// Save should delegate to room objects
|
||||
auto save_status = dungeon_editor_v2_->Save();
|
||||
EXPECT_TRUE(save_status.ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Room Management Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, AddRoomTab) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
(void)dungeon_editor_v2_->Load();
|
||||
|
||||
// Add a room tab
|
||||
dungeon_editor_v2_->add_room(kTestRoomId);
|
||||
|
||||
// This should not crash or fail
|
||||
auto status = dungeon_editor_v2_->Update();
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, AddMultipleRoomTabs) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
(void)dungeon_editor_v2_->Load();
|
||||
|
||||
// Add multiple rooms
|
||||
dungeon_editor_v2_->add_room(0x00);
|
||||
dungeon_editor_v2_->add_room(0x01);
|
||||
dungeon_editor_v2_->add_room(0x02);
|
||||
|
||||
auto status = dungeon_editor_v2_->Update();
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component Delegation Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, RoomLoaderDelegation) {
|
||||
// Verify that Load() delegates to room_loader_
|
||||
dungeon_editor_v2_->Initialize();
|
||||
auto status = dungeon_editor_v2_->Load();
|
||||
|
||||
// If Load succeeds, room_loader_ must have worked
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, ComponentsInitializedAfterLoad) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
auto status = dungeon_editor_v2_->Load();
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// After Load(), all components should be properly initialized
|
||||
// We can't directly test this, but Update() should work
|
||||
(void)dungeon_editor_v2_->Update();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ROM Management Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, SetRomAfterConstruction) {
|
||||
// Create editor without ROM
|
||||
editor::DungeonEditorV2 editor;
|
||||
EXPECT_EQ(editor.rom(), nullptr);
|
||||
|
||||
// Set ROM
|
||||
editor.set_rom(rom_.get());
|
||||
EXPECT_EQ(editor.rom(), rom_.get());
|
||||
EXPECT_TRUE(editor.IsRomLoaded());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, SetRomAndLoad) {
|
||||
// Create editor without ROM
|
||||
editor::DungeonEditorV2 editor;
|
||||
|
||||
// Set ROM and load
|
||||
editor.set_rom(rom_.get());
|
||||
editor.Initialize();
|
||||
auto status = editor.Load();
|
||||
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unimplemented Methods Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, UnimplementedMethods) {
|
||||
// These should return UnimplementedError
|
||||
EXPECT_EQ(dungeon_editor_v2_->Undo().code(),
|
||||
absl::StatusCode::kUnimplemented);
|
||||
EXPECT_EQ(dungeon_editor_v2_->Redo().code(),
|
||||
absl::StatusCode::kUnimplemented);
|
||||
EXPECT_EQ(dungeon_editor_v2_->Cut().code(),
|
||||
absl::StatusCode::kUnimplemented);
|
||||
EXPECT_EQ(dungeon_editor_v2_->Copy().code(),
|
||||
absl::StatusCode::kUnimplemented);
|
||||
EXPECT_EQ(dungeon_editor_v2_->Paste().code(),
|
||||
absl::StatusCode::kUnimplemented);
|
||||
EXPECT_EQ(dungeon_editor_v2_->Find().code(),
|
||||
absl::StatusCode::kUnimplemented);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stress Testing
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, MultipleUpdateCycles) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
auto load_status = dungeon_editor_v2_->Load();
|
||||
ASSERT_TRUE(load_status.ok());
|
||||
|
||||
// Run multiple update cycles
|
||||
for (int i = 0; i < 10; i++) {
|
||||
(void)dungeon_editor_v2_->Update();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, InvalidRoomId) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
(void)dungeon_editor_v2_->Load();
|
||||
|
||||
// Add invalid room ID (beyond 0x128)
|
||||
dungeon_editor_v2_->add_room(0x200);
|
||||
|
||||
// Update should handle gracefully
|
||||
auto status = dungeon_editor_v2_->Update();
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, NegativeRoomId) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
(void)dungeon_editor_v2_->Load();
|
||||
|
||||
// Add negative room ID
|
||||
dungeon_editor_v2_->add_room(-1);
|
||||
|
||||
// Update should handle gracefully
|
||||
auto status = dungeon_editor_v2_->Update();
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(DungeonEditorV2IntegrationTest, LoadTwice) {
|
||||
dungeon_editor_v2_->Initialize();
|
||||
|
||||
// Load twice
|
||||
auto status1 = dungeon_editor_v2_->Load();
|
||||
auto status2 = dungeon_editor_v2_->Load();
|
||||
|
||||
// Both should succeed
|
||||
EXPECT_TRUE(status1.ok());
|
||||
EXPECT_TRUE(status2.ok());
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
52
test/integration/dungeon_editor_v2_test.h
Normal file
52
test/integration/dungeon_editor_v2_test.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_V2_TEST_H
|
||||
#define YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_V2_TEST_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "app/editor/dungeon/dungeon_editor_v2.h"
|
||||
#include "app/rom.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Integration test framework for DungeonEditorV2
|
||||
*
|
||||
* Tests the simplified component delegation architecture
|
||||
*/
|
||||
class DungeonEditorV2IntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Use the real ROM (try multiple locations)
|
||||
rom_ = std::make_unique<Rom>();
|
||||
auto status = rom_->LoadFromFile("assets/zelda3.sfc");
|
||||
if (!status.ok()) {
|
||||
status = rom_->LoadFromFile("build/bin/zelda3.sfc");
|
||||
}
|
||||
if (!status.ok()) {
|
||||
status = rom_->LoadFromFile("zelda3.sfc");
|
||||
}
|
||||
ASSERT_TRUE(status.ok()) << "Could not load zelda3.sfc from any location";
|
||||
|
||||
// Create V2 editor with ROM
|
||||
dungeon_editor_v2_ = std::make_unique<editor::DungeonEditorV2>(rom_.get());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
dungeon_editor_v2_.reset();
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<editor::DungeonEditorV2> dungeon_editor_v2_;
|
||||
|
||||
static constexpr int kTestRoomId = 0x01;
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_TEST_INTEGRATION_DUNGEON_EDITOR_V2_TEST_H
|
||||
|
||||
207
test/integration/editor/editor_integration_test.cc
Normal file
207
test/integration/editor/editor_integration_test.cc
Normal file
@@ -0,0 +1,207 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
|
||||
#include "integration/editor/editor_integration_test.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "app/platform/window.h"
|
||||
#include "app/gui/core/style.h"
|
||||
#include "imgui/backends/imgui_impl_sdl2.h"
|
||||
#include "imgui/backends/imgui_impl_sdlrenderer2.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
#include "imgui_test_engine/imgui_te_context.h"
|
||||
#include "imgui_test_engine/imgui_te_engine.h"
|
||||
#include "imgui_test_engine/imgui_te_imconfig.h"
|
||||
#include "imgui_test_engine/imgui_te_ui.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
EditorIntegrationTest::EditorIntegrationTest()
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
: engine_(nullptr), show_demo_window_(true)
|
||||
#else
|
||||
|
||||
#endif
|
||||
{}
|
||||
|
||||
EditorIntegrationTest::~EditorIntegrationTest() {
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
if (engine_) {
|
||||
ImGuiTestEngine_Stop(engine_);
|
||||
ImGuiTestEngine_DestroyContext(engine_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::Initialize() {
|
||||
// Create renderer for test
|
||||
test_renderer_ = std::make_unique<gfx::SDL2Renderer>();
|
||||
RETURN_IF_ERROR(core::CreateWindow(window_, test_renderer_.get(), SDL_WINDOW_RESIZABLE));
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
// Initialize Test Engine
|
||||
engine_ = ImGuiTestEngine_CreateContext();
|
||||
ImGuiTestEngineIO& test_io = ImGuiTestEngine_GetIO(engine_);
|
||||
test_io.ConfigVerboseLevel = ImGuiTestVerboseLevel_Info;
|
||||
test_io.ConfigVerboseLevelOnError = ImGuiTestVerboseLevel_Debug;
|
||||
#endif
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
|
||||
// Initialize ImGui for SDL
|
||||
SDL_Renderer* sdl_renderer = static_cast<SDL_Renderer*>(test_renderer_->GetBackendRenderer());
|
||||
ImGui_ImplSDL2_InitForSDLRenderer(controller_.window(), sdl_renderer);
|
||||
ImGui_ImplSDLRenderer2_Init(sdl_renderer);
|
||||
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
// Register tests
|
||||
RegisterTests(engine_);
|
||||
ImGuiTestEngine_Start(engine_, ImGui::GetCurrentContext());
|
||||
#endif
|
||||
controller_.set_active(true);
|
||||
|
||||
// Set the default style
|
||||
yaze::gui::ColorsYaze();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
int EditorIntegrationTest::RunTest() {
|
||||
auto status = Initialize();
|
||||
if (!status.ok()) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Build a new ImGui frame
|
||||
ImGui_ImplSDLRenderer2_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
|
||||
while (controller_.IsActive()) {
|
||||
controller_.OnInput();
|
||||
auto status = Update();
|
||||
if (!status.ok()) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
controller_.DoRender();
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::Update() {
|
||||
ImGui::NewFrame();
|
||||
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
// Show test engine windows
|
||||
ImGuiTestEngine_ShowTestEngineWindows(engine_, &show_demo_window_);
|
||||
#endif
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
|
||||
// Helper methods for testing with a ROM
|
||||
absl::Status EditorIntegrationTest::LoadTestRom(const std::string& filename) {
|
||||
test_rom_ = std::make_unique<Rom>();
|
||||
return test_rom_->LoadFromFile(filename);
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::SaveTestRom(const std::string& filename) {
|
||||
if (!test_rom_) {
|
||||
return absl::FailedPreconditionError("No test ROM loaded");
|
||||
}
|
||||
Rom::SaveSettings settings;
|
||||
settings.backup = false;
|
||||
settings.save_new = false;
|
||||
settings.filename = filename;
|
||||
return test_rom_->SaveToFile(settings);
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorInitialize(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
editor->Initialize();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorLoad(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Load();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorSave(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Save();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorUpdate(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Update();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorCut(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Cut();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorCopy(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Copy();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorPaste(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Paste();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorUndo(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Undo();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorRedo(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Redo();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorFind(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Find();
|
||||
}
|
||||
|
||||
absl::Status EditorIntegrationTest::TestEditorClear(editor::Editor* editor) {
|
||||
if (!editor) {
|
||||
return absl::InternalError("Editor is null");
|
||||
}
|
||||
return editor->Clear();
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
91
test/integration/editor/editor_integration_test.h
Normal file
91
test/integration/editor/editor_integration_test.h
Normal file
@@ -0,0 +1,91 @@
|
||||
#ifndef YAZE_TEST_EDITOR_INTEGRATION_TEST_H
|
||||
#define YAZE_TEST_EDITOR_INTEGRATION_TEST_H
|
||||
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "app/editor/editor.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/controller.h"
|
||||
#include "app/platform/window.h"
|
||||
#include "app/gfx/backend/sdl2_renderer.h"
|
||||
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
#include "imgui_test_engine/imgui_te_context.h"
|
||||
#include "imgui_test_engine/imgui_te_engine.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @class EditorIntegrationTest
|
||||
* @brief Base class for editor integration tests
|
||||
*
|
||||
* This class provides common functionality for testing editors in the application.
|
||||
* It sets up the test environment and provides helper methods for ROM operations.
|
||||
*
|
||||
* For UI interaction testing, use the ImGui test engine API directly within your test functions:
|
||||
*
|
||||
* ImGuiTest* test = IM_REGISTER_TEST(engine, "test_suite", "test_name");
|
||||
* test->TestFunc = [](ImGuiTestContext* ctx) {
|
||||
* ctx->SetRef("Window Name");
|
||||
* ctx->ItemClick("Button Name");
|
||||
* };
|
||||
*/
|
||||
class EditorIntegrationTest {
|
||||
public:
|
||||
EditorIntegrationTest();
|
||||
~EditorIntegrationTest();
|
||||
|
||||
// Initialize the test environment
|
||||
absl::Status Initialize();
|
||||
|
||||
// Run the test
|
||||
int RunTest();
|
||||
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
// Register tests for a specific editor
|
||||
virtual void RegisterTests(ImGuiTestEngine* engine) = 0;
|
||||
#else
|
||||
// Default implementation when ImGui Test Engine is disabled
|
||||
virtual void RegisterTests(void* engine) {}
|
||||
#endif
|
||||
|
||||
// Update the test environment
|
||||
virtual absl::Status Update();
|
||||
|
||||
protected:
|
||||
|
||||
// Helper methods for testing with a ROM
|
||||
absl::Status LoadTestRom(const std::string& filename);
|
||||
absl::Status SaveTestRom(const std::string& filename);
|
||||
|
||||
// Helper methods for testing with a specific editor
|
||||
absl::Status TestEditorInitialize(editor::Editor* editor);
|
||||
absl::Status TestEditorLoad(editor::Editor* editor);
|
||||
absl::Status TestEditorSave(editor::Editor* editor);
|
||||
absl::Status TestEditorUpdate(editor::Editor* editor);
|
||||
absl::Status TestEditorCut(editor::Editor* editor);
|
||||
absl::Status TestEditorCopy(editor::Editor* editor);
|
||||
absl::Status TestEditorPaste(editor::Editor* editor);
|
||||
absl::Status TestEditorUndo(editor::Editor* editor);
|
||||
absl::Status TestEditorRedo(editor::Editor* editor);
|
||||
absl::Status TestEditorFind(editor::Editor* editor);
|
||||
absl::Status TestEditorClear(editor::Editor* editor);
|
||||
|
||||
private:
|
||||
Controller controller_;
|
||||
#ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||
ImGuiTestEngine* engine_;
|
||||
bool show_demo_window_;
|
||||
#endif
|
||||
std::unique_ptr<Rom> test_rom_;
|
||||
core::Window window_;
|
||||
std::unique_ptr<gfx::SDL2Renderer> test_renderer_;
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_TEST_EDITOR_INTEGRATION_TEST_H
|
||||
309
test/integration/editor/tile16_editor_test.cc
Normal file
309
test/integration/editor/tile16_editor_test.cc
Normal file
@@ -0,0 +1,309 @@
|
||||
#include "app/editor/overworld/tile16_editor.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "app/rom.h"
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gfx/backend/sdl2_renderer.h"
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gfx/render/tilemap.h"
|
||||
#include "zelda3/overworld/overworld.h"
|
||||
#include "app/platform/window.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace editor {
|
||||
namespace test {
|
||||
|
||||
class Tile16EditorIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
static void SetUpTestSuite() {
|
||||
// Initialize SDL and rendering system once for all tests
|
||||
InitializeTestEnvironment();
|
||||
}
|
||||
|
||||
static void TearDownTestSuite() {
|
||||
// Clean up SDL
|
||||
if (window_initialized_) {
|
||||
auto shutdown_result = core::ShutdownWindow(test_window_);
|
||||
(void)shutdown_result; // Suppress unused variable warning
|
||||
window_initialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!window_initialized_) {
|
||||
GTEST_SKIP() << "Failed to initialize graphics system";
|
||||
}
|
||||
|
||||
// Load the test ROM
|
||||
rom_ = std::make_unique<Rom>();
|
||||
auto load_result = rom_->LoadFromFile(YAZE_TEST_ROM_PATH);
|
||||
ASSERT_TRUE(load_result.ok()) << "Failed to load test ROM: " << load_result.message();
|
||||
|
||||
// Load overworld data
|
||||
overworld_ = std::make_unique<zelda3::Overworld>(rom_.get());
|
||||
auto overworld_load_result = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(overworld_load_result.ok()) << "Failed to load overworld: " << overworld_load_result.message();
|
||||
|
||||
// Create tile16 blockset
|
||||
auto tile16_data = overworld_->tile16_blockset_data();
|
||||
auto palette = overworld_->current_area_palette();
|
||||
|
||||
tile16_blockset_ = std::make_unique<gfx::Tilemap>(
|
||||
gfx::CreateTilemap(nullptr, tile16_data, 0x80, 0x2000, 16,
|
||||
zelda3::kNumTile16Individual, palette));
|
||||
|
||||
// Create graphics bitmap
|
||||
current_gfx_bmp_ = std::make_unique<gfx::Bitmap>();
|
||||
current_gfx_bmp_->Create(0x80, 512, 0x40, overworld_->current_graphics());
|
||||
current_gfx_bmp_->SetPalette(palette);
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, current_gfx_bmp_.get());
|
||||
|
||||
// Create tile16 blockset bitmap
|
||||
tile16_blockset_bmp_ = std::make_unique<gfx::Bitmap>();
|
||||
tile16_blockset_bmp_->Create(0x80, 0x2000, 0x08, tile16_data);
|
||||
tile16_blockset_bmp_->SetPalette(palette);
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::CREATE, tile16_blockset_bmp_.get());
|
||||
|
||||
// Initialize the tile16 editor
|
||||
editor_ = std::make_unique<Tile16Editor>(rom_.get(), tile16_blockset_.get());
|
||||
auto init_result = editor_->Initialize(*tile16_blockset_bmp_, *current_gfx_bmp_,
|
||||
*overworld_->mutable_all_tiles_types());
|
||||
ASSERT_TRUE(init_result.ok()) << "Failed to initialize editor: " << init_result.message();
|
||||
|
||||
rom_loaded_ = true;
|
||||
#else
|
||||
// Fallback for non-ROM tests
|
||||
rom_ = std::make_unique<Rom>();
|
||||
tilemap_ = std::make_unique<gfx::Tilemap>();
|
||||
editor_ = std::make_unique<Tile16Editor>(rom_.get(), tilemap_.get());
|
||||
rom_loaded_ = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
protected:
|
||||
static void InitializeTestEnvironment() {
|
||||
// Create renderer for test
|
||||
test_renderer_ = std::make_unique<gfx::SDL2Renderer>();
|
||||
auto window_result = core::CreateWindow(test_window_, test_renderer_.get(), SDL_WINDOW_HIDDEN);
|
||||
if (window_result.ok()) {
|
||||
window_initialized_ = true;
|
||||
} else {
|
||||
window_initialized_ = false;
|
||||
// Log the error but don't fail test setup
|
||||
std::cerr << "Failed to initialize test window: " << window_result.message() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
static bool window_initialized_;
|
||||
static core::Window test_window_;
|
||||
static std::unique_ptr<gfx::SDL2Renderer> test_renderer_;
|
||||
|
||||
bool rom_loaded_ = false;
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<gfx::Tilemap> tilemap_;
|
||||
std::unique_ptr<gfx::Tilemap> tile16_blockset_;
|
||||
std::unique_ptr<gfx::Bitmap> current_gfx_bmp_;
|
||||
std::unique_ptr<gfx::Bitmap> tile16_blockset_bmp_;
|
||||
std::unique_ptr<zelda3::Overworld> overworld_;
|
||||
std::unique_ptr<Tile16Editor> editor_;
|
||||
};
|
||||
|
||||
// Static member definitions
|
||||
bool Tile16EditorIntegrationTest::window_initialized_ = false;
|
||||
core::Window Tile16EditorIntegrationTest::test_window_;
|
||||
std::unique_ptr<gfx::SDL2Renderer> Tile16EditorIntegrationTest::test_renderer_;
|
||||
|
||||
// Basic validation tests (no ROM required)
|
||||
TEST_F(Tile16EditorIntegrationTest, BasicValidation) {
|
||||
// Test with invalid tile ID
|
||||
EXPECT_FALSE(editor_->IsTile16Valid(-1));
|
||||
EXPECT_FALSE(editor_->IsTile16Valid(9999));
|
||||
|
||||
// Test scratch space operations with invalid slots
|
||||
auto save_invalid = editor_->SaveTile16ToScratchSpace(-1);
|
||||
EXPECT_FALSE(save_invalid.ok());
|
||||
EXPECT_EQ(save_invalid.code(), absl::StatusCode::kInvalidArgument);
|
||||
|
||||
auto load_invalid = editor_->LoadTile16FromScratchSpace(5);
|
||||
EXPECT_FALSE(load_invalid.ok());
|
||||
EXPECT_EQ(load_invalid.code(), absl::StatusCode::kInvalidArgument);
|
||||
|
||||
// Test valid scratch space clearing
|
||||
auto clear_valid = editor_->ClearScratchSpace(0);
|
||||
EXPECT_TRUE(clear_valid.ok());
|
||||
}
|
||||
|
||||
// ROM-dependent tests
|
||||
TEST_F(Tile16EditorIntegrationTest, ValidateTile16DataWithROM) {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!rom_loaded_) {
|
||||
GTEST_SKIP() << "ROM not loaded, skipping integration test";
|
||||
}
|
||||
|
||||
// Test validation with properly loaded ROM
|
||||
auto status = editor_->ValidateTile16Data();
|
||||
EXPECT_TRUE(status.ok()) << "Validation failed: " << status.message();
|
||||
#else
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(Tile16EditorIntegrationTest, SetCurrentTileWithROM) {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!rom_loaded_) {
|
||||
GTEST_SKIP() << "ROM not loaded, skipping integration test";
|
||||
}
|
||||
|
||||
// Test setting a valid tile
|
||||
auto valid_tile_result = editor_->SetCurrentTile(0);
|
||||
EXPECT_TRUE(valid_tile_result.ok()) << "Failed to set tile 0: " << valid_tile_result.message();
|
||||
|
||||
auto valid_tile_result2 = editor_->SetCurrentTile(100);
|
||||
EXPECT_TRUE(valid_tile_result2.ok()) << "Failed to set tile 100: " << valid_tile_result2.message();
|
||||
|
||||
// Test invalid ranges still fail
|
||||
auto invalid_low = editor_->SetCurrentTile(-1);
|
||||
EXPECT_FALSE(invalid_low.ok());
|
||||
EXPECT_EQ(invalid_low.code(), absl::StatusCode::kOutOfRange);
|
||||
|
||||
auto invalid_high = editor_->SetCurrentTile(10000);
|
||||
EXPECT_FALSE(invalid_high.ok());
|
||||
EXPECT_EQ(invalid_high.code(), absl::StatusCode::kOutOfRange);
|
||||
#else
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(Tile16EditorIntegrationTest, FlipOperationsWithROM) {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!rom_loaded_) {
|
||||
GTEST_SKIP() << "ROM not loaded, skipping integration test";
|
||||
}
|
||||
|
||||
// Set a valid tile first
|
||||
auto set_result = editor_->SetCurrentTile(1);
|
||||
ASSERT_TRUE(set_result.ok()) << "Failed to set initial tile: " << set_result.message();
|
||||
|
||||
// Test flip operations
|
||||
auto flip_h_result = editor_->FlipTile16Horizontal();
|
||||
EXPECT_TRUE(flip_h_result.ok()) << "Horizontal flip failed: " << flip_h_result.message();
|
||||
|
||||
auto flip_v_result = editor_->FlipTile16Vertical();
|
||||
EXPECT_TRUE(flip_v_result.ok()) << "Vertical flip failed: " << flip_v_result.message();
|
||||
|
||||
auto rotate_result = editor_->RotateTile16();
|
||||
EXPECT_TRUE(rotate_result.ok()) << "Rotation failed: " << rotate_result.message();
|
||||
#else
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(Tile16EditorIntegrationTest, UndoRedoWithROM) {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!rom_loaded_) {
|
||||
GTEST_SKIP() << "ROM not loaded, skipping integration test";
|
||||
}
|
||||
|
||||
// Set a tile and perform an operation to create undo state
|
||||
auto set_result = editor_->SetCurrentTile(1);
|
||||
ASSERT_TRUE(set_result.ok());
|
||||
|
||||
auto clear_result = editor_->ClearTile16();
|
||||
ASSERT_TRUE(clear_result.ok()) << "Clear operation failed: " << clear_result.message();
|
||||
|
||||
// Test undo
|
||||
auto undo_result = editor_->Undo();
|
||||
EXPECT_TRUE(undo_result.ok()) << "Undo failed: " << undo_result.message();
|
||||
|
||||
// Test redo
|
||||
auto redo_result = editor_->Redo();
|
||||
EXPECT_TRUE(redo_result.ok()) << "Redo failed: " << redo_result.message();
|
||||
#else
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(Tile16EditorIntegrationTest, PaletteOperationsWithROM) {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!rom_loaded_) {
|
||||
GTEST_SKIP() << "ROM not loaded, skipping integration test";
|
||||
}
|
||||
|
||||
// Test palette cycling
|
||||
auto cycle_forward = editor_->CyclePalette(true);
|
||||
EXPECT_TRUE(cycle_forward.ok()) << "Palette cycle forward failed: " << cycle_forward.message();
|
||||
|
||||
auto cycle_backward = editor_->CyclePalette(false);
|
||||
EXPECT_TRUE(cycle_backward.ok()) << "Palette cycle backward failed: " << cycle_backward.message();
|
||||
|
||||
// Test valid palette preview
|
||||
auto valid_palette = editor_->PreviewPaletteChange(3);
|
||||
EXPECT_TRUE(valid_palette.ok()) << "Palette preview failed: " << valid_palette.message();
|
||||
|
||||
// Test invalid palette
|
||||
auto invalid_palette = editor_->PreviewPaletteChange(10);
|
||||
EXPECT_FALSE(invalid_palette.ok());
|
||||
EXPECT_EQ(invalid_palette.code(), absl::StatusCode::kInvalidArgument);
|
||||
#else
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(Tile16EditorIntegrationTest, CopyPasteOperationsWithROM) {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!rom_loaded_) {
|
||||
GTEST_SKIP() << "ROM not loaded, skipping integration test";
|
||||
}
|
||||
|
||||
// Set a tile first
|
||||
auto set_result = editor_->SetCurrentTile(10);
|
||||
ASSERT_TRUE(set_result.ok());
|
||||
|
||||
// Test copy operation
|
||||
auto copy_result = editor_->CopyTile16ToClipboard(10);
|
||||
EXPECT_TRUE(copy_result.ok()) << "Copy failed: " << copy_result.message();
|
||||
|
||||
// Test paste operation
|
||||
auto paste_result = editor_->PasteTile16FromClipboard();
|
||||
EXPECT_TRUE(paste_result.ok()) << "Paste failed: " << paste_result.message();
|
||||
#else
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(Tile16EditorIntegrationTest, ScratchSpaceWithROM) {
|
||||
#ifdef YAZE_ENABLE_ROM_TESTS
|
||||
if (!rom_loaded_) {
|
||||
GTEST_SKIP() << "ROM not loaded, skipping integration test";
|
||||
}
|
||||
|
||||
// Set a tile first
|
||||
auto set_result = editor_->SetCurrentTile(15);
|
||||
ASSERT_TRUE(set_result.ok());
|
||||
|
||||
// Test scratch space save
|
||||
auto save_result = editor_->SaveTile16ToScratchSpace(0);
|
||||
EXPECT_TRUE(save_result.ok()) << "Scratch save failed: " << save_result.message();
|
||||
|
||||
// Test scratch space load
|
||||
auto load_result = editor_->LoadTile16FromScratchSpace(0);
|
||||
EXPECT_TRUE(load_result.ok()) << "Scratch load failed: " << load_result.message();
|
||||
|
||||
// Test scratch space clear
|
||||
auto clear_result = editor_->ClearScratchSpace(0);
|
||||
EXPECT_TRUE(clear_result.ok()) << "Scratch clear failed: " << clear_result.message();
|
||||
#else
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace editor
|
||||
} // namespace yaze
|
||||
360
test/integration/palette_manager_test.cc
Normal file
360
test/integration/palette_manager_test.cc
Normal file
@@ -0,0 +1,360 @@
|
||||
#include "app/gfx/util/palette_manager.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gfx/types/snes_color.h"
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
namespace {
|
||||
|
||||
// Test fixture for PaletteManager integration tests
|
||||
class PaletteManagerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// PaletteManager is a singleton, so we need to reset it between tests
|
||||
// Note: In a real scenario, we'd need a way to reset the singleton
|
||||
// For now, we'll work with the existing instance
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up any test state
|
||||
PaletteManager::Get().ClearHistory();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Initialization Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, InitializationState) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Before initialization, should not be initialized
|
||||
// Note: This might fail if other tests have already initialized it
|
||||
// In production, we'd need a Reset() method for testing
|
||||
|
||||
// After initialization with null ROM, should handle gracefully
|
||||
manager.Initialize(nullptr);
|
||||
EXPECT_FALSE(manager.IsInitialized());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, HasNoUnsavedChangesInitially) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Should have no unsaved changes initially
|
||||
EXPECT_FALSE(manager.HasUnsavedChanges());
|
||||
EXPECT_EQ(manager.GetModifiedColorCount(), 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dirty Tracking Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, TracksModifiedGroups) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Initially, no groups should be modified
|
||||
auto modified_groups = manager.GetModifiedGroups();
|
||||
EXPECT_TRUE(modified_groups.empty());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, GetModifiedColorCount) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Initially, no colors modified
|
||||
EXPECT_EQ(manager.GetModifiedColorCount(), 0);
|
||||
|
||||
// After initialization and making changes, count should increase
|
||||
// (This would require a valid ROM to test properly)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Undo/Redo Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, UndoRedoInitialState) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Initially, should not be able to undo or redo
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
EXPECT_EQ(manager.GetUndoStackSize(), 0);
|
||||
EXPECT_EQ(manager.GetRedoStackSize(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, ClearHistoryResetsStacks) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Clear history should reset both stacks
|
||||
manager.ClearHistory();
|
||||
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
EXPECT_EQ(manager.GetUndoStackSize(), 0);
|
||||
EXPECT_EQ(manager.GetRedoStackSize(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, UndoWithoutChangesIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
|
||||
// Should not crash
|
||||
manager.Undo();
|
||||
|
||||
EXPECT_FALSE(manager.CanUndo());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, RedoWithoutUndoIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
|
||||
// Should not crash
|
||||
manager.Redo();
|
||||
|
||||
EXPECT_FALSE(manager.CanRedo());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Operations Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, BatchModeTracking) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
|
||||
manager.BeginBatch();
|
||||
EXPECT_TRUE(manager.InBatch());
|
||||
|
||||
manager.EndBatch();
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, NestedBatchOperations) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
|
||||
manager.BeginBatch();
|
||||
EXPECT_TRUE(manager.InBatch());
|
||||
|
||||
manager.BeginBatch(); // Nested
|
||||
EXPECT_TRUE(manager.InBatch());
|
||||
|
||||
manager.EndBatch();
|
||||
EXPECT_TRUE(manager.InBatch()); // Still in batch (outer)
|
||||
|
||||
manager.EndBatch();
|
||||
EXPECT_FALSE(manager.InBatch()); // Now out of batch
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, EndBatchWithoutBeginIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
|
||||
// Should not crash
|
||||
manager.EndBatch();
|
||||
|
||||
EXPECT_FALSE(manager.InBatch());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Change Notification Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, RegisterAndUnregisterListener) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
int callback_count = 0;
|
||||
auto callback = [&callback_count](const PaletteChangeEvent& event) {
|
||||
callback_count++;
|
||||
};
|
||||
|
||||
// Register listener
|
||||
int id = manager.RegisterChangeListener(callback);
|
||||
EXPECT_GT(id, 0);
|
||||
|
||||
// Unregister listener
|
||||
manager.UnregisterChangeListener(id);
|
||||
|
||||
// After unregistering, callback should not be called
|
||||
// (Would need to trigger an event to test this properly)
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, MultipleListeners) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
int callback1_count = 0;
|
||||
int callback2_count = 0;
|
||||
|
||||
auto callback1 = [&callback1_count](const PaletteChangeEvent& event) {
|
||||
callback1_count++;
|
||||
};
|
||||
|
||||
auto callback2 = [&callback2_count](const PaletteChangeEvent& event) {
|
||||
callback2_count++;
|
||||
};
|
||||
|
||||
int id1 = manager.RegisterChangeListener(callback1);
|
||||
int id2 = manager.RegisterChangeListener(callback2);
|
||||
|
||||
EXPECT_NE(id1, id2);
|
||||
|
||||
// Clean up
|
||||
manager.UnregisterChangeListener(id1);
|
||||
manager.UnregisterChangeListener(id2);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Color Query Tests (without ROM)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, GetColorWithoutInitialization) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Getting color without initialization should return default color
|
||||
SnesColor color = manager.GetColor("ow_main", 0, 0);
|
||||
|
||||
// Default SnesColor should have zero values
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, SetColorWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
SnesColor new_color(0x7FFF);
|
||||
auto status = manager.SetColor("ow_main", 0, 0, new_color);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, ResetColorWithoutInitializationReturnsError) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.ResetColor("ow_main", 0, 0);
|
||||
|
||||
// Should return an error or default color
|
||||
// Exact behavior depends on implementation
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, ResetPaletteWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.ResetPalette("ow_main", 0);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Save/Discard Tests (without ROM)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, SaveGroupWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.SaveGroup("ow_main");
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, SaveAllWithoutInitializationFails) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
auto status = manager.SaveAllToRom();
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, DiscardGroupWithoutInitializationIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Should not crash
|
||||
manager.DiscardGroup("ow_main");
|
||||
|
||||
// No unsaved changes
|
||||
EXPECT_FALSE(manager.HasUnsavedChanges());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, DiscardAllWithoutInitializationIsNoOp) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
// Should not crash
|
||||
manager.DiscardAllChanges();
|
||||
|
||||
// No unsaved changes
|
||||
EXPECT_FALSE(manager.HasUnsavedChanges());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Group Modification Query Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, IsGroupModifiedInitiallyFalse) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsGroupModified("ow_main"));
|
||||
EXPECT_FALSE(manager.IsGroupModified("dungeon_main"));
|
||||
EXPECT_FALSE(manager.IsGroupModified("global_sprites"));
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, IsPaletteModifiedInitiallyFalse) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsPaletteModified("ow_main", 0));
|
||||
EXPECT_FALSE(manager.IsPaletteModified("ow_main", 5));
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, IsColorModifiedInitiallyFalse) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsColorModified("ow_main", 0, 0));
|
||||
EXPECT_FALSE(manager.IsColorModified("ow_main", 0, 7));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Invalid Input Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(PaletteManagerTest, SetColorInvalidGroupName) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
SnesColor color(0x7FFF);
|
||||
auto status = manager.SetColor("invalid_group", 0, 0, color);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, GetColorInvalidGroupName) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
SnesColor color = manager.GetColor("invalid_group", 0, 0);
|
||||
|
||||
// Should return default color
|
||||
auto rgb = color.rgb();
|
||||
EXPECT_FLOAT_EQ(rgb.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(rgb.z, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(PaletteManagerTest, IsGroupModifiedInvalidGroupName) {
|
||||
auto& manager = PaletteManager::Get();
|
||||
|
||||
EXPECT_FALSE(manager.IsGroupModified("invalid_group"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace gfx
|
||||
} // namespace yaze
|
||||
@@ -0,0 +1,578 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <chrono>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "zelda3/dungeon/dungeon_editor_system.h"
|
||||
#include "zelda3/dungeon/dungeon_object_editor.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
class DungeonEditorSystemIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Skip tests on Linux for automated github builds
|
||||
#if defined(__linux__)
|
||||
GTEST_SKIP();
|
||||
#endif
|
||||
|
||||
// Use the real ROM from build directory
|
||||
rom_path_ = "build/bin/zelda3.sfc";
|
||||
|
||||
// Load ROM
|
||||
rom_ = std::make_unique<Rom>();
|
||||
ASSERT_TRUE(rom_->LoadFromFile(rom_path_).ok());
|
||||
|
||||
// Initialize dungeon editor system
|
||||
dungeon_editor_system_ = std::make_unique<DungeonEditorSystem>(rom_.get());
|
||||
ASSERT_TRUE(dungeon_editor_system_->Initialize().ok());
|
||||
|
||||
// Load test room data
|
||||
ASSERT_TRUE(LoadTestRoomData().ok());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
dungeon_editor_system_.reset();
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
absl::Status LoadTestRoomData() {
|
||||
// Load representative rooms for testing
|
||||
test_rooms_ = {0x0000, 0x0001, 0x0002, 0x0010, 0x0012, 0x0020};
|
||||
|
||||
for (int room_id : test_rooms_) {
|
||||
auto room_result = dungeon_editor_system_->GetRoom(room_id);
|
||||
if (room_result.ok()) {
|
||||
rooms_[room_id] = room_result.value();
|
||||
std::cout << "Loaded room 0x" << std::hex << room_id << std::dec << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string rom_path_;
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<DungeonEditorSystem> dungeon_editor_system_;
|
||||
|
||||
std::vector<int> test_rooms_;
|
||||
std::map<int, Room> rooms_;
|
||||
};
|
||||
|
||||
// Test basic dungeon editor system initialization
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, BasicInitialization) {
|
||||
EXPECT_NE(dungeon_editor_system_, nullptr);
|
||||
EXPECT_EQ(dungeon_editor_system_->GetROM(), rom_.get());
|
||||
EXPECT_FALSE(dungeon_editor_system_->IsDirty());
|
||||
}
|
||||
|
||||
// Test room loading and management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, RoomLoadingAndManagement) {
|
||||
// Test loading a specific room
|
||||
auto room_result = dungeon_editor_system_->GetRoom(0x0000);
|
||||
ASSERT_TRUE(room_result.ok()) << "Failed to load room 0x0000: " << room_result.status().message();
|
||||
|
||||
const auto& room = room_result.value();
|
||||
// Note: room_id_ is private, so we can't directly access it in tests
|
||||
|
||||
// Test setting current room
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
|
||||
EXPECT_EQ(dungeon_editor_system_->GetCurrentRoom(), 0x0000);
|
||||
|
||||
// Test loading another room
|
||||
auto room2_result = dungeon_editor_system_->GetRoom(0x0001);
|
||||
ASSERT_TRUE(room2_result.ok()) << "Failed to load room 0x0001: " << room2_result.status().message();
|
||||
|
||||
const auto& room2 = room2_result.value();
|
||||
// Note: room_id_ is private, so we can't directly access it in tests
|
||||
}
|
||||
|
||||
// Test object editor integration
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, ObjectEditorIntegration) {
|
||||
// Get object editor from system
|
||||
auto object_editor = dungeon_editor_system_->GetObjectEditor();
|
||||
ASSERT_NE(object_editor, nullptr);
|
||||
|
||||
// Set current room
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
|
||||
|
||||
// Test object insertion
|
||||
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
|
||||
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
|
||||
|
||||
// Verify objects were added
|
||||
EXPECT_EQ(object_editor->GetObjectCount(), 2);
|
||||
|
||||
// Test object selection
|
||||
ASSERT_TRUE(object_editor->SelectObject(5 * 16, 5 * 16).ok());
|
||||
auto selection = object_editor->GetSelection();
|
||||
EXPECT_EQ(selection.selected_objects.size(), 1);
|
||||
|
||||
// Test object deletion
|
||||
ASSERT_TRUE(object_editor->DeleteSelectedObjects().ok());
|
||||
EXPECT_EQ(object_editor->GetObjectCount(), 1);
|
||||
}
|
||||
|
||||
// Test sprite management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, SpriteManagement) {
|
||||
// Set current room
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
|
||||
|
||||
// Create sprite data
|
||||
DungeonEditorSystem::SpriteData sprite_data;
|
||||
sprite_data.sprite_id = 1;
|
||||
sprite_data.name = "Test Sprite";
|
||||
sprite_data.type = DungeonEditorSystem::SpriteType::kEnemy;
|
||||
sprite_data.x = 100;
|
||||
sprite_data.y = 100;
|
||||
sprite_data.layer = 0;
|
||||
sprite_data.is_active = true;
|
||||
|
||||
// Add sprite
|
||||
ASSERT_TRUE(dungeon_editor_system_->AddSprite(sprite_data).ok());
|
||||
|
||||
// Get sprites for room
|
||||
auto sprites_result = dungeon_editor_system_->GetSpritesByRoom(0x0000);
|
||||
ASSERT_TRUE(sprites_result.ok()) << "Failed to get sprites: " << sprites_result.status().message();
|
||||
|
||||
const auto& sprites = sprites_result.value();
|
||||
EXPECT_EQ(sprites.size(), 1);
|
||||
EXPECT_EQ(sprites[0].sprite_id, 1);
|
||||
EXPECT_EQ(sprites[0].name, "Test Sprite");
|
||||
|
||||
// Update sprite
|
||||
sprite_data.x = 150;
|
||||
ASSERT_TRUE(dungeon_editor_system_->UpdateSprite(1, sprite_data).ok());
|
||||
|
||||
// Get updated sprite
|
||||
auto sprite_result = dungeon_editor_system_->GetSprite(1);
|
||||
ASSERT_TRUE(sprite_result.ok());
|
||||
EXPECT_EQ(sprite_result.value().x, 150);
|
||||
|
||||
// Remove sprite
|
||||
ASSERT_TRUE(dungeon_editor_system_->RemoveSprite(1).ok());
|
||||
|
||||
// Verify sprite was removed
|
||||
auto sprites_after = dungeon_editor_system_->GetSpritesByRoom(0x0000);
|
||||
ASSERT_TRUE(sprites_after.ok());
|
||||
EXPECT_EQ(sprites_after.value().size(), 0);
|
||||
}
|
||||
|
||||
// Test item management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, ItemManagement) {
|
||||
// Set current room
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
|
||||
|
||||
// Create item data
|
||||
DungeonEditorSystem::ItemData item_data;
|
||||
item_data.item_id = 1;
|
||||
item_data.type = DungeonEditorSystem::ItemType::kKey;
|
||||
item_data.name = "Small Key";
|
||||
item_data.x = 200;
|
||||
item_data.y = 200;
|
||||
item_data.room_id = 0x0000;
|
||||
item_data.is_hidden = false;
|
||||
|
||||
// Add item
|
||||
ASSERT_TRUE(dungeon_editor_system_->AddItem(item_data).ok());
|
||||
|
||||
// Get items for room
|
||||
auto items_result = dungeon_editor_system_->GetItemsByRoom(0x0000);
|
||||
ASSERT_TRUE(items_result.ok()) << "Failed to get items: " << items_result.status().message();
|
||||
|
||||
const auto& items = items_result.value();
|
||||
EXPECT_EQ(items.size(), 1);
|
||||
EXPECT_EQ(items[0].item_id, 1);
|
||||
EXPECT_EQ(items[0].name, "Small Key");
|
||||
|
||||
// Update item
|
||||
item_data.is_hidden = true;
|
||||
ASSERT_TRUE(dungeon_editor_system_->UpdateItem(1, item_data).ok());
|
||||
|
||||
// Get updated item
|
||||
auto item_result = dungeon_editor_system_->GetItem(1);
|
||||
ASSERT_TRUE(item_result.ok());
|
||||
EXPECT_TRUE(item_result.value().is_hidden);
|
||||
|
||||
// Remove item
|
||||
ASSERT_TRUE(dungeon_editor_system_->RemoveItem(1).ok());
|
||||
|
||||
// Verify item was removed
|
||||
auto items_after = dungeon_editor_system_->GetItemsByRoom(0x0000);
|
||||
ASSERT_TRUE(items_after.ok());
|
||||
EXPECT_EQ(items_after.value().size(), 0);
|
||||
}
|
||||
|
||||
// Test entrance management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, EntranceManagement) {
|
||||
// Create entrance data
|
||||
DungeonEditorSystem::EntranceData entrance_data;
|
||||
entrance_data.entrance_id = 1;
|
||||
entrance_data.type = DungeonEditorSystem::EntranceType::kDoor;
|
||||
entrance_data.name = "Test Entrance";
|
||||
entrance_data.source_room_id = 0x0000;
|
||||
entrance_data.target_room_id = 0x0001;
|
||||
entrance_data.source_x = 100;
|
||||
entrance_data.source_y = 100;
|
||||
entrance_data.target_x = 200;
|
||||
entrance_data.target_y = 200;
|
||||
entrance_data.is_bidirectional = true;
|
||||
|
||||
// Add entrance
|
||||
ASSERT_TRUE(dungeon_editor_system_->AddEntrance(entrance_data).ok());
|
||||
|
||||
// Get entrances for room
|
||||
auto entrances_result = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
|
||||
ASSERT_TRUE(entrances_result.ok()) << "Failed to get entrances: " << entrances_result.status().message();
|
||||
|
||||
const auto& entrances = entrances_result.value();
|
||||
EXPECT_EQ(entrances.size(), 1);
|
||||
EXPECT_EQ(entrances[0].name, "Test Entrance");
|
||||
|
||||
// Store the entrance ID for later removal
|
||||
int entrance_id = entrances[0].entrance_id;
|
||||
|
||||
// Test room connection
|
||||
ASSERT_TRUE(dungeon_editor_system_->ConnectRooms(0x0000, 0x0001, 150, 150, 250, 250).ok());
|
||||
|
||||
// Get updated entrances
|
||||
auto entrances_after = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
|
||||
ASSERT_TRUE(entrances_after.ok());
|
||||
EXPECT_GE(entrances_after.value().size(), 1);
|
||||
|
||||
// Remove entrance using the correct ID
|
||||
ASSERT_TRUE(dungeon_editor_system_->RemoveEntrance(entrance_id).ok());
|
||||
|
||||
// Verify entrance was removed
|
||||
auto entrances_final = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
|
||||
ASSERT_TRUE(entrances_final.ok());
|
||||
EXPECT_EQ(entrances_final.value().size(), 0);
|
||||
}
|
||||
|
||||
// Test door management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, DoorManagement) {
|
||||
// Create door data
|
||||
DungeonEditorSystem::DoorData door_data;
|
||||
door_data.door_id = 1;
|
||||
door_data.name = "Test Door";
|
||||
door_data.room_id = 0x0000;
|
||||
door_data.x = 100;
|
||||
door_data.y = 100;
|
||||
door_data.direction = 0; // up
|
||||
door_data.target_room_id = 0x0001;
|
||||
door_data.target_x = 200;
|
||||
door_data.target_y = 200;
|
||||
door_data.requires_key = false;
|
||||
door_data.key_type = 0;
|
||||
door_data.is_locked = false;
|
||||
|
||||
// Add door
|
||||
ASSERT_TRUE(dungeon_editor_system_->AddDoor(door_data).ok());
|
||||
|
||||
// Get doors for room
|
||||
auto doors_result = dungeon_editor_system_->GetDoorsByRoom(0x0000);
|
||||
ASSERT_TRUE(doors_result.ok()) << "Failed to get doors: " << doors_result.status().message();
|
||||
|
||||
const auto& doors = doors_result.value();
|
||||
EXPECT_EQ(doors.size(), 1);
|
||||
EXPECT_EQ(doors[0].door_id, 1);
|
||||
EXPECT_EQ(doors[0].name, "Test Door");
|
||||
|
||||
// Update door
|
||||
door_data.is_locked = true;
|
||||
ASSERT_TRUE(dungeon_editor_system_->UpdateDoor(1, door_data).ok());
|
||||
|
||||
// Get updated door
|
||||
auto door_result = dungeon_editor_system_->GetDoor(1);
|
||||
ASSERT_TRUE(door_result.ok());
|
||||
EXPECT_TRUE(door_result.value().is_locked);
|
||||
|
||||
// Set door key requirement
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetDoorKeyRequirement(1, true, 1).ok());
|
||||
|
||||
// Get door with key requirement
|
||||
auto door_with_key = dungeon_editor_system_->GetDoor(1);
|
||||
ASSERT_TRUE(door_with_key.ok());
|
||||
EXPECT_TRUE(door_with_key.value().requires_key);
|
||||
EXPECT_EQ(door_with_key.value().key_type, 1);
|
||||
|
||||
// Remove door
|
||||
ASSERT_TRUE(dungeon_editor_system_->RemoveDoor(1).ok());
|
||||
|
||||
// Verify door was removed
|
||||
auto doors_after = dungeon_editor_system_->GetDoorsByRoom(0x0000);
|
||||
ASSERT_TRUE(doors_after.ok());
|
||||
EXPECT_EQ(doors_after.value().size(), 0);
|
||||
}
|
||||
|
||||
// Test chest management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, ChestManagement) {
|
||||
// Create chest data
|
||||
DungeonEditorSystem::ChestData chest_data;
|
||||
chest_data.chest_id = 1;
|
||||
chest_data.room_id = 0x0000;
|
||||
chest_data.x = 100;
|
||||
chest_data.y = 100;
|
||||
chest_data.is_big_chest = false;
|
||||
chest_data.item_id = 10;
|
||||
chest_data.item_quantity = 1;
|
||||
chest_data.is_opened = false;
|
||||
|
||||
// Add chest
|
||||
ASSERT_TRUE(dungeon_editor_system_->AddChest(chest_data).ok());
|
||||
|
||||
// Get chests for room
|
||||
auto chests_result = dungeon_editor_system_->GetChestsByRoom(0x0000);
|
||||
ASSERT_TRUE(chests_result.ok()) << "Failed to get chests: " << chests_result.status().message();
|
||||
|
||||
const auto& chests = chests_result.value();
|
||||
EXPECT_EQ(chests.size(), 1);
|
||||
EXPECT_EQ(chests[0].chest_id, 1);
|
||||
EXPECT_EQ(chests[0].item_id, 10);
|
||||
|
||||
// Update chest item
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetChestItem(1, 20, 5).ok());
|
||||
|
||||
// Get updated chest
|
||||
auto chest_result = dungeon_editor_system_->GetChest(1);
|
||||
ASSERT_TRUE(chest_result.ok());
|
||||
EXPECT_EQ(chest_result.value().item_id, 20);
|
||||
EXPECT_EQ(chest_result.value().item_quantity, 5);
|
||||
|
||||
// Set chest as opened
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetChestOpened(1, true).ok());
|
||||
|
||||
// Get opened chest
|
||||
auto opened_chest = dungeon_editor_system_->GetChest(1);
|
||||
ASSERT_TRUE(opened_chest.ok());
|
||||
EXPECT_TRUE(opened_chest.value().is_opened);
|
||||
|
||||
// Remove chest
|
||||
ASSERT_TRUE(dungeon_editor_system_->RemoveChest(1).ok());
|
||||
|
||||
// Verify chest was removed
|
||||
auto chests_after = dungeon_editor_system_->GetChestsByRoom(0x0000);
|
||||
ASSERT_TRUE(chests_after.ok());
|
||||
EXPECT_EQ(chests_after.value().size(), 0);
|
||||
}
|
||||
|
||||
// Test room properties management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, RoomPropertiesManagement) {
|
||||
// Create room properties
|
||||
DungeonEditorSystem::RoomProperties properties;
|
||||
properties.room_id = 0x0000;
|
||||
properties.name = "Test Room";
|
||||
properties.description = "A test room for integration testing";
|
||||
properties.dungeon_id = 1;
|
||||
properties.floor_level = 0;
|
||||
properties.is_boss_room = false;
|
||||
properties.is_save_room = false;
|
||||
properties.is_shop_room = false;
|
||||
properties.music_id = 1;
|
||||
properties.ambient_sound_id = 0;
|
||||
|
||||
// Set room properties
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
|
||||
|
||||
// Get room properties
|
||||
auto properties_result = dungeon_editor_system_->GetRoomProperties(0x0000);
|
||||
ASSERT_TRUE(properties_result.ok()) << "Failed to get room properties: " << properties_result.status().message();
|
||||
|
||||
const auto& retrieved_properties = properties_result.value();
|
||||
EXPECT_EQ(retrieved_properties.room_id, 0x0000);
|
||||
EXPECT_EQ(retrieved_properties.name, "Test Room");
|
||||
EXPECT_EQ(retrieved_properties.description, "A test room for integration testing");
|
||||
EXPECT_EQ(retrieved_properties.dungeon_id, 1);
|
||||
|
||||
// Update properties
|
||||
properties.name = "Updated Test Room";
|
||||
properties.is_boss_room = true;
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
|
||||
|
||||
// Verify update
|
||||
auto updated_properties = dungeon_editor_system_->GetRoomProperties(0x0000);
|
||||
ASSERT_TRUE(updated_properties.ok());
|
||||
EXPECT_EQ(updated_properties.value().name, "Updated Test Room");
|
||||
EXPECT_TRUE(updated_properties.value().is_boss_room);
|
||||
}
|
||||
|
||||
// Test dungeon settings management
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, DungeonSettingsManagement) {
|
||||
// Create dungeon settings
|
||||
DungeonEditorSystem::DungeonSettings settings;
|
||||
settings.dungeon_id = 1;
|
||||
settings.name = "Test Dungeon";
|
||||
settings.description = "A test dungeon for integration testing";
|
||||
settings.total_rooms = 10;
|
||||
settings.starting_room_id = 0x0000;
|
||||
settings.boss_room_id = 0x0001;
|
||||
settings.music_theme_id = 1;
|
||||
settings.color_palette_id = 0;
|
||||
settings.has_map = true;
|
||||
settings.has_compass = true;
|
||||
settings.has_big_key = true;
|
||||
|
||||
// Set dungeon settings
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetDungeonSettings(settings).ok());
|
||||
|
||||
// Get dungeon settings
|
||||
auto settings_result = dungeon_editor_system_->GetDungeonSettings();
|
||||
ASSERT_TRUE(settings_result.ok()) << "Failed to get dungeon settings: " << settings_result.status().message();
|
||||
|
||||
const auto& retrieved_settings = settings_result.value();
|
||||
EXPECT_EQ(retrieved_settings.dungeon_id, 1);
|
||||
EXPECT_EQ(retrieved_settings.name, "Test Dungeon");
|
||||
EXPECT_EQ(retrieved_settings.total_rooms, 10);
|
||||
EXPECT_EQ(retrieved_settings.starting_room_id, 0x0000);
|
||||
EXPECT_EQ(retrieved_settings.boss_room_id, 0x0001);
|
||||
EXPECT_TRUE(retrieved_settings.has_map);
|
||||
EXPECT_TRUE(retrieved_settings.has_compass);
|
||||
EXPECT_TRUE(retrieved_settings.has_big_key);
|
||||
}
|
||||
|
||||
// Test undo/redo functionality
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, UndoRedoFunctionality) {
|
||||
// Set current room
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
|
||||
|
||||
// Get object editor
|
||||
auto object_editor = dungeon_editor_system_->GetObjectEditor();
|
||||
ASSERT_NE(object_editor, nullptr);
|
||||
|
||||
// Add some objects
|
||||
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
|
||||
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
|
||||
|
||||
// Verify objects were added
|
||||
EXPECT_EQ(object_editor->GetObjectCount(), 2);
|
||||
|
||||
// Test undo
|
||||
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
|
||||
EXPECT_EQ(object_editor->GetObjectCount(), 1);
|
||||
|
||||
// Test redo
|
||||
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
|
||||
EXPECT_EQ(object_editor->GetObjectCount(), 2);
|
||||
|
||||
// Test multiple undos
|
||||
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
|
||||
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
|
||||
EXPECT_EQ(object_editor->GetObjectCount(), 0);
|
||||
|
||||
// Test multiple redos
|
||||
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
|
||||
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
|
||||
EXPECT_EQ(object_editor->GetObjectCount(), 2);
|
||||
}
|
||||
|
||||
// Test validation functionality
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, ValidationFunctionality) {
|
||||
// Set current room
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
|
||||
|
||||
// Validate room
|
||||
auto room_validation = dungeon_editor_system_->ValidateRoom(0x0000);
|
||||
ASSERT_TRUE(room_validation.ok()) << "Room validation failed: " << room_validation.message();
|
||||
|
||||
// Validate dungeon
|
||||
auto dungeon_validation = dungeon_editor_system_->ValidateDungeon();
|
||||
ASSERT_TRUE(dungeon_validation.ok()) << "Dungeon validation failed: " << dungeon_validation.message();
|
||||
}
|
||||
|
||||
// Test save/load functionality
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, SaveLoadFunctionality) {
|
||||
// Set current room and add some objects
|
||||
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
|
||||
|
||||
auto object_editor = dungeon_editor_system_->GetObjectEditor();
|
||||
ASSERT_NE(object_editor, nullptr);
|
||||
|
||||
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
|
||||
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
|
||||
|
||||
// Save room
|
||||
ASSERT_TRUE(dungeon_editor_system_->SaveRoom(0x0000).ok());
|
||||
|
||||
// Reload room
|
||||
ASSERT_TRUE(dungeon_editor_system_->ReloadRoom(0x0000).ok());
|
||||
|
||||
// Verify objects are still there
|
||||
auto reloaded_objects = object_editor->GetObjects();
|
||||
EXPECT_EQ(reloaded_objects.size(), 2);
|
||||
|
||||
// Save entire dungeon
|
||||
ASSERT_TRUE(dungeon_editor_system_->SaveDungeon().ok());
|
||||
}
|
||||
|
||||
// Test performance with multiple operations
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, PerformanceTest) {
|
||||
auto start_time = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Perform many operations
|
||||
for (int i = 0; i < 100; i++) {
|
||||
// Add sprite
|
||||
DungeonEditorSystem::SpriteData sprite_data;
|
||||
sprite_data.sprite_id = i;
|
||||
sprite_data.type = DungeonEditorSystem::SpriteType::kEnemy;
|
||||
sprite_data.x = i * 10;
|
||||
sprite_data.y = i * 10;
|
||||
sprite_data.layer = 0;
|
||||
|
||||
ASSERT_TRUE(dungeon_editor_system_->AddSprite(sprite_data).ok());
|
||||
|
||||
// Add item
|
||||
DungeonEditorSystem::ItemData item_data;
|
||||
item_data.item_id = i;
|
||||
item_data.type = DungeonEditorSystem::ItemType::kKey;
|
||||
item_data.x = i * 15;
|
||||
item_data.y = i * 15;
|
||||
item_data.room_id = 0x0000;
|
||||
|
||||
ASSERT_TRUE(dungeon_editor_system_->AddItem(item_data).ok());
|
||||
}
|
||||
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
|
||||
|
||||
// Should complete in reasonable time (less than 5 seconds for 200 operations)
|
||||
EXPECT_LT(duration.count(), 5000) << "Performance test too slow: " << duration.count() << "ms";
|
||||
|
||||
std::cout << "Performance test: 200 operations took " << duration.count() << "ms" << std::endl;
|
||||
}
|
||||
|
||||
// Test error handling
|
||||
TEST_F(DungeonEditorSystemIntegrationTest, ErrorHandling) {
|
||||
// Test with invalid room ID
|
||||
auto invalid_room = dungeon_editor_system_->GetRoom(-1);
|
||||
EXPECT_FALSE(invalid_room.ok());
|
||||
|
||||
auto invalid_room_large = dungeon_editor_system_->GetRoom(10000);
|
||||
EXPECT_FALSE(invalid_room_large.ok());
|
||||
|
||||
// Test with invalid sprite ID
|
||||
auto invalid_sprite = dungeon_editor_system_->GetSprite(-1);
|
||||
EXPECT_FALSE(invalid_sprite.ok());
|
||||
|
||||
// Test with invalid item ID
|
||||
auto invalid_item = dungeon_editor_system_->GetItem(-1);
|
||||
EXPECT_FALSE(invalid_item.ok());
|
||||
|
||||
// Test with invalid entrance ID
|
||||
auto invalid_entrance = dungeon_editor_system_->GetEntrance(-1);
|
||||
EXPECT_FALSE(invalid_entrance.ok());
|
||||
|
||||
// Test with invalid door ID
|
||||
auto invalid_door = dungeon_editor_system_->GetDoor(-1);
|
||||
EXPECT_FALSE(invalid_door.ok());
|
||||
|
||||
// Test with invalid chest ID
|
||||
auto invalid_chest = dungeon_editor_system_->GetChest(-1);
|
||||
EXPECT_FALSE(invalid_chest.ok());
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
216
test/integration/zelda3/dungeon_object_rendering_tests.cc
Normal file
216
test/integration/zelda3/dungeon_object_rendering_tests.cc
Normal file
@@ -0,0 +1,216 @@
|
||||
// Integration tests for dungeon object rendering using ObjectDrawer
|
||||
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
|
||||
// instead of the obsolete ObjectRenderer
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
|
||||
#include "zelda3/dungeon/object_drawer.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
#include "app/gfx/render/background_buffer.h"
|
||||
#include "testing.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Tests for ObjectDrawer with realistic dungeon scenarios
|
||||
*
|
||||
* These tests validate that ObjectDrawer correctly renders dungeon objects
|
||||
* to BackgroundBuffers using pattern-based drawing routines.
|
||||
*/
|
||||
class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
|
||||
// Create drawer
|
||||
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
|
||||
|
||||
// Create background buffers
|
||||
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
|
||||
bg2_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
|
||||
|
||||
// Setup test palette
|
||||
palette_group_ = CreateTestPaletteGroup();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
bg2_.reset();
|
||||
bg1_.reset();
|
||||
drawer_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
gfx::PaletteGroup CreateTestPaletteGroup() {
|
||||
gfx::PaletteGroup group;
|
||||
gfx::SnesPalette palette;
|
||||
|
||||
// Create standard dungeon palette
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int intensity = i * 16;
|
||||
palette.AddColor(gfx::SnesColor(intensity, intensity, intensity));
|
||||
}
|
||||
|
||||
group.AddPalette(palette);
|
||||
return group;
|
||||
}
|
||||
|
||||
zelda3::RoomObject CreateTestObject(int id, int x, int y, int size = 0x12, int layer = 0) {
|
||||
zelda3::RoomObject obj(id, x, y, size, layer);
|
||||
obj.set_rom(rom());
|
||||
obj.EnsureTilesLoaded();
|
||||
return obj;
|
||||
}
|
||||
|
||||
std::unique_ptr<zelda3::ObjectDrawer> drawer_;
|
||||
std::unique_ptr<gfx::BackgroundBuffer> bg1_;
|
||||
std::unique_ptr<gfx::BackgroundBuffer> bg2_;
|
||||
gfx::PaletteGroup palette_group_;
|
||||
};
|
||||
|
||||
// Test basic object drawing
|
||||
TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // Wall
|
||||
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 0)); // Floor
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
|
||||
|
||||
// Verify buffers have content
|
||||
auto& bg1_bitmap = bg1_->bitmap();
|
||||
EXPECT_TRUE(bg1_bitmap.is_active());
|
||||
EXPECT_GT(bg1_bitmap.width(), 0);
|
||||
}
|
||||
|
||||
// Test objects on different layers
|
||||
TEST_F(DungeonObjectRenderingTests, MultiLayerRendering) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // BG1
|
||||
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 1)); // BG2
|
||||
objects.push_back(CreateTestObject(0x30, 15, 15, 0x12, 2)); // BG3
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// Both buffers should be active
|
||||
EXPECT_TRUE(bg1_->bitmap().is_active());
|
||||
EXPECT_TRUE(bg2_->bitmap().is_active());
|
||||
}
|
||||
|
||||
// Test empty object list
|
||||
TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
|
||||
std::vector<zelda3::RoomObject> objects; // Empty
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
// Should succeed (drawing nothing is valid)
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// Test large object set
|
||||
TEST_F(DungeonObjectRenderingTests, LargeObjectSet) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
|
||||
// Create 100 test objects
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int x = (i % 10) * 5;
|
||||
int y = (i / 10) * 5;
|
||||
objects.push_back(CreateTestObject(0x10 + (i % 20), x, y, 0x12, i % 2));
|
||||
}
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
// Should complete in reasonable time
|
||||
EXPECT_LT(duration.count(), 1000) << "Rendered 100 objects in " << duration.count() << "ms";
|
||||
}
|
||||
|
||||
// Test boundary conditions
|
||||
TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
|
||||
// Objects at boundaries
|
||||
objects.push_back(CreateTestObject(0x10, 0, 0, 0x12, 0)); // Origin
|
||||
objects.push_back(CreateTestObject(0x10, 63, 63, 0x12, 0)); // Max valid
|
||||
objects.push_back(CreateTestObject(0x10, 32, 32, 0x12, 0)); // Center
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// Test various object types
|
||||
TEST_F(DungeonObjectRenderingTests, VariousObjectTypes) {
|
||||
// Test common object types
|
||||
std::vector<int> object_types = {
|
||||
0x00, 0x01, 0x02, 0x03, // Floor/wall objects
|
||||
0x09, 0x0A, // Diagonal objects
|
||||
0x10, 0x11, 0x12, // Standard objects
|
||||
0x20, 0x21, // Decorative objects
|
||||
0x34, // Solid block
|
||||
};
|
||||
|
||||
for (int obj_type : object_types) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(obj_type, 10, 10, 0x12, 0));
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
// Some object types might not be valid, that's okay
|
||||
if (!status.ok()) {
|
||||
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
|
||||
<< " not renderable: " << status.message() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test error handling
|
||||
TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
|
||||
// Test with null ROM
|
||||
zelda3::ObjectDrawer null_drawer(nullptr);
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(0x10, 5, 5));
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = null_drawer.DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
212
test/integration/zelda3/dungeon_object_rendering_tests_new.cc
Normal file
212
test/integration/zelda3/dungeon_object_rendering_tests_new.cc
Normal file
@@ -0,0 +1,212 @@
|
||||
// Integration tests for dungeon object rendering using ObjectDrawer
|
||||
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
|
||||
// instead of the obsolete ObjectRenderer
|
||||
|
||||
#include "zelda3/dungeon/object_drawer.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/gfx/background_buffer.h"
|
||||
#include "testing.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
/**
|
||||
* @brief Tests for ObjectDrawer with realistic dungeon scenarios
|
||||
*
|
||||
* These tests validate that ObjectDrawer correctly renders dungeon objects
|
||||
* to BackgroundBuffers using pattern-based drawing routines.
|
||||
*/
|
||||
class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
|
||||
// Create drawer
|
||||
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
|
||||
|
||||
// Create background buffers
|
||||
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
|
||||
bg2_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
|
||||
|
||||
// Setup test palette
|
||||
palette_group_ = CreateTestPaletteGroup();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
bg2_.reset();
|
||||
bg1_.reset();
|
||||
drawer_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
gfx::PaletteGroup CreateTestPaletteGroup() {
|
||||
gfx::PaletteGroup group;
|
||||
gfx::SnesPalette palette;
|
||||
|
||||
// Create standard dungeon palette
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int intensity = i * 16;
|
||||
palette.AddColor(gfx::SnesColor(intensity, intensity, intensity));
|
||||
}
|
||||
|
||||
group.AddPalette(palette);
|
||||
return group;
|
||||
}
|
||||
|
||||
zelda3::RoomObject CreateTestObject(int id, int x, int y, int size = 0x12, int layer = 0) {
|
||||
zelda3::RoomObject obj(id, x, y, size, layer);
|
||||
obj.set_rom(rom());
|
||||
obj.EnsureTilesLoaded();
|
||||
return obj;
|
||||
}
|
||||
|
||||
std::unique_ptr<zelda3::ObjectDrawer> drawer_;
|
||||
std::unique_ptr<gfx::BackgroundBuffer> bg1_;
|
||||
std::unique_ptr<gfx::BackgroundBuffer> bg2_;
|
||||
gfx::PaletteGroup palette_group_;
|
||||
};
|
||||
|
||||
// Test basic object drawing
|
||||
TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // Wall
|
||||
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 0)); // Floor
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
|
||||
|
||||
// Verify buffers have content
|
||||
auto& bg1_bitmap = bg1_->bitmap();
|
||||
EXPECT_TRUE(bg1_bitmap.is_active());
|
||||
EXPECT_GT(bg1_bitmap.width(), 0);
|
||||
}
|
||||
|
||||
// Test objects on different layers
|
||||
TEST_F(DungeonObjectRenderingTests, MultiLayerRendering) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // BG1
|
||||
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 1)); // BG2
|
||||
objects.push_back(CreateTestObject(0x30, 15, 15, 0x12, 2)); // BG3
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// Both buffers should be active
|
||||
EXPECT_TRUE(bg1_->bitmap().is_active());
|
||||
EXPECT_TRUE(bg2_->bitmap().is_active());
|
||||
}
|
||||
|
||||
// Test empty object list
|
||||
TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
|
||||
std::vector<zelda3::RoomObject> objects; // Empty
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
// Should succeed (drawing nothing is valid)
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// Test large object set
|
||||
TEST_F(DungeonObjectRenderingTests, LargeObjectSet) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
|
||||
// Create 100 test objects
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int x = (i % 10) * 5;
|
||||
int y = (i / 10) * 5;
|
||||
objects.push_back(CreateTestObject(0x10 + (i % 20), x, y, 0x12, i % 2));
|
||||
}
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
// Should complete in reasonable time
|
||||
EXPECT_LT(duration.count(), 1000) << "Rendered 100 objects in " << duration.count() << "ms";
|
||||
}
|
||||
|
||||
// Test boundary conditions
|
||||
TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
|
||||
// Objects at boundaries
|
||||
objects.push_back(CreateTestObject(0x10, 0, 0, 0x12, 0)); // Origin
|
||||
objects.push_back(CreateTestObject(0x10, 63, 63, 0x12, 0)); // Max valid
|
||||
objects.push_back(CreateTestObject(0x10, 32, 32, 0x12, 0)); // Center
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
EXPECT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// Test various object types
|
||||
TEST_F(DungeonObjectRenderingTests, VariousObjectTypes) {
|
||||
// Test common object types
|
||||
std::vector<int> object_types = {
|
||||
0x00, 0x01, 0x02, 0x03, // Floor/wall objects
|
||||
0x09, 0x0A, // Diagonal objects
|
||||
0x10, 0x11, 0x12, // Standard objects
|
||||
0x20, 0x21, // Decorative objects
|
||||
0x34, // Solid block
|
||||
};
|
||||
|
||||
for (int obj_type : object_types) {
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(obj_type, 10, 10, 0x12, 0));
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
// Some object types might not be valid, that's okay
|
||||
if (!status.ok()) {
|
||||
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
|
||||
<< " not renderable: " << status.message() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test error handling
|
||||
TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
|
||||
// Test with null ROM
|
||||
zelda3::ObjectDrawer null_drawer(nullptr);
|
||||
std::vector<zelda3::RoomObject> objects;
|
||||
objects.push_back(CreateTestObject(0x10, 5, 5));
|
||||
|
||||
bg1_->ClearBuffer();
|
||||
bg2_->ClearBuffer();
|
||||
|
||||
auto status = null_drawer.DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
356
test/integration/zelda3/dungeon_rendering_test.cc
Normal file
356
test/integration/zelda3/dungeon_rendering_test.cc
Normal file
@@ -0,0 +1,356 @@
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/gfx/background_buffer.h"
|
||||
#include "app/gfx/snes_palette.h"
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/dungeon/object_drawer.h"
|
||||
#include "zelda3/dungeon/object_parser.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
class DungeonRenderingIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a mock ROM for testing
|
||||
rom_ = std::make_unique<Rom>();
|
||||
// Initialize with minimal ROM data for testing
|
||||
std::vector<uint8_t> mock_rom_data(1024 * 1024, 0); // 1MB mock ROM
|
||||
rom_->LoadFromData(mock_rom_data);
|
||||
|
||||
// Create test rooms
|
||||
room_0x00_ = CreateTestRoom(0x00); // Link's House
|
||||
room_0x01_ = CreateTestRoom(0x01); // Another test room
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
|
||||
// Create a test room with various objects
|
||||
Room CreateTestRoom(int room_id) {
|
||||
Room room(room_id, rom_.get());
|
||||
|
||||
// Add some test objects to the room
|
||||
std::vector<RoomObject> objects;
|
||||
|
||||
// Add floor objects (object 0x00)
|
||||
objects.emplace_back(0x00, 5, 5, 3, 0); // Horizontal floor
|
||||
objects.emplace_back(0x00, 10, 10, 5, 0); // Another floor section
|
||||
|
||||
// Add wall objects (object 0x01)
|
||||
objects.emplace_back(0x01, 15, 15, 2, 0); // Vertical wall
|
||||
objects.emplace_back(0x01, 20, 20, 4, 1); // Horizontal wall on BG2
|
||||
|
||||
// Add diagonal stairs (object 0x09)
|
||||
objects.emplace_back(0x09, 25, 25, 6, 0); // Diagonal stairs
|
||||
|
||||
// Add solid blocks (object 0x34)
|
||||
objects.emplace_back(0x34, 30, 30, 1, 0); // Solid block
|
||||
objects.emplace_back(0x34, 35, 35, 2, 1); // Another solid block on BG2
|
||||
|
||||
// Set ROM for all objects
|
||||
for (auto& obj : objects) {
|
||||
obj.set_rom(rom_.get());
|
||||
}
|
||||
|
||||
// Add objects to room (this would normally be done by LoadObjects)
|
||||
for (const auto& obj : objects) {
|
||||
room.AddObject(obj);
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
// Create a test palette
|
||||
gfx::SnesPalette CreateTestPalette() {
|
||||
gfx::SnesPalette palette;
|
||||
// Add some test colors
|
||||
palette.AddColor(gfx::SnesColor(0, 0, 0)); // Transparent
|
||||
palette.AddColor(gfx::SnesColor(255, 0, 0)); // Red
|
||||
palette.AddColor(gfx::SnesColor(0, 255, 0)); // Green
|
||||
palette.AddColor(gfx::SnesColor(0, 0, 255)); // Blue
|
||||
palette.AddColor(gfx::SnesColor(255, 255, 0)); // Yellow
|
||||
palette.AddColor(gfx::SnesColor(255, 0, 255)); // Magenta
|
||||
palette.AddColor(gfx::SnesColor(0, 255, 255)); // Cyan
|
||||
palette.AddColor(gfx::SnesColor(255, 255, 255)); // White
|
||||
return palette;
|
||||
}
|
||||
|
||||
gfx::PaletteGroup CreateTestPaletteGroup() {
|
||||
gfx::PaletteGroup group;
|
||||
group.AddPalette(CreateTestPalette());
|
||||
return group;
|
||||
}
|
||||
|
||||
private:
|
||||
Room room_0x00_;
|
||||
Room room_0x01_;
|
||||
};
|
||||
|
||||
// Test full room rendering with ObjectDrawer
|
||||
TEST_F(DungeonRenderingIntegrationTest, FullRoomRenderingWorks) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
|
||||
// Test that room has objects
|
||||
EXPECT_GT(test_room.GetTileObjects().size(), 0);
|
||||
|
||||
// Test ObjectDrawer can render the room
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
auto status = drawer.DrawObjectList(test_room.GetTileObjects(),
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test room rendering with different palette configurations
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithDifferentPalettes) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
|
||||
// Test with different palette configurations
|
||||
std::vector<gfx::PaletteGroup> palette_groups;
|
||||
|
||||
// Create multiple palette groups
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
palette_groups.push_back(CreateTestPaletteGroup());
|
||||
}
|
||||
|
||||
for (const auto& palette_group : palette_groups) {
|
||||
auto status = drawer.DrawObjectList(test_room.GetTileObjects(),
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
}
|
||||
|
||||
// Test room rendering with objects on different layers
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMultipleLayers) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Separate objects by layer
|
||||
std::vector<RoomObject> bg1_objects;
|
||||
std::vector<RoomObject> bg2_objects;
|
||||
|
||||
for (const auto& obj : test_room.GetTileObjects()) {
|
||||
if (obj.GetLayerValue() == 0) {
|
||||
bg1_objects.push_back(obj);
|
||||
} else if (obj.GetLayerValue() == 1) {
|
||||
bg2_objects.push_back(obj);
|
||||
}
|
||||
}
|
||||
|
||||
// Render BG1 objects
|
||||
if (!bg1_objects.empty()) {
|
||||
auto status = drawer.DrawObjectList(bg1_objects,
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Render BG2 objects
|
||||
if (!bg2_objects.empty()) {
|
||||
auto status = drawer.DrawObjectList(bg2_objects,
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
}
|
||||
|
||||
// Test room rendering with various object sizes
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithVariousObjectSizes) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Group objects by size
|
||||
std::map<int, std::vector<RoomObject>> objects_by_size;
|
||||
|
||||
for (const auto& obj : test_room.GetTileObjects()) {
|
||||
objects_by_size[obj.size_].push_back(obj);
|
||||
}
|
||||
|
||||
// Render objects of each size
|
||||
for (const auto& [size, objects] : objects_by_size) {
|
||||
auto status = drawer.DrawObjectList(objects,
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
}
|
||||
|
||||
// Test room rendering performance
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingPerformance) {
|
||||
// Create a room with many objects
|
||||
Room large_room(0x00, rom_.get());
|
||||
|
||||
// Add many test objects
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
int id = i % 65; // Cycle through object IDs 0-64
|
||||
int x = (i * 2) % 60; // Spread across buffer
|
||||
int y = (i * 3) % 60;
|
||||
int size = (i % 8) + 1; // Size 1-8
|
||||
int layer = i % 2; // Alternate layers
|
||||
|
||||
RoomObject obj(id, x, y, size, layer);
|
||||
obj.set_rom(rom_.get());
|
||||
large_room.AddObject(obj);
|
||||
}
|
||||
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Time the rendering operation
|
||||
auto start_time = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto status = drawer.DrawObjectList(large_room.GetTileObjects(),
|
||||
large_room.bg1_buffer(),
|
||||
large_room.bg2_buffer(),
|
||||
palette_group);
|
||||
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
end_time - start_time);
|
||||
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
|
||||
// Should complete in reasonable time (less than 2 seconds for 200 objects)
|
||||
EXPECT_LT(duration.count(), 2000);
|
||||
|
||||
std::cout << "Rendered room with 200 objects in " << duration.count() << "ms" << std::endl;
|
||||
}
|
||||
|
||||
// Test room rendering with edge case coordinates
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithEdgeCaseCoordinates) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Add objects at edge coordinates
|
||||
std::vector<RoomObject> edge_objects;
|
||||
|
||||
edge_objects.emplace_back(0x34, 0, 0, 1, 0); // Origin
|
||||
edge_objects.emplace_back(0x34, 63, 63, 1, 0); // Near buffer edge
|
||||
edge_objects.emplace_back(0x34, 32, 32, 1, 0); // Center
|
||||
edge_objects.emplace_back(0x34, 1, 1, 1, 0); // Near origin
|
||||
edge_objects.emplace_back(0x34, 62, 62, 1, 0); // Near edge
|
||||
|
||||
// Set ROM for all objects
|
||||
for (auto& obj : edge_objects) {
|
||||
obj.set_rom(rom_.get());
|
||||
}
|
||||
|
||||
auto status = drawer.DrawObjectList(edge_objects,
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test room rendering with mixed object types
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMixedObjectTypes) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Add various object types
|
||||
std::vector<RoomObject> mixed_objects;
|
||||
|
||||
// Floor objects
|
||||
mixed_objects.emplace_back(0x00, 5, 5, 3, 0);
|
||||
mixed_objects.emplace_back(0x01, 10, 10, 2, 0);
|
||||
|
||||
// Wall objects
|
||||
mixed_objects.emplace_back(0x02, 15, 15, 4, 0);
|
||||
mixed_objects.emplace_back(0x03, 20, 20, 1, 1);
|
||||
|
||||
// Diagonal objects
|
||||
mixed_objects.emplace_back(0x09, 25, 25, 5, 0);
|
||||
mixed_objects.emplace_back(0x0A, 30, 30, 3, 0);
|
||||
|
||||
// Solid objects
|
||||
mixed_objects.emplace_back(0x34, 35, 35, 1, 0);
|
||||
mixed_objects.emplace_back(0x33, 40, 40, 2, 1);
|
||||
|
||||
// Decorative objects
|
||||
mixed_objects.emplace_back(0x36, 45, 45, 3, 0);
|
||||
mixed_objects.emplace_back(0x38, 50, 50, 1, 0);
|
||||
|
||||
// Set ROM for all objects
|
||||
for (auto& obj : mixed_objects) {
|
||||
obj.set_rom(rom_.get());
|
||||
}
|
||||
|
||||
auto status = drawer.DrawObjectList(mixed_objects,
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
// Test room rendering error handling
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingErrorHandling) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
|
||||
// Test with null ROM
|
||||
ObjectDrawer null_drawer(nullptr);
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
auto status = null_drawer.DrawObjectList(test_room.GetTileObjects(),
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
|
||||
}
|
||||
|
||||
// Test room rendering with invalid object data
|
||||
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithInvalidObjectData) {
|
||||
Room test_room = CreateTestRoom(0x00);
|
||||
ObjectDrawer drawer(rom_.get());
|
||||
auto palette_group = CreateTestPaletteGroup();
|
||||
|
||||
// Create objects with invalid data
|
||||
std::vector<RoomObject> invalid_objects;
|
||||
|
||||
invalid_objects.emplace_back(0x999, 5, 5, 1, 0); // Invalid object ID
|
||||
invalid_objects.emplace_back(0x00, -1, -1, 1, 0); // Negative coordinates
|
||||
invalid_objects.emplace_back(0x00, 100, 100, 1, 0); // Out of bounds coordinates
|
||||
invalid_objects.emplace_back(0x00, 5, 5, 255, 0); // Maximum size
|
||||
|
||||
// Set ROM for all objects
|
||||
for (auto& obj : invalid_objects) {
|
||||
obj.set_rom(rom_.get());
|
||||
}
|
||||
|
||||
// Should handle gracefully
|
||||
auto status = drawer.DrawObjectList(invalid_objects,
|
||||
test_room.bg1_buffer(),
|
||||
test_room.bg2_buffer(),
|
||||
palette_group);
|
||||
|
||||
// Should succeed or fail gracefully
|
||||
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
34
test/integration/zelda3/dungeon_room_test.cc
Normal file
34
test/integration/zelda3/dungeon_room_test.cc
Normal file
@@ -0,0 +1,34 @@
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/dungeon/room.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
class DungeonRoomTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Skip tests on Linux for automated github builds
|
||||
#if defined(__linux__)
|
||||
GTEST_SKIP();
|
||||
#else
|
||||
if (!rom_.LoadFromFile("./zelda3.sfc").ok()) {
|
||||
GTEST_SKIP_("Failed to load test ROM");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void TearDown() override {}
|
||||
|
||||
Rom rom_;
|
||||
};
|
||||
|
||||
TEST_F(DungeonRoomTest, SingleRoomLoadOk) {
|
||||
zelda3::Room test_room(/*room_id=*/0, &rom_);
|
||||
|
||||
test_room = zelda3::LoadRoomFromRom(&rom_, /*room_id=*/0);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
300
test/integration/zelda3/message_test.cc
Normal file
300
test/integration/zelda3/message_test.cc
Normal file
@@ -0,0 +1,300 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <filesystem>
|
||||
|
||||
#include "app/editor/message/message_data.h"
|
||||
#include "app/editor/message/message_editor.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
class MessageRomTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Skip tests if ROM is not available
|
||||
if (getenv("YAZE_SKIP_ROM_TESTS")) {
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
}
|
||||
|
||||
// Check if ROM file exists
|
||||
std::string rom_path = "zelda3.sfc";
|
||||
if (!std::filesystem::exists(rom_path)) {
|
||||
GTEST_SKIP() << "Test ROM not found: " << rom_path;
|
||||
}
|
||||
|
||||
EXPECT_OK(rom_.LoadFromFile(rom_path));
|
||||
dictionary_ = editor::BuildDictionaryEntries(&rom_);
|
||||
}
|
||||
void TearDown() override {}
|
||||
|
||||
Rom rom_;
|
||||
editor::MessageEditor message_editor_;
|
||||
std::vector<editor::DictionaryEntry> dictionary_;
|
||||
};
|
||||
|
||||
TEST_F(MessageRomTest, ParseSingleMessage_CommandParsing) {
|
||||
std::vector<uint8_t> mock_data = {0x6A, 0x7F, 0x00};
|
||||
int pos = 0;
|
||||
|
||||
auto result = editor::ParseSingleMessage(mock_data, &pos);
|
||||
EXPECT_TRUE(result.ok());
|
||||
const auto message_data = result.value();
|
||||
|
||||
// Verify that the command was recognized and parsed
|
||||
EXPECT_EQ(message_data.ContentsParsed, "[L]");
|
||||
EXPECT_EQ(pos, 2);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseSingleMessage_BasicAscii) {
|
||||
// A, B, C, terminator
|
||||
std::vector<uint8_t> mock_data = {0x00, 0x01, 0x02, 0x7F, 0x00};
|
||||
int pos = 0;
|
||||
|
||||
auto result = editor::ParseSingleMessage(mock_data, &pos);
|
||||
ASSERT_TRUE(result.ok());
|
||||
const auto message_data = result.value();
|
||||
EXPECT_EQ(pos, 4); // consumed all 4 bytes
|
||||
|
||||
std::vector<editor::MessageData> message_data_vector = {message_data};
|
||||
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
|
||||
|
||||
EXPECT_THAT(parsed, ::testing::ElementsAre("ABC"));
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, FindMatchingCharacter_Success) {
|
||||
EXPECT_EQ(editor::FindMatchingCharacter('A'), 0x00);
|
||||
EXPECT_EQ(editor::FindMatchingCharacter('Z'), 0x19);
|
||||
EXPECT_EQ(editor::FindMatchingCharacter('a'), 0x1A);
|
||||
EXPECT_EQ(editor::FindMatchingCharacter('z'), 0x33);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, FindMatchingCharacter_Failure) {
|
||||
EXPECT_EQ(editor::FindMatchingCharacter('@'), 0xFF);
|
||||
EXPECT_EQ(editor::FindMatchingCharacter('#'), 0xFF);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, FindDictionaryEntry_Success) {
|
||||
EXPECT_EQ(editor::FindDictionaryEntry(0x88), 0x00);
|
||||
EXPECT_EQ(editor::FindDictionaryEntry(0x90), 0x08);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, FindDictionaryEntry_Failure) {
|
||||
EXPECT_EQ(editor::FindDictionaryEntry(0x00), -1);
|
||||
EXPECT_EQ(editor::FindDictionaryEntry(0xFF), -1);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseMessageToData_Basic) {
|
||||
std::string input = "[L][C:01]ABC";
|
||||
auto result = editor::ParseMessageToData(input);
|
||||
std::vector<uint8_t> expected = {0x6A, 0x77, 0x01, 0x00, 0x01, 0x02};
|
||||
EXPECT_EQ(result, expected);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ReplaceAllDictionaryWords_Success) {
|
||||
std::vector<editor::DictionaryEntry> mock_dict = {
|
||||
editor::DictionaryEntry(0x00, "test"),
|
||||
editor::DictionaryEntry(0x01, "message")};
|
||||
std::string input = "This is a test message.";
|
||||
auto result = editor::ReplaceAllDictionaryWords(input, mock_dict);
|
||||
EXPECT_EQ(result, "This is a [D:00] [D:01].");
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ReplaceAllDictionaryWords_NoMatch) {
|
||||
std::vector<editor::DictionaryEntry> mock_dict = {
|
||||
editor::DictionaryEntry(0x00, "hello")};
|
||||
std::string input = "No matching words.";
|
||||
auto result = editor::ReplaceAllDictionaryWords(input, mock_dict);
|
||||
EXPECT_EQ(result, "No matching words.");
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseTextDataByte_Success) {
|
||||
EXPECT_EQ(editor::ParseTextDataByte(0x00), "A");
|
||||
EXPECT_EQ(editor::ParseTextDataByte(0x74), "[1]");
|
||||
EXPECT_EQ(editor::ParseTextDataByte(0x88), "[D:00]");
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseTextDataByte_Failure) {
|
||||
EXPECT_EQ(editor::ParseTextDataByte(0xFF), "");
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseSingleMessage_SpecialCharacters) {
|
||||
std::vector<uint8_t> mock_data = {0x4D, 0x4E, 0x4F, 0x50, 0x7F};
|
||||
int pos = 0;
|
||||
|
||||
auto result = editor::ParseSingleMessage(mock_data, &pos);
|
||||
ASSERT_TRUE(result.ok());
|
||||
const auto message_data = result.value();
|
||||
|
||||
EXPECT_EQ(message_data.ContentsParsed, "[UP][DOWN][LEFT][RIGHT]");
|
||||
EXPECT_EQ(pos, 5);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseSingleMessage_DictionaryReference) {
|
||||
std::vector<uint8_t> mock_data = {0x88, 0x89, 0x7F};
|
||||
int pos = 0;
|
||||
|
||||
auto result = editor::ParseSingleMessage(mock_data, &pos);
|
||||
ASSERT_TRUE(result.ok());
|
||||
const auto message_data = result.value();
|
||||
|
||||
EXPECT_EQ(message_data.ContentsParsed, "[D:00][D:01]");
|
||||
EXPECT_EQ(pos, 3);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseSingleMessage_InvalidTerminator) {
|
||||
std::vector<uint8_t> mock_data = {0x00, 0x01, 0x02}; // No terminator
|
||||
int pos = 0;
|
||||
|
||||
auto result = editor::ParseSingleMessage(mock_data, &pos);
|
||||
EXPECT_FALSE(result.ok());
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseSingleMessage_EmptyData) {
|
||||
std::vector<uint8_t> mock_data = {0x7F};
|
||||
int pos = 0;
|
||||
|
||||
auto result = editor::ParseSingleMessage(mock_data, &pos);
|
||||
ASSERT_TRUE(result.ok());
|
||||
const auto message_data = result.value();
|
||||
|
||||
EXPECT_EQ(message_data.ContentsParsed, "");
|
||||
EXPECT_EQ(pos, 1);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, OptimizeMessageForDictionary_Basic) {
|
||||
std::vector<editor::DictionaryEntry> mock_dict = {
|
||||
editor::DictionaryEntry(0x00, "Link"),
|
||||
editor::DictionaryEntry(0x01, "Zelda")};
|
||||
std::string input = "[L] rescued Zelda from danger.";
|
||||
|
||||
editor::MessageData message_data;
|
||||
std::string optimized =
|
||||
message_data.OptimizeMessageForDictionary(input, mock_dict);
|
||||
|
||||
EXPECT_EQ(optimized, "[L] rescued [D:01] from danger.");
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, SetMessage_Success) {
|
||||
std::vector<editor::DictionaryEntry> mock_dict = {
|
||||
editor::DictionaryEntry(0x00, "item")};
|
||||
editor::MessageData message_data;
|
||||
std::string input = "You got an item!";
|
||||
|
||||
message_data.SetMessage(input, mock_dict);
|
||||
|
||||
EXPECT_EQ(message_data.RawString, "You got an item!");
|
||||
EXPECT_EQ(message_data.ContentsParsed, "You got an [D:00]!");
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, FindMatchingElement_CommandWithArgument) {
|
||||
std::string input = "[W:02]";
|
||||
editor::ParsedElement result = editor::FindMatchingElement(input);
|
||||
|
||||
EXPECT_TRUE(result.Active);
|
||||
EXPECT_EQ(result.Parent.Token, "W");
|
||||
EXPECT_EQ(result.Value, 0x02);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, FindMatchingElement_InvalidCommand) {
|
||||
std::string input = "[INVALID]";
|
||||
editor::ParsedElement result = editor::FindMatchingElement(input);
|
||||
|
||||
EXPECT_FALSE(result.Active);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, BuildDictionaryEntries_CorrectSize) {
|
||||
auto result = editor::BuildDictionaryEntries(&rom_);
|
||||
EXPECT_EQ(result.size(), editor::kNumDictionaryEntries);
|
||||
EXPECT_FALSE(result.empty());
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseMessageData_CommandWithArgument_NoExtraCharacters) {
|
||||
// This test specifically checks for the bug where command arguments
|
||||
// were being incorrectly parsed as characters (e.g., capital 'A' after [W])
|
||||
// The bug was caused by using a range-based for loop while also tracking position
|
||||
|
||||
// Message: [W:01]ABC
|
||||
// Bytes: 0x6B (W command), 0x01 (argument), 0x00 (A), 0x01 (B), 0x02 (C)
|
||||
std::vector<uint8_t> data = {0x6B, 0x01, 0x00, 0x01, 0x02};
|
||||
|
||||
editor::MessageData message;
|
||||
message.ID = 0;
|
||||
message.Address = 0;
|
||||
message.Data = data;
|
||||
|
||||
std::vector<editor::MessageData> message_data_vector = {message};
|
||||
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
|
||||
|
||||
// Should be "[W:01]ABC" NOT "[W:01]BABC" or "[W:01]AABC"
|
||||
EXPECT_EQ(parsed[0], "[W:01]ABC");
|
||||
|
||||
// The 'B' should not appear twice or be skipped
|
||||
EXPECT_EQ(parsed[0].find("BABC"), std::string::npos);
|
||||
EXPECT_EQ(parsed[0].find("AABC"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseMessageData_MultipleCommandsWithArguments) {
|
||||
// Test multiple commands with arguments in sequence
|
||||
// [W:01][C:02]AB
|
||||
std::vector<uint8_t> data = {
|
||||
0x6B, 0x01, // [W:01] - Window border command with arg
|
||||
0x77, 0x02, // [C:02] - Color command with arg
|
||||
0x00, 0x01 // AB - Regular characters
|
||||
};
|
||||
|
||||
editor::MessageData message;
|
||||
message.ID = 0;
|
||||
message.Data = data;
|
||||
|
||||
std::vector<editor::MessageData> message_data_vector = {message};
|
||||
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
|
||||
|
||||
EXPECT_EQ(parsed[0], "[W:01][C:02]AB");
|
||||
|
||||
// Make sure argument bytes (0x01, 0x02) weren't parsed as characters
|
||||
EXPECT_EQ(parsed[0].find("BAB"), std::string::npos);
|
||||
EXPECT_EQ(parsed[0].find("CAB"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseMessageData_CommandWithoutArgument) {
|
||||
// Test command without argument followed by text
|
||||
// [K]ABC - Wait for key command (no arg) followed by ABC
|
||||
std::vector<uint8_t> data = {
|
||||
0x7E, // [K] - Wait for key (no argument)
|
||||
0x00, 0x01, 0x02 // ABC
|
||||
};
|
||||
|
||||
editor::MessageData message;
|
||||
message.ID = 0;
|
||||
message.Data = data;
|
||||
|
||||
std::vector<editor::MessageData> message_data_vector = {message};
|
||||
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
|
||||
|
||||
EXPECT_EQ(parsed[0], "[K]ABC");
|
||||
}
|
||||
|
||||
TEST_F(MessageRomTest, ParseMessageData_MixedCommands) {
|
||||
// Test mix of commands with and without arguments
|
||||
// [W:01]A[K]B[C:02]C
|
||||
std::vector<uint8_t> data = {
|
||||
0x6B, 0x01, // [W:01] - with arg
|
||||
0x00, // A
|
||||
0x7E, // [K] - no arg
|
||||
0x01, // B
|
||||
0x77, 0x02, // [C:02] - with arg
|
||||
0x02 // C
|
||||
};
|
||||
|
||||
editor::MessageData message;
|
||||
message.ID = 0;
|
||||
message.Data = data;
|
||||
|
||||
std::vector<editor::MessageData> message_data_vector = {message};
|
||||
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
|
||||
|
||||
EXPECT_EQ(parsed[0], "[W:01]A[K]B[C:02]C");
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
406
test/integration/zelda3/overworld_integration_test.cc
Normal file
406
test/integration/zelda3/overworld_integration_test.cc
Normal file
@@ -0,0 +1,406 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/overworld/overworld.h"
|
||||
#include "zelda3/overworld/overworld_map.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
/**
|
||||
* @brief Comprehensive overworld integration test that validates YAZE C++
|
||||
* implementation against ZScream C# logic and existing test infrastructure
|
||||
*
|
||||
* This test suite:
|
||||
* 1. Validates overworld loading logic matches ZScream behavior
|
||||
* 2. Tests integration with ZSCustomOverworld versions (vanilla, v2, v3)
|
||||
* 3. Uses existing RomDependentTestSuite infrastructure when available
|
||||
* 4. Provides both mock data and real ROM testing capabilities
|
||||
*/
|
||||
class OverworldIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
#if defined(__linux__)
|
||||
GTEST_SKIP();
|
||||
#endif
|
||||
|
||||
// Check if we should use real ROM or mock data
|
||||
const char* rom_path_env = getenv("YAZE_TEST_ROM_PATH");
|
||||
const char* skip_rom_tests = getenv("YAZE_SKIP_ROM_TESTS");
|
||||
|
||||
if (skip_rom_tests) {
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
}
|
||||
|
||||
if (rom_path_env && std::filesystem::exists(rom_path_env)) {
|
||||
// Use real ROM for testing
|
||||
rom_ = std::make_unique<Rom>();
|
||||
auto status = rom_->LoadFromFile(rom_path_env);
|
||||
if (status.ok()) {
|
||||
use_real_rom_ = true;
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to mock data
|
||||
use_real_rom_ = false;
|
||||
rom_ = std::make_unique<Rom>();
|
||||
SetupMockRomData();
|
||||
rom_->LoadFromData(mock_rom_data_);
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
overworld_.reset();
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
void SetupMockRomData() {
|
||||
mock_rom_data_.resize(0x200000, 0x00);
|
||||
|
||||
// Basic ROM structure
|
||||
mock_rom_data_[0x140145] = 0xFF; // Vanilla ASM
|
||||
|
||||
// Tile16 expansion flag
|
||||
mock_rom_data_[0x017D28] = 0x0F; // Vanilla
|
||||
|
||||
// Tile32 expansion flag
|
||||
mock_rom_data_[0x01772E] = 0x04; // Vanilla
|
||||
|
||||
// Basic map data
|
||||
for (int i = 0; i < 160; i++) {
|
||||
mock_rom_data_[0x012844 + i] = 0x00; // Small areas
|
||||
}
|
||||
|
||||
// Setup entrance data (matches ZScream Constants.OWEntranceMap/Pos/EntranceId)
|
||||
for (int i = 0; i < 129; i++) {
|
||||
mock_rom_data_[0x0DB96F + (i * 2)] = i & 0xFF; // Map ID
|
||||
mock_rom_data_[0x0DB96F + (i * 2) + 1] = (i >> 8) & 0xFF;
|
||||
mock_rom_data_[0x0DBA71 + (i * 2)] = (i * 16) & 0xFF; // Map Position
|
||||
mock_rom_data_[0x0DBA71 + (i * 2) + 1] = ((i * 16) >> 8) & 0xFF;
|
||||
mock_rom_data_[0x0DBB73 + i] = i & 0xFF; // Entrance ID
|
||||
}
|
||||
|
||||
// Setup exit data (matches ZScream Constants.OWExit*)
|
||||
for (int i = 0; i < 0x4F; i++) {
|
||||
mock_rom_data_[0x015D8A + (i * 2)] = i & 0xFF; // Room ID
|
||||
mock_rom_data_[0x015D8A + (i * 2) + 1] = (i >> 8) & 0xFF;
|
||||
mock_rom_data_[0x015E28 + i] = i & 0xFF; // Map ID
|
||||
mock_rom_data_[0x015E77 + (i * 2)] = i & 0xFF; // VRAM
|
||||
mock_rom_data_[0x015E77 + (i * 2) + 1] = (i >> 8) & 0xFF;
|
||||
// Add other exit fields...
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> mock_rom_data_;
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<Overworld> overworld_;
|
||||
bool use_real_rom_ = false;
|
||||
};
|
||||
|
||||
// Test Tile32 expansion detection
|
||||
TEST_F(OverworldIntegrationTest, Tile32ExpansionDetection) {
|
||||
mock_rom_data_[0x01772E] = 0x04;
|
||||
mock_rom_data_[0x140145] = 0xFF;
|
||||
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// Test expanded detection
|
||||
mock_rom_data_[0x01772E] = 0x05;
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
|
||||
status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// Test Tile16 expansion detection
|
||||
TEST_F(OverworldIntegrationTest, Tile16ExpansionDetection) {
|
||||
mock_rom_data_[0x017D28] = 0x0F;
|
||||
mock_rom_data_[0x140145] = 0xFF;
|
||||
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// Test expanded detection
|
||||
mock_rom_data_[0x017D28] = 0x10;
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
|
||||
status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
}
|
||||
|
||||
// Test entrance loading matches ZScream coordinate calculation
|
||||
TEST_F(OverworldIntegrationTest, EntranceCoordinateCalculation) {
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
const auto& entrances = overworld_->entrances();
|
||||
EXPECT_EQ(entrances.size(), 129);
|
||||
|
||||
// Verify coordinate calculation matches ZScream logic:
|
||||
// int p = mapPos >> 1;
|
||||
// int x = p % 64;
|
||||
// int y = p >> 6;
|
||||
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
|
||||
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
|
||||
|
||||
for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
|
||||
const auto& entrance = entrances[i];
|
||||
|
||||
uint16_t map_pos = i * 16; // Our test data
|
||||
uint16_t map_id = i; // Our test data
|
||||
|
||||
int position = map_pos >> 1;
|
||||
int x_coord = position % 64;
|
||||
int y_coord = position >> 6;
|
||||
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
|
||||
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
|
||||
|
||||
EXPECT_EQ(entrance.x_, expected_x);
|
||||
EXPECT_EQ(entrance.y_, expected_y);
|
||||
EXPECT_EQ(entrance.entrance_id_, i);
|
||||
EXPECT_FALSE(entrance.is_hole_);
|
||||
}
|
||||
}
|
||||
|
||||
// Test exit loading matches ZScream data structure
|
||||
TEST_F(OverworldIntegrationTest, ExitDataLoading) {
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
const auto& exits = overworld_->exits();
|
||||
EXPECT_EQ(exits->size(), 0x4F);
|
||||
|
||||
// Verify exit data matches our test data
|
||||
for (int i = 0; i < std::min(5, static_cast<int>(exits->size())); i++) {
|
||||
const auto& exit = exits->at(i);
|
||||
// EXPECT_EQ(exit.room_id_, i);
|
||||
// EXPECT_EQ(exit.map_id_, i);
|
||||
// EXPECT_EQ(exit.map_pos_, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Test ASM version detection affects item loading
|
||||
TEST_F(OverworldIntegrationTest, ASMVersionItemLoading) {
|
||||
// Test vanilla ASM (should limit to 0x80 maps)
|
||||
mock_rom_data_[0x140145] = 0xFF;
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
const auto& items = overworld_->all_items();
|
||||
|
||||
// Test v3+ ASM (should support all 0xA0 maps)
|
||||
mock_rom_data_[0x140145] = 0x03;
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
|
||||
status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
const auto& items_v3 = overworld_->all_items();
|
||||
// v3 should have more comprehensive support
|
||||
EXPECT_GE(items_v3.size(), items.size());
|
||||
}
|
||||
|
||||
// Test map size assignment logic
|
||||
TEST_F(OverworldIntegrationTest, MapSizeAssignment) {
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
const auto& maps = overworld_->overworld_maps();
|
||||
EXPECT_EQ(maps.size(), 160);
|
||||
|
||||
// Verify all maps are initialized
|
||||
for (const auto& map : maps) {
|
||||
EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
|
||||
EXPECT_LE(map.area_size(), AreaSizeEnum::TallArea);
|
||||
}
|
||||
}
|
||||
|
||||
// Test integration with ZSCustomOverworld version detection
|
||||
TEST_F(OverworldIntegrationTest, ZSCustomOverworldVersionIntegration) {
|
||||
if (!use_real_rom_) {
|
||||
GTEST_SKIP() << "Real ROM required for ZSCustomOverworld version testing";
|
||||
}
|
||||
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// Check ASM version detection
|
||||
auto version_byte = rom_->ReadByte(0x140145);
|
||||
ASSERT_TRUE(version_byte.ok());
|
||||
|
||||
uint8_t asm_version = *version_byte;
|
||||
|
||||
if (asm_version == 0xFF) {
|
||||
// Vanilla ROM
|
||||
EXPECT_FALSE(overworld_->expanded_tile16());
|
||||
EXPECT_FALSE(overworld_->expanded_tile32());
|
||||
} else if (asm_version >= 0x02 && asm_version <= 0x03) {
|
||||
// ZSCustomOverworld v2/v3
|
||||
// Should have expanded features
|
||||
EXPECT_TRUE(overworld_->expanded_tile16());
|
||||
EXPECT_TRUE(overworld_->expanded_tile32());
|
||||
}
|
||||
|
||||
// Verify version-specific features are properly detected
|
||||
if (asm_version >= 0x03) {
|
||||
// v3 features should be available
|
||||
const auto& maps = overworld_->overworld_maps();
|
||||
EXPECT_EQ(maps.size(), 160); // All 160 maps supported in v3
|
||||
}
|
||||
}
|
||||
|
||||
// Test compatibility with RomDependentTestSuite infrastructure
|
||||
TEST_F(OverworldIntegrationTest, RomDependentTestSuiteCompatibility) {
|
||||
if (!use_real_rom_) {
|
||||
GTEST_SKIP() << "Real ROM required for RomDependentTestSuite compatibility testing";
|
||||
}
|
||||
|
||||
// Test that our overworld loading works with the same patterns as RomDependentTestSuite
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// Verify ROM-dependent features work correctly
|
||||
EXPECT_TRUE(overworld_->is_loaded());
|
||||
|
||||
const auto& maps = overworld_->overworld_maps();
|
||||
EXPECT_EQ(maps.size(), 160);
|
||||
|
||||
// Test that we can access the same data structures as RomDependentTestSuite
|
||||
for (int i = 0; i < std::min(10, static_cast<int>(maps.size())); i++) {
|
||||
const auto& map = maps[i];
|
||||
|
||||
// Verify map properties are accessible
|
||||
EXPECT_GE(map.area_graphics(), 0);
|
||||
EXPECT_GE(map.main_palette(), 0);
|
||||
EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
|
||||
EXPECT_LE(map.area_size(), AreaSizeEnum::TallArea);
|
||||
}
|
||||
|
||||
// Test that sprite data is accessible (matches RomDependentTestSuite expectations)
|
||||
const auto& sprites = overworld_->sprites(0);
|
||||
EXPECT_EQ(sprites.size(), 3); // Three game states
|
||||
|
||||
// Test that item data is accessible
|
||||
const auto& items = overworld_->all_items();
|
||||
EXPECT_GE(items.size(), 0);
|
||||
|
||||
// Test that entrance/exit data is accessible
|
||||
const auto& entrances = overworld_->entrances();
|
||||
const auto& exits = overworld_->exits();
|
||||
EXPECT_EQ(entrances.size(), 129);
|
||||
EXPECT_EQ(exits->size(), 0x4F);
|
||||
}
|
||||
|
||||
// Test comprehensive overworld data integrity
|
||||
TEST_F(OverworldIntegrationTest, ComprehensiveDataIntegrity) {
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
// Verify all major data structures are properly loaded
|
||||
EXPECT_GT(overworld_->tiles16().size(), 0);
|
||||
EXPECT_GT(overworld_->tiles32_unique().size(), 0);
|
||||
|
||||
// Verify map organization matches ZScream expectations
|
||||
const auto& map_tiles = overworld_->map_tiles();
|
||||
EXPECT_EQ(map_tiles.light_world.size(), 512);
|
||||
EXPECT_EQ(map_tiles.dark_world.size(), 512);
|
||||
EXPECT_EQ(map_tiles.special_world.size(), 512);
|
||||
|
||||
// Verify each world has proper 512x512 tile data
|
||||
for (const auto& row : map_tiles.light_world) {
|
||||
EXPECT_EQ(row.size(), 512);
|
||||
}
|
||||
for (const auto& row : map_tiles.dark_world) {
|
||||
EXPECT_EQ(row.size(), 512);
|
||||
}
|
||||
for (const auto& row : map_tiles.special_world) {
|
||||
EXPECT_EQ(row.size(), 512);
|
||||
}
|
||||
|
||||
// Verify overworld maps are properly initialized
|
||||
const auto& maps = overworld_->overworld_maps();
|
||||
EXPECT_EQ(maps.size(), 160);
|
||||
|
||||
for (const auto& map : maps) {
|
||||
// TODO: Find a way to compare
|
||||
// EXPECT_TRUE(map.bitmap_data() != nullptr);
|
||||
}
|
||||
|
||||
// Verify tile types are loaded
|
||||
const auto& tile_types = overworld_->all_tiles_types();
|
||||
EXPECT_EQ(tile_types.size(), 0x200);
|
||||
}
|
||||
|
||||
// Test ZScream coordinate calculation compatibility
|
||||
TEST_F(OverworldIntegrationTest, ZScreamCoordinateCompatibility) {
|
||||
auto status = overworld_->Load(rom_.get());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
const auto& entrances = overworld_->entrances();
|
||||
EXPECT_EQ(entrances.size(), 129);
|
||||
|
||||
// Test coordinate calculation matches ZScream logic exactly
|
||||
for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
|
||||
const auto& entrance = entrances[i];
|
||||
|
||||
// ZScream coordinate calculation:
|
||||
// int p = mapPos >> 1;
|
||||
// int x = p % 64;
|
||||
// int y = p >> 6;
|
||||
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
|
||||
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
|
||||
|
||||
uint16_t map_pos = entrance.map_pos_;
|
||||
uint16_t map_id = entrance.map_id_;
|
||||
|
||||
int position = map_pos >> 1;
|
||||
int x_coord = position % 64;
|
||||
int y_coord = position >> 6;
|
||||
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
|
||||
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
|
||||
|
||||
EXPECT_EQ(entrance.x_, expected_x);
|
||||
EXPECT_EQ(entrance.y_, expected_y);
|
||||
}
|
||||
|
||||
// Test hole coordinate calculation with 0x400 offset
|
||||
const auto& holes = overworld_->holes();
|
||||
EXPECT_EQ(holes.size(), 0x13);
|
||||
|
||||
for (int i = 0; i < std::min(5, static_cast<int>(holes.size())); i++) {
|
||||
const auto& hole = holes[i];
|
||||
|
||||
// ZScream hole coordinate calculation:
|
||||
// int p = (mapPos + 0x400) >> 1;
|
||||
// int x = p % 64;
|
||||
// int y = p >> 6;
|
||||
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
|
||||
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
|
||||
|
||||
uint16_t map_pos = hole.map_pos_;
|
||||
uint16_t map_id = hole.map_id_;
|
||||
|
||||
int position = map_pos >> 1;
|
||||
int x_coord = position % 64;
|
||||
int y_coord = position >> 6;
|
||||
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
|
||||
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
|
||||
|
||||
EXPECT_EQ(hole.x_, expected_x);
|
||||
EXPECT_EQ(hole.y_, expected_y);
|
||||
EXPECT_TRUE(hole.is_hole_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
327
test/integration/zelda3/room_integration_test.cc
Normal file
327
test/integration/zelda3/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 "zelda3/dungeon/room.h"
|
||||
#include "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
|
||||
|
||||
157
test/integration/zelda3/sprite_position_test.cc
Normal file
157
test/integration/zelda3/sprite_position_test.cc
Normal file
@@ -0,0 +1,157 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <fstream>
|
||||
|
||||
#include "app/rom.h"
|
||||
#include "zelda3/overworld/overworld.h"
|
||||
#include "zelda3/overworld/overworld_map.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
class SpritePositionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Try to load a vanilla ROM for testing
|
||||
rom_ = std::make_unique<Rom>();
|
||||
std::string rom_path = "bin/zelda3.sfc";
|
||||
|
||||
// Check if ROM exists in build directory
|
||||
std::ifstream rom_file(rom_path);
|
||||
if (rom_file.good()) {
|
||||
ASSERT_TRUE(rom_->LoadFromFile(rom_path).ok()) << "Failed to load ROM from " << rom_path;
|
||||
} else {
|
||||
// Skip test if ROM not found
|
||||
GTEST_SKIP() << "ROM file not found at " << rom_path;
|
||||
}
|
||||
|
||||
overworld_ = std::make_unique<Overworld>(rom_.get());
|
||||
ASSERT_TRUE(overworld_->Load(rom_.get()).ok()) << "Failed to load overworld";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
overworld_.reset();
|
||||
rom_.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
std::unique_ptr<Overworld> overworld_;
|
||||
};
|
||||
|
||||
// Test sprite coordinate system understanding
|
||||
TEST_F(SpritePositionTest, SpriteCoordinateSystem) {
|
||||
// Test sprites from different worlds
|
||||
for (int game_state = 0; game_state < 3; game_state++) {
|
||||
const auto& sprites = overworld_->sprites(game_state);
|
||||
std::cout << "\n=== Game State " << game_state << " ===" << std::endl;
|
||||
std::cout << "Total sprites: " << sprites.size() << std::endl;
|
||||
|
||||
int sprite_count = 0;
|
||||
for (const auto& sprite : sprites) {
|
||||
if (!sprite.deleted() && sprite_count < 10) { // Show first 10 sprites
|
||||
std::cout << "Sprite " << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< static_cast<int>(sprite.id()) << " (" << const_cast<Sprite&>(sprite).name() << ")" << std::endl;
|
||||
std::cout << " Map ID: 0x" << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< sprite.map_id() << std::endl;
|
||||
std::cout << " X: " << std::dec << sprite.x() << " (0x" << std::hex << sprite.x() << ")" << std::endl;
|
||||
std::cout << " Y: " << std::dec << sprite.y() << " (0x" << std::hex << sprite.y() << ")" << std::endl;
|
||||
std::cout << " map_x: " << std::dec << sprite.map_x() << std::endl;
|
||||
std::cout << " map_y: " << std::dec << sprite.map_y() << std::endl;
|
||||
|
||||
// Calculate expected world ranges
|
||||
int world_start = game_state * 0x40;
|
||||
int world_end = world_start + 0x40;
|
||||
std::cout << " World range: 0x" << std::hex << world_start << " - 0x" << world_end << std::endl;
|
||||
|
||||
sprite_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test sprite filtering logic
|
||||
TEST_F(SpritePositionTest, SpriteFilteringLogic) {
|
||||
// Test the filtering logic used in DrawOverworldSprites
|
||||
for (int current_world = 0; current_world < 3; current_world++) {
|
||||
const auto& sprites = overworld_->sprites(current_world);
|
||||
|
||||
std::cout << "\n=== Testing World " << current_world << " Filtering ===" << std::endl;
|
||||
|
||||
int visible_sprites = 0;
|
||||
int total_sprites = 0;
|
||||
|
||||
for (const auto& sprite : sprites) {
|
||||
if (!sprite.deleted()) {
|
||||
total_sprites++;
|
||||
|
||||
// This is the filtering logic from DrawOverworldSprites
|
||||
bool should_show = (sprite.map_id() < 0x40 + (current_world * 0x40) &&
|
||||
sprite.map_id() >= (current_world * 0x40));
|
||||
|
||||
if (should_show) {
|
||||
visible_sprites++;
|
||||
std::cout << " Visible: Sprite 0x" << std::hex << static_cast<int>(sprite.id())
|
||||
<< " on map 0x" << sprite.map_id() << " at ("
|
||||
<< std::dec << sprite.x() << ", " << sprite.y() << ")" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "World " << current_world << ": " << visible_sprites << "/"
|
||||
<< total_sprites << " sprites visible" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Test map coordinate calculations
|
||||
TEST_F(SpritePositionTest, MapCoordinateCalculations) {
|
||||
// Test how map coordinates should be calculated
|
||||
for (int current_world = 0; current_world < 3; current_world++) {
|
||||
const auto& sprites = overworld_->sprites(current_world);
|
||||
|
||||
std::cout << "\n=== World " << current_world << " Coordinate Analysis ===" << std::endl;
|
||||
|
||||
for (const auto& sprite : sprites) {
|
||||
if (!sprite.deleted() &&
|
||||
sprite.map_id() < 0x40 + (current_world * 0x40) &&
|
||||
sprite.map_id() >= (current_world * 0x40)) {
|
||||
|
||||
// Calculate map position
|
||||
int sprite_map_id = sprite.map_id();
|
||||
int local_map_index = sprite_map_id - (current_world * 0x40);
|
||||
int map_col = local_map_index % 8;
|
||||
int map_row = local_map_index / 8;
|
||||
|
||||
int map_canvas_x = map_col * 512; // kOverworldMapSize
|
||||
int map_canvas_y = map_row * 512;
|
||||
|
||||
std::cout << "Sprite 0x" << std::hex << static_cast<int>(sprite.id())
|
||||
<< " on map 0x" << sprite_map_id << std::endl;
|
||||
std::cout << " Local map index: " << std::dec << local_map_index << std::endl;
|
||||
std::cout << " Map position: (" << map_col << ", " << map_row << ")" << std::endl;
|
||||
std::cout << " Map canvas pos: (" << map_canvas_x << ", " << map_canvas_y << ")" << std::endl;
|
||||
std::cout << " Sprite global pos: (" << sprite.x() << ", " << sprite.y() << ")" << std::endl;
|
||||
std::cout << " Sprite local pos: (" << sprite.map_x() << ", " << sprite.map_y() << ")" << std::endl;
|
||||
|
||||
// Verify the calculation
|
||||
int expected_global_x = map_canvas_x + sprite.map_x();
|
||||
int expected_global_y = map_canvas_y + sprite.map_y();
|
||||
|
||||
std::cout << " Expected global: (" << expected_global_x << ", " << expected_global_y << ")" << std::endl;
|
||||
std::cout << " Actual global: (" << sprite.x() << ", " << sprite.y() << ")" << std::endl;
|
||||
|
||||
if (expected_global_x == sprite.x() && expected_global_y == sprite.y()) {
|
||||
std::cout << " ✓ Coordinates match!" << std::endl;
|
||||
} else {
|
||||
std::cout << " ✗ Coordinate mismatch!" << std::endl;
|
||||
}
|
||||
|
||||
break; // Only test first sprite for brevity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
Reference in New Issue
Block a user