3#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
17#include "absl/status/status.h"
27#include "nlohmann/json.hpp"
30#if __has_include("imgui_test_engine/imgui_te_context.h")
31#include "imgui_test_engine/imgui_te_context.h"
32#elif __has_include("imgui_te_context.h")
33#include "imgui_te_context.h"
35#error "ImGui Test Engine context header not found"
38#if __has_include("imgui_test_engine/imgui_te_engine.h")
39#include "imgui_test_engine/imgui_te_engine.h"
40#elif __has_include("imgui_te_engine.h")
41#include "imgui_te_engine.h"
43#error "ImGui Test Engine engine header not found"
50void EnsureTile16PanelOpenAndFocused(ImGuiTestContext* ctx) {
51 ctx->SetRef(
"Tile16 Editor");
52 if (ctx->GetWindowByRef(
"") ==
nullptr) {
53 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_T);
57 ctx->SetRef(
"Tile16 Editor");
58 IM_CHECK(ctx->GetWindowByRef(
"") !=
nullptr);
63void ExpectActiveQuadrant(ImGuiTestContext* ctx,
const char* label) {
64 const std::string pattern = std::string(
"**/Active ") + label +
":*";
65 IM_CHECK(ctx->ItemExists(pattern.c_str()));
68void ExpectBrushPalette(ImGuiTestContext* ctx,
int palette) {
69 const std::string pattern =
70 std::string(
"**/*Brush Palette: ") + std::to_string(palette) +
"*";
71 IM_CHECK(ctx->ItemExists(pattern.c_str()));
89static void RegisterKeyboardShortcutTests(ImGuiTestEngine* engine) {
93 IM_REGISTER_TEST(engine,
"overworld_keys",
"mouse_mode_key_1");
94 test->TestFunc = [](ImGuiTestContext* ctx) {
95 ctx->KeyPress(ImGuiKey_1);
103 IM_REGISTER_TEST(engine,
"overworld_keys",
"draw_mode_key_2");
104 test->TestFunc = [](ImGuiTestContext* ctx) {
105 ctx->KeyPress(ImGuiKey_2);
108 ctx->KeyPress(ImGuiKey_1);
116 IM_REGISTER_TEST(engine,
"overworld_keys",
"brush_toggle_key_b");
117 test->TestFunc = [](ImGuiTestContext* ctx) {
118 ctx->KeyPress(ImGuiKey_B);
121 ctx->KeyPress(ImGuiKey_B);
129 IM_REGISTER_TEST(engine,
"overworld_keys",
"fill_tool_key_f");
130 test->TestFunc = [](ImGuiTestContext* ctx) {
131 ctx->KeyPress(ImGuiKey_F);
134 ctx->KeyPress(ImGuiKey_F);
142 IM_REGISTER_TEST(engine,
"overworld_keys",
"eyedropper_key_i");
143 test->TestFunc = [](ImGuiTestContext* ctx) {
144 ctx->KeyPress(ImGuiKey_I);
152 IM_REGISTER_TEST(engine,
"overworld_keys",
"lock_toggle_ctrl_l");
153 test->TestFunc = [](ImGuiTestContext* ctx) {
154 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_L);
157 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_L);
165 IM_REGISTER_TEST(engine,
"overworld_keys",
"fullscreen_toggle_f11");
166 test->TestFunc = [](ImGuiTestContext* ctx) {
167 ctx->KeyPress(ImGuiKey_F11);
170 ctx->KeyPress(ImGuiKey_F11);
178 IM_REGISTER_TEST(engine,
"overworld_keys",
"undo_no_crash");
179 test->TestFunc = [](ImGuiTestContext* ctx) {
180 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_Z);
188 IM_REGISTER_TEST(engine,
"overworld_keys",
"redo_no_crash");
189 test->TestFunc = [](ImGuiTestContext* ctx) {
190 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_Y);
198 IM_REGISTER_TEST(engine,
"overworld_keys",
"tile16_editor_ctrl_t");
199 test->TestFunc = [](ImGuiTestContext* ctx) {
200 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_T);
203 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_T);
210 ImGuiTest* test = IM_REGISTER_TEST(engine,
"overworld_keys",
211 "tile16_quadrant_hotkeys_apply_focus");
212 test->TestFunc = [](ImGuiTestContext* ctx) {
213 EnsureTile16PanelOpenAndFocused(ctx);
215 struct QuadrantStep {
219 const QuadrantStep steps[] = {
226 for (
const auto& step : steps) {
227 ctx->KeyPress(step.key);
229 ExpectActiveQuadrant(ctx, step.label);
232 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_T);
239 ImGuiTest* test = IM_REGISTER_TEST(
240 engine,
"overworld_keys",
"tile16_ctrl_numeric_hotkeys_palette_rows");
241 test->TestFunc = [](ImGuiTestContext* ctx) {
242 EnsureTile16PanelOpenAndFocused(ctx);
244 ctx->KeyPress(ImGuiKey_4);
246 ExpectActiveQuadrant(ctx,
"BR");
248 for (
int palette = 0; palette < 8; ++palette) {
249 const ImGuiKey number_key =
static_cast<ImGuiKey
>(ImGuiKey_1 + palette);
250 ctx->KeyPress(ImGuiMod_Ctrl | number_key);
252 ExpectBrushPalette(ctx, palette);
253 ExpectActiveQuadrant(ctx,
"BR");
256 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_T);
270static void RegisterEntityModeTests(ImGuiTestEngine* engine) {
274 IM_REGISTER_TEST(engine,
"overworld_entity",
"mode_keys_3_to_8");
275 test->TestFunc = [](ImGuiTestContext* ctx) {
277 ctx->KeyPress(ImGuiKey_3);
279 ctx->KeyPress(ImGuiKey_4);
281 ctx->KeyPress(ImGuiKey_5);
283 ctx->KeyPress(ImGuiKey_6);
285 ctx->KeyPress(ImGuiKey_7);
287 ctx->KeyPress(ImGuiKey_8);
290 ctx->KeyPress(ImGuiKey_1);
303static void RegisterModeRoundTripTests(ImGuiTestEngine* engine) {
307 IM_REGISTER_TEST(engine,
"overworld_modes",
"full_mode_cycle");
308 test->TestFunc = [](ImGuiTestContext* ctx) {
310 ctx->KeyPress(ImGuiKey_1);
314 ctx->KeyPress(ImGuiKey_2);
318 ctx->KeyPress(ImGuiKey_F);
322 ctx->KeyPress(ImGuiKey_B);
326 ctx->KeyPress(ImGuiKey_B);
330 ctx->KeyPress(ImGuiKey_1);
338 IM_REGISTER_TEST(engine,
"overworld_modes",
"rapid_mode_switch");
339 test->TestFunc = [](ImGuiTestContext* ctx) {
340 for (
int i = 0; i < 10; ++i) {
341 ctx->KeyPress(ImGuiKey_B);
345 ctx->KeyPress(ImGuiKey_1);
364static void RegisterCanvasNavigationTests(ImGuiTestEngine* engine) {
368 IM_REGISTER_TEST(engine,
"overworld_canvas",
"idle_frames_stable");
369 test->TestFunc = [](ImGuiTestContext* ctx) {
385static void RegisterWorldSelectorTests(ImGuiTestEngine* engine) {
389 IM_REGISTER_TEST(engine,
"overworld_toolbar",
"toolbar_renders_stable");
390 test->TestFunc = [](ImGuiTestContext* ctx) {
402static void RegisterLyraTerrainAcceptanceTest(ImGuiTestEngine* engine) {
403 const char* requested = std::getenv(
"YAZE_LYRA_TERRAIN_ACCEPTANCE_PROJECT");
404 if (!requested || !*requested ||
405 std::getenv(
"YAZE_LYRA_TERRAIN_REOPEN_PROJECT"))
407 ImGuiTestEngine_GetIO(engine).ConfigLogToTTY =
true;
408 ImGuiTest* test = IM_REGISTER_TEST(engine,
"lyra_terrain",
409 "cross_map_stroke_undo_redo_project_save");
410 test->TestFunc = [](ImGuiTestContext* ctx) {
411 namespace fs = std::filesystem;
412 const char* requested = std::getenv(
"YAZE_LYRA_TERRAIN_ACCEPTANCE_PROJECT");
413 IM_CHECK(requested && *requested);
414 IM_CHECK(fs::path(requested).is_absolute());
416 const auto expected_project = fs::canonical(requested, ec);
419 editor::EditorManager* manager =
nullptr;
421 for (
int frame = 0; frame < 200; ++frame) {
437 const auto project_directory = expected_project.parent_path();
438 const auto rom_path = fs::canonical(
440 IM_CHECK(!ec && rom_path.parent_path() == project_directory);
441 IM_CHECK(rom_path.filename() ==
"main-with-d3.sfc");
442 auto*
rom = manager->GetCurrentRom();
444 IM_CHECK(fs::canonical(
rom->
filename(), ec) == rom_path && !ec);
446 const auto read_file =
447 [](
const fs::path& path) -> std::optional<std::string> {
448 std::ifstream file(path, std::ios::binary);
451 std::string bytes{std::istreambuf_iterator<char>(file), {}};
452 return file.bad() ? std::nullopt
453 : std::optional<std::string>(std::move(bytes));
455 const auto rom_disk_before = read_file(rom_path);
456 IM_CHECK(rom_disk_before.has_value());
458 const auto source = fs::canonical(
461 IM_CHECK(!ec && source.parent_path() == project_directory);
464 manager->OpenEditorAndPanelsFromFlags(
465 "Overworld",
"overworld.canvas,overworld.tile16_selector");
467 editor::OverworldEditor* overworld =
nullptr;
468 for (
int frame = 0; frame < 200; ++frame) {
469 auto* editors = manager->GetCurrentEditorSet();
470 overworld = editors ? editors->GetEditorAs<editor::OverworldEditor>(
473 if (overworld && overworld->overworld().is_loaded() &&
474 overworld->lyra_canvas_.loaded() &&
475 widgets.GetWidgetId(
"OverworldToolbar/world:2") != 0 &&
476 widgets.GetWidgetId(
"OwBlocksetSelector/button:Lookup") != 0 &&
477 ctx->ItemExists(
"**/OwMap"))
481 IM_CHECK(overworld && overworld->overworld().is_loaded());
482 if (!overworld->lyra_canvas_status_.ok()) {
483 IM_ERRORF(
"Native Loom binding failed: %s",
484 overworld->lyra_canvas_status_.ToString().c_str());
487 IM_CHECK(overworld->lyra_canvas_.loaded());
490 IM_CHECK_EQ(overworld->current_world_id(), 2);
491 IM_CHECK_EQ(overworld->current_map_id(), 0xA0);
492 IM_CHECK_EQ(overworld->DisplayedMapCount(), 192);
493 IM_CHECK(!overworld->IsProjectTerrainMap(0x80));
494 IM_CHECK(overworld->IsProjectTerrainMap(0xA0));
495 IM_CHECK(overworld->IsProjectTerrainMap(0xBF));
496 const auto* document = overworld->lyra_canvas_.document();
497 IM_CHECK(document !=
nullptr && !document->dirty());
498 const auto source_path = fs::weakly_canonical(
501 IM_CHECK(!ec && source_path.parent_path() == project_directory);
502 IM_CHECK(source_path.extension() ==
".json");
505 ctx->ItemInfo(widgets.GetWidgetId(
"OverworldToolbar/world:2"));
506 IM_CHECK(world.ID != 0 && world.Window !=
nullptr);
507 ctx->SetRef(world.Window);
508 ctx->WindowFocus(
"");
509 ctx->ItemClick(world.ID);
511 IM_CHECK_EQ(overworld->current_world_id(), 2);
516 constexpr int kX = 4;
517 constexpr int kStartY = 30;
518 constexpr int kLyraWorldY = 128;
519 auto& canvas = overworld->ow_map_canvas();
520 canvas.set_global_scale(0.25f);
521 overworld->LocateOverworldTile(0xA0, kX, kLyraWorldY + 48);
523 auto terrain = ctx->ItemInfo(
"**/OwMap");
524 IM_CHECK(terrain.ID != 0 && terrain.Window !=
nullptr);
525 const auto cell_position = [&](
int y) {
526 const auto origin = canvas.zero_point();
527 const auto scrolling = canvas.scrolling();
528 const float cell_pixels = 16.0f * canvas.global_scale();
530 origin.x + scrolling.x + (kX + 0.5f) * cell_pixels,
531 origin.y + scrolling.y + (kLyraWorldY + y + 0.5f) * cell_pixels);
533 const bool reaches_b0 = terrain.RectClipped.Contains(cell_position(65));
534 const int end_y = reaches_b0 ? 65 : 33;
535 IM_CHECK(terrain.RectClipped.Contains(cell_position(kStartY)));
536 IM_CHECK(terrain.RectClipped.Contains(cell_position(end_y)));
540 std::array<bool, 8> used{};
541 for (
const int y : {31, 32, end_y}) {
542 const auto tile = document->tile(kX, y);
544 if (*tile < used.size())
547 const int old_brush = overworld->current_tile16_id();
548 if (old_brush >= 0 && old_brush <
static_cast<int>(used.size()))
549 used[old_brush] =
true;
551 while (brush <
static_cast<int>(used.size()) && used[brush])
553 IM_CHECK(brush < 8 &&
554 brush <
static_cast<int>(document->profile().tile_count));
559 auto* toasts = manager->toast_manager();
560 IM_CHECK(toasts !=
nullptr);
561 if (toasts->pending_count() != 0) {
564 ctx->MouseSetViewport(terrain.Window);
565 ctx->MouseMoveToPos(cell_position(kStartY));
566 const auto deadline =
567 std::chrono::steady_clock::now() + std::chrono::seconds(15);
568 while (toasts->pending_count() != 0 &&
569 std::chrono::steady_clock::now() < deadline && !ctx->IsError())
571 IM_CHECK_EQ(toasts->pending_count(), 0);
577 const auto visible_widget = [&](
const std::string& suffix) -> ImGuiID {
578 const std::string tail =
"/" + suffix;
579 for (
const auto& path : widgets.FindWidgets(
"*" + suffix)) {
582 (path.size() > tail.size() &&
583 path.compare(path.size() - tail.size(), tail.size(), tail) == 0);
584 const ImGuiID
id = match ? widgets.GetWidgetId(path) : 0;
585 if (
id != 0 && ctx->ItemExists(
id))
594 constexpr int kLegacyMap = 0x3A;
595 const bool selector_was_pinned = overworld->map_pinned();
596 if (!selector_was_pinned)
597 overworld->ToggleMapLock();
598 manager->OpenEditorAndPanelsFromFlags(
"Overworld",
599 "overworld.tile16_selector");
601 const auto selector_window =
602 manager->window_manager().GetWorkspaceWindowName(
603 manager->GetCurrentSessionId(),
"overworld.tile16_selector");
604 ctx->SetRef(selector_window.c_str());
605 ImGuiWindow* selector = ctx->GetWindowByRef(
"");
606 IM_CHECK(selector !=
nullptr);
607 ctx->WindowFocus(
"");
611 const std::string grid_prefix = std::string(selector->Name) +
"/";
612 const auto tile16_grid = [&]() -> ImGuiWindow* {
613 for (ImGuiWindow* window : ctx->UiContext->Windows) {
614 const std::string
name = window->Name;
615 if (window->WasActive &&
name.rfind(grid_prefix, 0) == 0 &&
616 name.find(
"/##Tile16Grid") != std::string::npos)
624 const auto click_cell = [&](ImGuiWindow* grid,
int index) ->
int {
627 (index % kColumns + 0.5f) * cell,
628 grid->Pos.y - grid->Scroll.y + (index / kColumns + 0.5f) * cell);
629 if (!grid->InnerClipRect.Contains(pos))
631 const auto name = [](
const ImGuiWindow* window) {
632 return window !=
nullptr ? window->Name :
"(null)";
634 ctx->SetRef(selector);
635 const auto canvas_item =
636 ctx->ItemInfo(grid->GetID(
"OwBlockset"), ImGuiTestOpFlags_NoError);
637 const ImRect& full = canvas_item.RectFull;
638 const ImRect& clipped = canvas_item.RectClipped;
640 "Tile16 click map=0x%02X selected=0x%03X requested=0x%03X "
641 "cell=%.2f grid=%s pos=(%.1f,%.1f) scroll=(%.1f,%.1f) "
642 "target=(%.1f,%.1f) canvas_id=0x%08X canvas_window=%s "
643 "full=(%.1f,%.1f)-(%.1f,%.1f) clipped=(%.1f,%.1f)-(%.1f,%.1f) "
644 "in_canvas=%d hovered=%s",
645 overworld->current_map_id(), overworld->current_tile16_id(), index,
646 cell,
name(grid), grid->Pos.x, grid->Pos.y, grid->Scroll.x,
647 grid->Scroll.y, pos.x, pos.y, canvas_item.ID,
648 name(canvas_item.Window), full.Min.x, full.Min.y, full.Max.x,
649 full.Max.y, clipped.Min.x, clipped.Min.y, clipped.Max.x,
650 clipped.Max.y, canvas_item.ID != 0 && clipped.Contains(pos),
651 name(ctx->UiContext->HoveredWindow));
652 ctx->MouseSetViewport(grid);
653 ctx->MouseMoveToPos(pos);
654 ctx->LogInfo(
"Tile16 click moved hovered=%s",
655 name(ctx->UiContext->HoveredWindow));
656 ctx->MouseClick(ImGuiMouseButton_Left);
658 ctx->SleepNoSkip(0.5f, 1.0f / 60.0f);
659 ctx->LogInfo(
"Tile16 click requested=0x%03X selected=0x%03X hovered=%s",
660 index, overworld->current_tile16_id(),
661 name(ctx->UiContext->HoveredWindow));
662 return overworld->current_tile16_id();
664 const auto lookup_tile = [&](
int tile) ->
bool {
665 const ImGuiID button =
666 widgets.GetWidgetId(
"OwBlocksetSelector/button:Lookup");
669 ctx->ItemClick(button);
671 const ImGuiID input =
672 widgets.GetWidgetId(
"OwBlocksetSelector/input:TileID");
676 std::snprintf(hex,
sizeof(hex),
"%X", tile);
677 ctx->ItemInputValue(input, hex);
679 return overworld->current_tile16_id() == tile;
683 const auto check_lyra_grid = [&](
int map_id) ->
bool {
684 overworld->JumpToMap(map_id);
686 ImGuiWindow* grid = tile16_grid();
687 if (overworld->current_map_id() != map_id || grid ==
nullptr)
690 if (!lookup_tile(0x800))
692 const float top = grid->Scroll.y;
693 if (!lookup_tile(0x880))
695 cell = std::round((grid->Scroll.y - top) / 16.0f);
701 return lookup_tile(0xFFF) && click_cell(grid, 0xFF8) == 0xFF8 &&
702 click_cell(grid, 0xFFF) == 0xFFF;
704 overworld->JumpToMap(kLegacyMap);
705 ImGuiID scope_button = 0;
706 for (
int frame = 0; frame < 120 && scope_button == 0 && !ctx->IsError();
709 if (overworld->current_map_id() == kLegacyMap)
710 scope_button = visible_widget(
"button:tile16_usage_scope");
712 IM_CHECK_EQ(overworld->current_map_id(), kLegacyMap);
713 IM_CHECK(scope_button != 0);
714 ctx->ItemClick(scope_button);
716 ImGuiWindow* scope_popup = ctx->GetWindowByRef(
"//$FOCUSED");
717 IM_CHECK(scope_popup !=
nullptr &&
718 (scope_popup->Flags & ImGuiWindowFlags_Popup) != 0);
719 ctx->SetRef(scope_popup);
720 ctx->ItemClick(
"Current area");
721 ctx->ItemCheck(
"Hide unused tiles");
722 IM_CHECK(ctx->ItemIsChecked(
"Hide unused tiles"));
723 ctx->PopupCloseAll();
725 const std::string legacy_scope = ctx->ItemInfo(scope_button).DebugLabel;
726 IM_CHECK(legacy_scope.find(
'*') != std::string::npos);
728 for (
const int map_id : {0xA0, 0xA1})
729 IM_CHECK(check_lyra_grid(map_id));
731 overworld->JumpToMap(kLegacyMap);
733 for (
int frame = 0; frame < 120 && scope_button == 0 && !ctx->IsError();
736 if (overworld->current_map_id() == kLegacyMap)
737 scope_button = visible_widget(
"button:tile16_usage_scope");
739 IM_CHECK_EQ(overworld->current_map_id(), kLegacyMap);
740 IM_CHECK(scope_button != 0);
742 IM_CHECK(ctx->ItemInfo(scope_button).DebugLabel == legacy_scope);
743 ctx->ItemClick(scope_button);
745 scope_popup = ctx->GetWindowByRef(
"//$FOCUSED");
746 IM_CHECK(scope_popup !=
nullptr &&
747 (scope_popup->Flags & ImGuiWindowFlags_Popup) != 0);
748 ctx->SetRef(scope_popup);
749 IM_CHECK(ctx->ItemIsChecked(
"Current area"));
750 IM_CHECK(ctx->ItemIsChecked(
"Hide unused tiles"));
751 ctx->PopupCloseAll();
754 canvas.set_global_scale(0.25f);
755 overworld->LocateOverworldTile(0xA0, kX, kLyraWorldY + 48);
757 if (!selector_was_pinned)
758 overworld->ToggleMapLock();
759 IM_CHECK_EQ(overworld->current_world_id(), 2);
760 IM_CHECK_EQ(overworld->current_map_id(), 0xA0);
763 ctx->ItemInfo(widgets.GetWidgetId(
"OwBlocksetSelector/button:Lookup"));
764 IM_CHECK(lookup.ID != 0 && lookup.Window !=
nullptr);
765 ctx->SetRef(lookup.Window);
766 ctx->WindowFocus(
"");
767 ctx->ItemClick(lookup.ID);
769 const ImGuiID tile_input =
770 widgets.GetWidgetId(
"OwBlocksetSelector/input:TileID");
771 IM_CHECK(tile_input != 0);
773 ctx->ItemInputValue(tile_input, std::to_string(brush).c_str());
775 IM_CHECK_EQ(overworld->current_tile16_id(), brush);
776 ctx->SetRef(world.Window);
777 ctx->WindowFocus(
"");
778 ImGuiID paint = widgets.GetWidgetId(
"OverworldToolbar/button:Paint");
779 const bool tools_overflow = paint == 0 || !ctx->ItemExists(paint);
780 if (tools_overflow) {
781 const ImGuiID tools =
782 widgets.GetWidgetId(
"OverworldToolbar/button:Tools");
783 IM_CHECK(tools != 0);
784 ctx->ItemClick(tools);
786 paint = widgets.GetWidgetId(
"OverworldToolbar/button:Paint");
788 IM_CHECK(paint != 0);
789 ctx->ItemClick(paint);
791 ctx->KeyPress(ImGuiKey_Escape);
794 std::make_unique<core::LyraTerrainDocument::Maps>(document->maps());
795 const size_t undo_before = overworld->undo_manager().UndoStackSize();
796 IM_CHECK(undo_before < overworld->undo_manager().GetMaxStackSize());
798 terrain = ctx->ItemInfo(
"**/OwMap");
799 IM_CHECK(terrain.RectClipped.Contains(cell_position(kStartY)));
800 IM_CHECK(terrain.RectClipped.Contains(cell_position(end_y)));
801 ctx->MouseSetViewport(terrain.Window);
802 ctx->MouseMoveToPos(cell_position(kStartY));
803 ctx->MouseDown(ImGuiMouseButton_Left);
805 ctx->MouseMoveToPos(cell_position(32));
807 ctx->MouseMoveToPos(cell_position(end_y));
809 ctx->MouseUp(ImGuiMouseButton_Left);
812 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), undo_before + 1);
813 IM_CHECK(document->dirty());
814 for (
int y = kStartY; y <= end_y; ++y)
815 IM_CHECK(document->tile(kX, y).value() == brush);
817 std::make_unique<core::LyraTerrainDocument::Maps>(document->maps());
818 IM_CHECK(manager->GetCurrentEditor() == overworld);
819 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_Z);
821 IM_CHECK(overworld->lyra_canvas_status_.ok());
822 IM_CHECK(document->maps() == *before);
823 IM_CHECK(!document->dirty());
824 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), undo_before);
825 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z);
827 IM_CHECK(overworld->lyra_canvas_status_.ok());
828 IM_CHECK(document->maps() == *painted);
829 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), undo_before + 1);
835 const auto xy_position = [&](
int x,
int y) {
836 const auto origin = canvas.zero_point();
837 const auto scrolling = canvas.scrolling();
838 const float size = 16.0f * canvas.global_scale();
839 return ImVec2(origin.x + scrolling.x + (x + 0.5f) * size,
840 origin.y + scrolling.y + (kLyraWorldY + y + 0.5f) * size);
842 editor::TileBrush rectangle{4, 3, {}};
843 for (
int y = 31; y <= 33; ++y)
844 for (
int x = kX; x <
kX + rectangle.width; ++x)
845 rectangle.tile_ids.push_back(document->tile(x, y).value());
846 ctx->MouseMoveToPos(xy_position(kX, 31));
847 ctx->MouseDown(ImGuiMouseButton_Right);
849 ctx->MouseMoveToPos(xy_position(kX + 3, 33));
851 ctx->MouseUp(ImGuiMouseButton_Right);
853 IM_CHECK(overworld->tile_painting()->selection_brush() !=
nullptr);
854 IM_CHECK_EQ(overworld->tile_painting()->selection_brush()->width, 4);
855 IM_CHECK_EQ(overworld->tile_painting()->selection_brush()->height, 3);
856 IM_CHECK(overworld->tile_painting()->selection_brush()->tile_ids ==
858 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_C);
860 const int paste_y = reaches_b0 ? 63 : 31;
861 IM_CHECK(terrain.RectClipped.Contains(xy_position(11, paste_y + 2)));
862 ctx->MouseMoveToPos(xy_position(8, paste_y));
864 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_V);
866 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), undo_before + 2);
867 for (
int y = 0; y < rectangle.height; ++y)
868 for (
int x = 0; x < rectangle.width; ++x)
869 IM_CHECK(document->tile(8 + x, paste_y + y).value() ==
872 std::make_unique<core::LyraTerrainDocument::Maps>(document->maps());
873 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_Z);
875 IM_CHECK(document->maps() == *painted);
876 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z);
878 IM_CHECK(document->maps() == *pasted);
882 ctx->MouseMoveToPos(xy_position(16, paste_y));
883 ctx->MouseClick(ImGuiMouseButton_Left);
885 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), undo_before + 3);
886 for (
int y = 0; y < rectangle.height; ++y)
887 for (
int x = 0; x < rectangle.width; ++x)
888 IM_CHECK(document->tile(16 + x, paste_y + y).value() ==
890 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_Z);
892 IM_CHECK(document->maps() == *pasted);
893 ImGuiID fill = widgets.GetWidgetId(
"OverworldToolbar/button:Fill");
894 const bool fill_overflow = fill == 0 || !ctx->ItemExists(fill);
896 const auto tools = widgets.GetWidgetId(
"OverworldToolbar/button:Tools");
897 IM_CHECK(tools != 0);
898 ctx->ItemClick(tools);
900 fill = widgets.GetWidgetId(
"OverworldToolbar/button:Fill");
903 ctx->ItemClick(fill);
905 ctx->KeyPress(ImGuiKey_Escape);
906 overworld->LocateOverworldTile(0xA9, 48, kLyraWorldY + 48);
908 terrain = ctx->ItemInfo(
"**/OwMap");
909 IM_CHECK(terrain.RectClipped.Contains(xy_position(48, 48)));
910 ctx->MouseSetViewport(terrain.Window);
911 ctx->MouseMoveToPos(xy_position(48, 48));
912 ctx->MouseClick(ImGuiMouseButton_Left);
914 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), undo_before + 3);
915 for (
int y = 0; y < 32; ++y)
916 for (
int x = 0; x < 32; ++x)
917 IM_CHECK(document->tile(32 + x, 32 + y).value() ==
918 rectangle.at(x % rectangle.width, y % rectangle.height));
920 std::make_unique<core::LyraTerrainDocument::Maps>(document->maps());
921 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiKey_Z);
923 IM_CHECK(document->maps() == *pasted);
924 ctx->KeyPress(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z);
926 IM_CHECK(document->maps() == *filled);
927 editor::UndoManager rejected_history;
928 const auto rejected = overworld->lyra_canvas_.PaintPattern(
929 {2, 1, {brush,
static_cast<int>(document->profile().tile_count)}}, 0, 0,
930 2, 1,
true, rejected_history);
931 IM_CHECK(!rejected.ok());
932 IM_CHECK_EQ(rejected_history.UndoStackSize(), 0);
933 IM_CHECK(document->maps() == *filled);
934 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), undo_before + 3);
939 const bool was_pinned = overworld->map_pinned();
941 overworld->ToggleMapLock();
942 overworld->set_current_map(0xA0);
943 manager->OpenEditorAndPanelsFromFlags(
"Overworld",
"overworld.properties");
945 const auto properties_window =
946 manager->window_manager().GetWorkspaceWindowName(
947 manager->GetCurrentSessionId(),
"overworld.properties");
948 ctx->SetRef(properties_window.c_str());
949 IM_CHECK(ctx->GetWindowByRef(
"") !=
nullptr);
950 ctx->WindowFocus(
"");
952 const auto property_input =
953 widgets.GetWidgetId(
"LyraMapProperties/input:MainPalette");
954 IM_CHECK(property_input != 0);
955 const auto property_item = ctx->ItemInfo(property_input);
956 IM_CHECK(property_item.Window !=
nullptr);
957 ctx->SetRef(property_item.Window);
958 ctx->WindowFocus(
"");
959 IM_CHECK_EQ(overworld->current_map_id(), 0xA0);
960 const auto authoring_before =
961 std::make_unique<core::LyraAuthoringState>(document->authoring());
962 const int next_palette =
963 document->authoring().maps[0].properties.main_palette == 0 ? 1 : 0;
964 ctx->ItemInputValue(property_input, std::to_string(next_palette).c_str());
965 ctx->ItemClick(widgets.GetWidgetId(
"LyraMapProperties/button:Apply"));
967 IM_CHECK_EQ(document->authoring().maps[0].properties.main_palette,
969 IM_CHECK(overworld->Undo().ok());
970 IM_CHECK(document->authoring() == *authoring_before);
971 IM_CHECK(overworld->Redo().ok());
972 IM_CHECK_EQ(document->authoring().maps[0].properties.main_palette,
975 const auto sprite_count = document->authoring().maps[0].sprites.size();
976 ctx->ItemClick(widgets.GetWidgetId(
"LyraMapProperties/button:AddSprite"));
978 IM_CHECK_EQ(document->authoring().maps[0].sprites.size(), sprite_count + 1);
979 IM_CHECK_EQ(overworld->current_map_id(), 0xA0);
981 overworld->ToggleMapLock();
983 std::make_unique<core::LyraAuthoringState>(document->authoring());
984 authored->maps[1].properties.graphics_sheets[0] = 0;
985 authored->maps[1].properties.graphics_sheets[1] = 0xFF;
986 authored->maps[0].doors = {{900001, 9, 10, std::nullopt}};
987 authored->maps[0].exits = {{900002, 0x105, 11, 12, std::nullopt}};
988 authored->flight_points = {
989 {900003, 0xA0, 13, 14,
"Lyra test", std::nullopt},
990 {900004, 0x20, 15, 16,
"Legacy test", std::nullopt}};
991 const auto edit = overworld->EditProjectMapAuthoring(*authored);
993 const auto expected_authoring =
994 std::make_unique<core::LyraAuthoringState>(document->authoring());
995 IM_CHECK(overworld->Undo().ok());
996 IM_CHECK(overworld->Redo().ok());
997 IM_CHECK(document->authoring() == *expected_authoring);
998 for (
int state = 0; state < 3; ++state)
999 IM_CHECK(overworld->lyra_canvas_
1000 .PrepareMapAssets(overworld->overworld(), state)
1002 IM_CHECK(overworld->lyra_canvas_.PrepareMapAssets(overworld->overworld(), 0)
1004 const auto* source_map = overworld->overworld().overworld_map(0x3A);
1005 auto normal = *source_map;
1006 normal.ClearInheritedAreaProperties();
1007 normal.set_main_palette(next_palette);
1008 IM_CHECK(normal.LoadPalette().ok());
1009 IM_CHECK_EQ(overworld->lyra_canvas_.MapAtlas(0xA0).palette()[33].snes(),
1010 normal.current_palette()[33].snes());
1011 IM_CHECK_EQ(overworld->lyra_canvas_.MapGraphics(0xA1)->vector()[0],
1012 static_cast<uint8_t
>(
1013 source_map->game_data()->graphics_buffer[0] + 0x88));
1014 auto invalid_authoring =
1015 std::make_unique<core::LyraAuthoringState>(document->authoring());
1016 invalid_authoring->maps[0].properties.main_palette = 0xFE;
1017 IM_CHECK(!overworld->EditProjectMapAuthoring(*invalid_authoring).ok());
1018 IM_CHECK(document->authoring() == *expected_authoring);
1019 IM_CHECK(document->maps() == *filled);
1023 namespace room_labels = editor::dungeon_project_labels;
1024 constexpr int kRenamedRoom = 0x25;
1025 constexpr int kHoverRoom = 0x12;
1026 const std::string room_rename =
"Lyra Native Rename";
1027 const std::string room_key = std::to_string(kRenamedRoom);
1028 const auto default_room_label = [&](
const project::YazeProject& project) {
1029 const auto type = project.resource_labels.find(
"room");
1030 if (type == project.resource_labels.end())
1031 return std::string();
1032 const auto label = type->second.find(room_key);
1033 return label == type->second.end() ? std::string() : label->second;
1036 const std::string room_default = default_room_label(*current_project);
1037 IM_CHECK(!room_default.empty());
1038 manager->OpenEditorAndPanelsFromFlags(
"Dungeon",
"dungeon.workbench");
1039 const auto workbench_window =
1040 manager->window_manager().GetWorkspaceWindowName(
1041 manager->GetCurrentSessionId(),
"dungeon.workbench");
1042 ctx->SetRef(workbench_window.c_str());
1044 frame < 120 && ctx->GetWindowByRef(
"") ==
nullptr && !ctx->IsError();
1047 IM_CHECK(ctx->GetWindowByRef(
"") !=
nullptr);
1048 ctx->WindowFocus(
"");
1050 editor::DungeonEditorV2* dungeon =
nullptr;
1051 ImGuiID hover_cell = 0;
1052 for (
int frame = 0; frame < 600 && !ctx->IsError(); ++frame) {
1053 auto* editors = manager->GetCurrentEditorSet();
1054 dungeon = editors ? editors->GetEditorAs<editor::DungeonEditorV2>(
1057 if (dungeon && dungeon->IsWorkbenchWorkflowEnabled() &&
1058 (hover_cell = visible_widget(
"button:Room 0x012")) != 0)
1062 IM_CHECK(dungeon !=
nullptr && hover_cell != 0);
1063 dungeon->add_room(kRenamedRoom);
1064 ImGuiID rename_button = 0;
1065 for (
int frame = 0; frame < 200 && rename_button == 0 && !ctx->IsError();
1068 if (dungeon->current_room_id() == kRenamedRoom)
1069 rename_button = visible_widget(
"button:rename_room_button");
1071 IM_CHECK_EQ(dungeon->current_room_id(), kRenamedRoom);
1072 IM_CHECK(rename_button != 0);
1076 if (toasts->pending_count() != 0) {
1077 const ImGuiID park = visible_widget(
"button:Room 0x025");
1078 IM_CHECK(park != 0);
1079 ctx->MouseMove(park, ImGuiTestOpFlags_NoCheckHoveredId);
1080 const auto deadline =
1081 std::chrono::steady_clock::now() + std::chrono::seconds(15);
1082 while (toasts->pending_count() != 0 &&
1083 std::chrono::steady_clock::now() < deadline && !ctx->IsError())
1085 IM_CHECK_EQ(toasts->pending_count(), 0);
1088 const auto visible_popup_widget = [&](
const char* suffix) {
1090 for (
int frame = 0; frame < 60 &&
id == 0 && !ctx->IsError(); ++frame) {
1092 id = visible_widget(suffix);
1098 const auto submit_room_name = [&](
const std::string&
name) {
1099 ctx->ItemClick(rename_button);
1100 const ImGuiID input =
1101 visible_popup_widget(
"input_text:rename_room_input");
1104 ctx->ItemInputValue(input,
name.c_str());
1106 return !ctx->IsError();
1108 IM_CHECK(submit_room_name(room_rename));
1109 IM_CHECK(room_labels::FindRoomOverride(current_project, kRenamedRoom) ==
1111 IM_CHECK(room_labels::GetRoomLabel(current_project, kRenamedRoom) ==
1114 IM_CHECK(manager->IsCurrentProjectDirty());
1115 IM_CHECK(default_room_label(*current_project) == room_default);
1116 ctx->ItemClick(rename_button);
1117 const ImGuiID rename_reset =
1118 visible_popup_widget(
"button:rename_room_reset");
1119 IM_CHECK(rename_reset != 0);
1120 ctx->ItemClick(rename_reset);
1122 IM_CHECK(!room_labels::HasCustomRoomLabel(current_project, kRenamedRoom));
1123 IM_CHECK(room_labels::GetRoomLabel(current_project, kRenamedRoom) ==
1126 IM_CHECK(default_room_label(*current_project) == room_default);
1127 IM_CHECK(submit_room_name(room_rename));
1128 IM_CHECK(room_labels::HasCustomRoomLabel(current_project, kRenamedRoom));
1129 IM_CHECK(room_labels::FindRoomOverride(current_project, kRenamedRoom) ==
1131 IM_CHECK(room_labels::GetRoomLabel(current_project, kRenamedRoom) ==
1134 IM_CHECK(manager->IsCurrentProjectDirty());
1135 IM_CHECK(default_room_label(*current_project) == room_default);
1139 IM_CHECK(dungeon->rooms().GetIfLoaded(kHoverRoom) ==
nullptr);
1140 hover_cell = visible_widget(
"button:Room 0x012");
1141 IM_CHECK(hover_cell != 0);
1142 ctx->MouseMove(hover_cell);
1144 IM_CHECK(dungeon->rooms().GetIfLoaded(kHoverRoom) ==
nullptr);
1145 if (
const auto* hovered = dungeon->rooms().GetIfMaterialized(kHoverRoom))
1146 IM_CHECK(!hovered->custom_collision_dirty() &&
1147 !hovered->HasUnsavedChanges());
1148 const auto* renamed_room = dungeon->rooms().GetIfLoaded(kRenamedRoom);
1149 IM_CHECK(renamed_room !=
nullptr && !renamed_room->HasUnsavedChanges());
1151 const auto save_status = manager->SaveProject();
1153 if (!save_status.ok()) {
1154 IM_ERRORF(
"SaveProject failed: %s", save_status.ToString().c_str());
1157 IM_CHECK(!overworld->HasPendingProjectDraftChanges());
1158 const auto saved = read_file(source_path);
1159 IM_CHECK(saved.has_value() && *saved == document->Serialize());
1163 auto reopened_project = std::make_unique<project::YazeProject>();
1164 IM_CHECK(reopened_project->Open(expected_project.string()).ok());
1165 IM_CHECK(fs::canonical(reopened_project->lyra_terrain_file, ec) ==
1168 IM_CHECK(reopened_project->hack_manifest.HasProjectRegistry());
1170 room_labels::HasCustomRoomLabel(reopened_project.get(), kRenamedRoom));
1171 IM_CHECK(room_labels::GetRoomLabel(reopened_project.get(), kRenamedRoom) ==
1173 IM_CHECK(default_room_label(*reopened_project) == room_default);
1175 IM_CHECK(read_file(rom_path) == rom_disk_before);
1179 const auto stamp = std::chrono::duration_cast<std::chrono::milliseconds>(
1180 std::chrono::system_clock::now().time_since_epoch())
1182 const auto receipt_path =
1184 (
"lyra-canvas-native-pass-" + std::to_string(stamp) +
".json");
1188 IM_CHECK(!manager->IsCurrentProjectDirty());
1189 const size_t project_session_id = manager->GetCurrentSessionId();
1190 const size_t project_session_index = manager->GetCurrentSessionIndex();
1192 const auto project_saved_bytes = read_file(expected_project);
1193 IM_CHECK(project_saved_bytes.has_value());
1194 auto second_rom_path = receipt_path;
1195 second_rom_path.replace_extension(
".session-b.sfc");
1199 fs::copy_file(rom_path, second_rom_path, fs::copy_options::none, ec));
1200 IM_CHECK(!ec && read_file(second_rom_path) == rom_disk_before);
1203 second_rom_path.string(),
1207 IM_CHECK(manager->GetCurrentSessionId() != project_session_id);
1208 auto* second_project = manager->GetCurrentProject();
1209 auto* second_rom = manager->GetCurrentRom();
1210 IM_CHECK(second_project && second_rom && !second_rom->dirty());
1211 IM_CHECK(!manager->IsCurrentProjectDirty());
1212 const auto second_labels = second_project->resource_labels;
1214 const auto refused =
1215 dungeon->ApplyRoomLabelEdit(kRenamedRoom,
"Wrong Session");
1216 IM_CHECK(absl::IsFailedPrecondition(refused));
1217 IM_CHECK(refused.message() ==
1218 "Activate this dungeon project before renaming rooms");
1219 IM_CHECK(second_project->resource_labels == second_labels);
1221 IM_CHECK(!manager->IsCurrentProjectDirty() && !second_rom->dirty());
1222 manager->SwitchToSession(project_session_index);
1224 IM_CHECK_EQ(manager->GetCurrentSessionId(), project_session_id);
1226 IM_CHECK(current_project &&
1228 IM_CHECK(!manager->IsCurrentProjectDirty());
1230 IM_CHECK(manager->GetCurrentRom() == rom && !
rom->
dirty());
1232 IM_CHECK(read_file(expected_project) == project_saved_bytes);
1233 IM_CHECK(read_file(source_path) == saved);
1234 IM_CHECK(read_file(rom_path) == rom_disk_before);
1235 IM_CHECK(read_file(second_rom_path) == rom_disk_before);
1237 const nlohmann::json receipt = {
1238 {
"test",
"lyra_terrain/cross_map_stroke_undo_redo_project_save"},
1240 {
"project", expected_project.string()},
1241 {
"terrain", source_path.string()},
1242 {
"rom", rom_path.string()},
1244 {
"paint_y_start", kStartY},
1245 {
"paint_y_end", end_y},
1247 {
"undo_actions_added", 3},
1248 {
"rectangle_selection_copy_paste",
true},
1249 {
"rectangle_stamp",
true},
1250 {
"pattern_fill",
true},
1251 {
"invalid_brush_refused_atomically",
true},
1252 {
"map_authoring_native",
true},
1253 {
"main_palette", next_palette},
1254 {
"pattern_width", rectangle.width},
1255 {
"pattern_height", rectangle.height},
1256 {
"pattern_tile_ids", rectangle.tile_ids},
1257 {
"pattern_paste_x", 8},
1258 {
"pattern_paste_y", paste_y},
1259 {
"surface",
"normal_special_overworld"},
1260 {
"normal_tile16_selector",
true},
1261 {
"displayed_map_count", overworld->DisplayedMapCount()},
1262 {
"a0_a8_crossed",
true},
1263 {
"a8_b0_crossed", reaches_b0},
1264 {
"tile16_selector_filter_preserved",
true},
1265 {
"tile16_legacy_map", kLegacyMap},
1266 {
"tile16_project_terrain_deep_tile", 0xFFF},
1267 {
"room_rename_native",
true},
1268 {
"room_label_room", kRenamedRoom},
1269 {
"room_override_label", room_rename},
1270 {
"room_default_label", room_default},
1271 {
"matrix_hover_room", kHoverRoom},
1272 {
"wrong_session_rename_refused",
true},
1275 std::ofstream output(receipt_path, std::ios::binary);
1276 IM_CHECK(output.is_open());
1277 output << receipt.dump(2) <<
'\n';
1279 IM_CHECK(!output.fail());
1280 ctx->LogInfo(
"Lyra native canvas PASS; receipt: %s",
1281 receipt_path.string().c_str());
1283 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromCommandLine);
1288static void RegisterLyraTerrainReopenTest(ImGuiTestEngine* engine) {
1289 const char* requested = std::getenv(
"YAZE_LYRA_TERRAIN_REOPEN_PROJECT");
1290 if (!requested || !*requested ||
1291 std::getenv(
"YAZE_LYRA_TERRAIN_ACCEPTANCE_PROJECT"))
1293 ImGuiTestEngine_GetIO(engine).ConfigLogToTTY =
true;
1294 ImGuiTest* test = IM_REGISTER_TEST(engine,
"lyra_terrain",
1295 "fresh_process_autoload_saved_terrain");
1296 test->TestFunc = [](ImGuiTestContext* ctx) {
1297 namespace fs = std::filesystem;
1298 const char* requested = std::getenv(
"YAZE_LYRA_TERRAIN_REOPEN_PROJECT");
1299 const char* prior_receipt = std::getenv(
"YAZE_LYRA_TERRAIN_REOPEN_RECEIPT");
1300 IM_CHECK(requested && *requested && prior_receipt && *prior_receipt);
1301 IM_CHECK(fs::path(requested).is_absolute());
1302 IM_CHECK(fs::path(prior_receipt).is_absolute());
1304 const auto expected_project = fs::canonical(requested, ec);
1306 const auto project_directory = expected_project.parent_path();
1307 const auto receipt_path = fs::canonical(prior_receipt, ec);
1308 IM_CHECK(!ec && receipt_path.parent_path() == project_directory);
1309 const auto read_file =
1310 [](
const fs::path& path) -> std::optional<std::string> {
1311 std::ifstream file(path, std::ios::binary);
1313 return std::nullopt;
1314 std::string bytes{std::istreambuf_iterator<char>(file), {}};
1315 return file.bad() ? std::nullopt
1316 : std::optional<std::string>(std::move(bytes));
1318 const auto prior_bytes = read_file(receipt_path);
1319 IM_CHECK(prior_bytes.has_value());
1320 const auto prior = nlohmann::json::parse(*prior_bytes,
nullptr,
false);
1321 IM_CHECK(prior.is_object());
1322 IM_CHECK(prior.value(
"result",
"") ==
"PASS");
1323 IM_CHECK(prior.value(
"test",
"") ==
1324 "lyra_terrain/cross_map_stroke_undo_redo_project_save");
1325 IM_CHECK(prior.value(
"project",
"") == expected_project.string());
1326 const auto project_before = read_file(expected_project);
1327 IM_CHECK(project_before.has_value());
1329 editor::EditorManager* manager =
nullptr;
1331 for (
int frame = 0; frame < 200; ++frame) {
1348 const auto source_path = fs::canonical(
1351 IM_CHECK(!ec && source_path.parent_path() == project_directory);
1352 IM_CHECK(source_path.string() == prior.value(
"terrain",
""));
1353 const auto source_before = read_file(source_path);
1354 IM_CHECK(source_before.has_value());
1356 prior.value(
"terrain_sha256",
""));
1357 const auto rom_path = fs::canonical(
1359 IM_CHECK(!ec && rom_path.parent_path() == project_directory);
1360 IM_CHECK(rom_path.filename() ==
"main-with-d3.sfc");
1361 IM_CHECK(rom_path.string() == prior.value(
"rom",
""));
1362 auto*
rom = manager->GetCurrentRom();
1364 IM_CHECK(fs::canonical(
rom->
filename(), ec) == rom_path && !ec);
1365 const auto rom_disk = read_file(rom_path);
1366 IM_CHECK(rom_disk.has_value());
1368 prior.value(
"rom_sha256",
""));
1369 const auto rom_bytes = [&]() {
1370 return std::string_view(
reinterpret_cast<const char*
>(
rom->
data()),
1374 prior.value(
"rom_sha256",
""));
1376 manager->OpenEditorAndPanelsFromFlags(
"Overworld",
"overworld.canvas");
1377 editor::OverworldEditor* overworld =
nullptr;
1378 for (
int frame = 0; frame < 200; ++frame) {
1379 auto* editors = manager->GetCurrentEditorSet();
1380 overworld = editors ? editors->GetEditorAs<editor::OverworldEditor>(
1383 if (overworld && overworld->overworld().is_loaded() &&
1384 overworld->lyra_canvas_.loaded() && ctx->ItemExists(
"**/OwMap"))
1388 IM_CHECK(overworld && overworld->overworld().is_loaded());
1389 if (!overworld->lyra_canvas_status_.ok()) {
1390 IM_ERRORF(
"Native Lyra autoload failed: %s",
1391 overworld->lyra_canvas_status_.ToString().c_str());
1394 IM_CHECK(overworld->lyra_canvas_.loaded());
1395 IM_CHECK_EQ(overworld->current_world_id(), 2);
1396 IM_CHECK_EQ(overworld->current_map_id(), 0xA0);
1397 IM_CHECK_EQ(overworld->DisplayedMapCount(), 192);
1398 IM_CHECK(!overworld->IsProjectTerrainMap(0x80));
1399 IM_CHECK(overworld->IsProjectTerrainMap(0xA0));
1400 IM_CHECK(overworld->IsProjectTerrainMap(0xBF));
1401 IM_CHECK(prior.value(
"surface",
"") ==
"normal_special_overworld");
1402 IM_CHECK(prior.value(
"normal_tile16_selector",
false));
1403 const auto* document = overworld->lyra_canvas_.document();
1404 IM_CHECK(document && !document->dirty());
1405 IM_CHECK(!overworld->HasPendingProjectDraftChanges());
1406 IM_CHECK(document->Serialize() == *source_before);
1408 prior.value(
"terrain_sha256",
""));
1409 const int x = prior.value(
"paint_x", -1);
1410 const int first_y = prior.value(
"paint_y_start", -1);
1411 const int last_y = prior.value(
"paint_y_end", -1);
1412 const int tile = prior.value(
"tile16", -1);
1414 IM_CHECK(first_y >= 0 && first_y <= 31 && last_y >= 32 &&
1416 IM_CHECK(tile >= 0 && tile < document->profile().tile_count);
1417 for (
int y = first_y; y <= last_y; ++y)
1418 IM_CHECK(document->tile(x, y).value() == tile);
1419 if (prior.value(
"rectangle_selection_copy_paste",
false)) {
1420 const editor::TileBrush rectangle{
1421 prior.at(
"pattern_width").get<
int>(),
1422 prior.at(
"pattern_height").get<
int>(),
1423 prior.at(
"pattern_tile_ids").get<std::vector<int>>()};
1424 IM_CHECK(rectangle.valid());
1425 const int px = prior.at(
"pattern_paste_x").get<
int>();
1426 const int py = prior.at(
"pattern_paste_y").get<
int>();
1427 for (
int y = 0; y < rectangle.height; ++y)
1428 for (
int x = 0; x < rectangle.width; ++x)
1429 IM_CHECK(document->tile(px + x, py + y).value() ==
1430 rectangle.at(x, y));
1431 for (
int y = 0; y < 32; ++y)
1432 for (
int x = 0; x < 32; ++x)
1433 IM_CHECK(document->tile(32 + x, 32 + y).value() ==
1434 rectangle.at(x % rectangle.width, y % rectangle.height));
1436 if (prior.value(
"map_authoring_native",
false)) {
1437 IM_CHECK_EQ(document->authoring().maps[0].properties.main_palette,
1438 prior.at(
"main_palette").get<
int>());
1439 IM_CHECK_EQ(document->authoring().maps[1].properties.graphics_sheets[0],
1441 IM_CHECK_EQ(document->authoring().maps[1].properties.graphics_sheets[1],
1443 IM_CHECK(!document->authoring().maps[0].sprites.empty());
1444 IM_CHECK_EQ(document->authoring().maps[0].doors.at(0).id, 900001);
1446 !document->authoring().maps[0].doors.at(0).entrance_id.has_value());
1447 IM_CHECK_EQ(document->authoring().maps[0].exits.at(0).room_id, 0x105);
1448 IM_CHECK_EQ(document->authoring().flight_points.size(), 2);
1449 IM_CHECK_EQ(document->authoring().flight_points.at(0).map_id, 0xA0);
1450 IM_CHECK_EQ(document->authoring().flight_points.at(1).map_id, 0x20);
1452 const bool room_rename_native = prior.value(
"room_rename_native",
false);
1453 if (room_rename_native) {
1454 namespace room_labels = editor::dungeon_project_labels;
1455 const int room = prior.at(
"room_label_room").get<
int>();
1456 const auto override_label =
1457 prior.at(
"room_override_label").get<std::string>();
1458 const auto default_label =
1459 prior.at(
"room_default_label").get<std::string>();
1460 IM_CHECK_EQ(room, 0x25);
1461 IM_CHECK_EQ(prior.value(
"matrix_hover_room", -1), 0x12);
1462 IM_CHECK(!override_label.empty() && !default_label.empty());
1464 IM_CHECK(room_labels::HasCustomRoomLabel(current_project, room));
1465 IM_CHECK(room_labels::GetRoomLabel(current_project, room) ==
1470 const auto default_it = labels->second.find(std::to_string(room));
1471 IM_CHECK(default_it != labels->second.end() &&
1472 default_it->second == default_label);
1474 IM_CHECK_EQ(overworld->undo_manager().UndoStackSize(), 0);
1478 const auto terrain = ctx->ItemInfo(
"**/OwMap");
1479 IM_CHECK(terrain.ID != 0 && terrain.Window !=
nullptr);
1480 ctx->SetRef(terrain.Window);
1481 ctx->WindowFocus(
"");
1482 overworld->ZoomToFit();
1484 const auto fitted = ctx->ItemInfo(
"**/OwMap");
1485 const auto& canvas = overworld->ow_map_canvas();
1486 const auto origin = canvas.zero_point();
1487 const auto scroll = canvas.scrolling();
1488 const float cell_pixels = 16.0f * canvas.global_scale();
1489 const auto map_center = [&](
int map_row) {
1491 origin.x + scroll.x + 16.0f * cell_pixels,
1492 origin.y + scroll.y + (map_row * 32.0f + 16.0f) * cell_pixels);
1494 IM_CHECK(fitted.RectClipped.Contains(map_center(0)));
1495 IM_CHECK(fitted.RectClipped.Contains(map_center(4)));
1496 IM_CHECK(!overworld->HasPendingProjectDraftChanges());
1497 IM_CHECK(read_file(expected_project) == project_before);
1498 IM_CHECK(read_file(source_path) == source_before);
1499 IM_CHECK(read_file(rom_path) == rom_disk && !
rom->
dirty());
1501 prior.value(
"rom_sha256",
""));
1503 const auto stamp = std::chrono::duration_cast<std::chrono::milliseconds>(
1504 std::chrono::system_clock::now().time_since_epoch())
1506 const auto output_path =
1508 (
"lyra-canvas-reopen-pass-" + std::to_string(stamp) +
".json");
1509 const nlohmann::json receipt = {
1510 {
"test",
"lyra_terrain/fresh_process_autoload_saved_terrain"},
1512 {
"project", expected_project.string()},
1513 {
"terrain", source_path.string()},
1514 {
"rom", rom_path.string()},
1515 {
"prior_receipt", receipt_path.string()},
1516 {
"autoloaded",
true},
1517 {
"surface",
"normal_special_overworld"},
1518 {
"map_80_and_a0_same_canvas",
true},
1519 {
"displayed_map_count", overworld->DisplayedMapCount()},
1520 {
"document_clean",
true},
1521 {
"saved_cells_exact",
true},
1522 {
"project_unchanged",
true},
1523 {
"room_override_persisted", room_rename_native},
1524 {
"terrain_sha256", prior.at(
"terrain_sha256")},
1525 {
"rom_sha256", prior.at(
"rom_sha256")}};
1526 std::ofstream output(output_path, std::ios::binary);
1527 IM_CHECK(output.is_open());
1528 output << receipt.dump(2) <<
'\n';
1530 IM_CHECK(!output.fail());
1531 ctx->LogInfo(
"Lyra native reopen PASS; receipt: %s",
1532 output_path.string().c_str());
1534 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromCommandLine);
1543static void RegisterProjectGraphicsImportPreviewTest(ImGuiTestEngine* engine) {
1544#ifdef YAZE_WITH_GRPC
1545 const char* requested = std::getenv(
"YAZE_GUI_BOAT_PROJECT");
1546 const char* pack = std::getenv(
"YAZE_GUI_BOAT_PACK");
1547 if (!requested || !*requested || !pack || !*pack)
1549 auto& io = ImGuiTestEngine_GetIO(engine);
1550 io.ConfigLogToTTY =
true;
1552 IM_REGISTER_TEST(engine,
"project_graphics",
"boat_pack_preview_fields");
1553 test->TestFunc = [](ImGuiTestContext* ctx) {
1554 namespace fs = std::filesystem;
1555 const char* requested = std::getenv(
"YAZE_GUI_BOAT_PROJECT");
1556 const char* pack = std::getenv(
"YAZE_GUI_BOAT_PACK");
1557 const char* capture = std::getenv(
"YAZE_GUI_BOAT_CAPTURE");
1558 IM_CHECK(requested && *requested && pack && *pack && capture && *capture);
1559 IM_CHECK(fs::path(requested).is_absolute() && fs::path(pack).is_absolute());
1560 IM_CHECK(fs::path(capture).is_absolute());
1562 const auto project_path = fs::canonical(requested, ec);
1563 IM_CHECK(!ec && project_path.filename() ==
"boat-import-reopen.yaze");
1564 const auto pack_path = fs::canonical(pack, ec);
1566 const fs::path capture_path(capture);
1567 const auto fields_path = capture_path.parent_path() /
1568 (capture_path.stem().string() +
"-fields.png");
1569 IM_CHECK(fs::is_directory(capture_path.parent_path()));
1570 IM_CHECK(!fs::exists(capture_path) && !fs::exists(fields_path));
1571 const auto read = [](
const fs::path& path) -> std::optional<std::string> {
1572 std::ifstream file(path, std::ios::binary);
1574 return std::nullopt;
1575 std::string bytes{std::istreambuf_iterator<char>(file), {}};
1576 return file.bad() ? std::nullopt : std::optional<std::string>(bytes);
1579 editor::EditorManager* manager =
nullptr;
1580 for (
int frame = 0; frame < 200 && !manager; ++frame) {
1586 IM_CHECK(manager !=
nullptr);
1589 auto opened = std::make_shared<std::optional<absl::Status>>();
1590 manager->QueueDeferredAction([manager, project_path, opened] {
1591 const auto status = manager->OpenRomOrProject(project_path.string());
1593 manager->OpenEditorAndPanelsFromFlags(
1594 "Overworld",
"overworld.canvas,overworld.area_graphics");
1598 for (
int frame = 0; frame < 200 && !opened->has_value(); ++frame)
1600 IM_CHECK(opened->has_value() && opened->value().ok());
1601 editor::OverworldEditor* overworld =
nullptr;
1602 for (
int frame = 0; frame < 200; ++frame) {
1603 auto* editors = manager->GetCurrentEditorSet();
1604 overworld = editors ? editors->GetEditorAs<editor::OverworldEditor>(
1607 if (overworld && overworld->overworld().is_loaded())
1611 IM_CHECK(overworld && overworld->overworld().is_loaded());
1612 auto* project = manager->GetCurrentProject();
1613 IM_CHECK(project && fs::canonical(project->filepath, ec) == project_path &&
1615 IM_CHECK(project->name.find(
"disposable") != std::string::npos);
1616 auto*
rom = manager->GetCurrentRom();
1618 const auto rom_path = fs::canonical(
rom->
filename(), ec);
1619 IM_CHECK(!ec && rom_path.parent_path() == project_path.parent_path());
1620 IM_CHECK(rom_path.filename() ==
"boat-import-save-as.sfc");
1622 const auto rom_disk_before = read(rom_path);
1623 const auto project_before = read(project_path);
1624 IM_CHECK(rom_disk_before && project_before);
1627 auto source_opened = std::make_shared<std::optional<absl::Status>>();
1628 manager->QueueDeferredAction([overworld, source_opened] {
1629 overworld->SetCurrentMap(0x31);
1630 auto status = overworld->overworld().EnsureMapBuilt(0x31);
1632 status = overworld->OpenProjectGraphicsSource();
1633 *source_opened = status;
1635 for (
int frame = 0; frame < 200 && !source_opened->has_value(); ++frame)
1637 IM_CHECK(source_opened->has_value() && source_opened->value().ok());
1639 ctx->SetRef(
"Edit Project Graphics");
1640 IM_CHECK(ctx->GetWindowByRef(
"") !=
nullptr);
1641 const auto input = ctx->ItemInfo(
"**/Logical pack JSON");
1642 IM_CHECK(input.ID != 0 && input.Window !=
nullptr);
1643 ctx->SetRef(input.Window);
1644 ctx->ItemInputValue(input.ID, pack_path.string().c_str());
1645 ctx->ItemClick(
"Load pack");
1647 struct PaletteInputs {
1653 const PaletteInputs palettes[] = {
1654 {
"P0", 3,
"100", 64}, {
"P1", 4,
"180", 32}, {
"P2", 2,
"140", 64}};
1655 for (
const auto& palette : palettes) {
1656 const std::string scope = std::string(palette.id) +
"/";
1657 IM_CHECK(ctx->ItemExists((scope +
"BG palette row").c_str()));
1658 ctx->ItemInputValue((scope +
"BG palette row").c_str(), palette.row);
1659 ctx->ItemInputValue((scope +
"First BG character (hex)").c_str(),
1661 ctx->ItemInputValue((scope +
"Owned character count").c_str(),
1664 ctx->ItemInputValue(
"First Tile16 (hex)",
"EEC");
1665 ctx->ItemInputValue(
"Owned Tile16 count", 96);
1666 ctx->ItemInputValue(
"Area anchor X (Tile16)", 33);
1667 ctx->ItemInputValue(
"Area anchor Y (Tile16)", 17);
1668 ctx->ItemClick(
"Preview import");
1670 IM_CHECK(ctx->ItemExists(
1671 "I reviewed the destination ranges and shared palette records"));
1675 const auto capture_renderer = [&](
const fs::path& path) -> absl::Status {
1677 std::make_shared<std::optional<absl::StatusOr<ScreenshotArtifact>>>();
1680 return absl::FailedPreconditionError(
"No GUI controller");
1681 controller->RequestScreenshot(
1682 {.preferred_path = path.string(),
1684 .reveal_to_user =
false,
1685 .callback = [result](absl::StatusOr<ScreenshotArtifact> image) {
1686 *result = std::move(image);
1688 for (
int frame = 0; frame < 200 && !result->has_value(); ++frame)
1690 if (!result->has_value())
1691 return absl::DeadlineExceededError(
"GUI capture timed out");
1692 if (!result->value().ok())
1693 return result->value().status();
1694 if (result->value()->file_path != path.string() ||
1695 result->value()->width <= 0 || result->value()->height <= 0)
1696 return absl::DataLossError(
1697 "GUI capture did not return the requested image");
1698 return absl::OkStatus();
1700 ctx->ScrollToTop(input.Window->ID);
1702 IM_CHECK(capture_renderer(fields_path).ok());
1704 "I reviewed the destination ranges and shared palette records");
1706 IM_CHECK(capture_renderer(capture_path).ok());
1707 IM_CHECK(fs::is_regular_file(capture_path) &&
1708 fs::is_regular_file(fields_path));
1709 ctx->SetRef(
"Edit Project Graphics");
1710 ctx->ItemClick(
"Close (keep draft)");
1712 const auto* closed_modal = ctx->GetWindowByRef(
"");
1713 IM_CHECK(!closed_modal || !closed_modal->Active);
1714 IM_CHECK(!overworld->HasPendingProjectDraftChanges());
1716 IM_CHECK(read(rom_path) == rom_disk_before &&
1717 read(project_path) == project_before);
1719 "Boat GUI preview PASS: P0/P1/P2, map31, no import/publish/save; "
1720 "captures %s and %s",
1721 capture_path.string().c_str(), fields_path.string().c_str());
1723 ImGuiTestEngine_QueueTest(engine, test, ImGuiTestRunFlags_RunFromCommandLine);
1730 if (engine ==
nullptr)
1732 RegisterKeyboardShortcutTests(engine);
1733 RegisterEntityModeTests(engine);
1734 RegisterModeRoundTripTests(engine);
1735 RegisterCanvasNavigationTests(engine);
1736 RegisterWorldSelectorTests(engine);
1737 RegisterLyraTerrainAcceptanceTest(engine);
1738 RegisterLyraTerrainReopenTest(engine);
1739 RegisterProjectGraphicsImportPreviewTest(engine);
Controller * GetController()
static Application & Instance()
editor::EditorManager * editor_manager()
const auto & vector() const
bool HasProjectRegistry() const
static constexpr int kWidth
static constexpr int kHeight
std::string ComputeSourceArtifactSha256(std::string_view content)
Rom * rom()
Get the current ROM instance.
::yaze::project::YazeProject * current_project()
Get the current project instance.
constexpr int kTile16SelectorColumns
constexpr float kTile16SelectorDrawOffsetX
void RegisterOverworldUITests(ImGuiTestEngine *)
std::string GetRoomLabel(int id)
Convenience function to get a room label.
bool project_opened() const
core::HackManifest hack_manifest
std::string lyra_terrain_file
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
std::string GetAbsolutePath(const std::string &relative_path) const