// Compile the real editor implementation in this translation unit so the // scenarios can exercise tools and playtest preparation without opening a UI. #include #include #include #include #define private public #include "../editor/src/editor_app.h" #undef private #include "../editor/src/editor_app.cpp" #include static int failures = 0, checks = 0; static void Check(bool ok, const char* name) { ++checks; if (!ok) ++failures; std::cout << (ok ? "PASS " : "FAIL ") << name << '\n'; } static std::vector Bytes(const fs::path& path) { std::ifstream in(path, std::ios::binary); return {std::istreambuf_iterator(in), {}}; } #include "cutscene_editor_checks.inl" #include "lighting_editor_checks.inl" #include "viewport_editor_checks.inl" int main(int argc, char** argv) { EditorApp app; std::vector defaults; ReadTextureSlots("absent-defaults.bin", defaults); { std::ofstream out("../assets/defaults.bin", std::ios::binary); out.write(reinterpret_cast(defaults.data()), defaults.size()); } Check(app.LoadLevel("../assets/map.dat"), "load v3 main map"); Check(app.sprites.size() == 2 && app.transitions.size() == 1, "load all entities"); Check(app.LoadLevel("../assets/legacy1.dat") && app.sprites[0].x == 224, "convert v1 coordinates"); Check(app.LoadLevel("../assets/legacy2.dat") && app.transitions.size() == 1, "read extended v2 maps"); Check(app.LoadLevel("../assets/black.dat") && app.ceilingColor == 0 && app.floorColor == 0, "apply black palette"); Check(app.LoadLevel("../assets/map.dat"), "restore main map"); auto before = app.CaptureSnapshot(); auto pathBefore = app.currentFilePath; for (const char* name : {"truncated.dat", "overflow.dat", "badname.dat", "badspawn.dat", "orphan.dat", "duplicate.dat"}) { Check(!app.LoadLevel(std::string("../assets/") + name), name); Check(app.currentFilePath == pathBefore && app.sprites.size() == before.sprites.size() && app.transitions.size() == before.transitions.size() && !std::memcmp(app.map, before.map, sizeof(app.map)), "failed load preserves current room"); } Check(app.SaveLevel("../assets/saved.dat"), "save current format atomically"); BinaryLevel saved; std::string error; Check(ReadLevelFile("../assets/saved.dat", saved, error) && saved.header.version == MAP_FORMAT_VERSION, "saved map reads back"); auto goodSave = Bytes("../assets/saved.dat"); app.playerStartX = -1; Check(!app.SaveLevel("../assets/saved.dat") && Bytes("../assets/saved.dat") == goodSave, "invalid save does not overwrite a valid file"); app.playerStartX = 2; std::vector rgba(128 * 128 * 4); for (int i = 0; i < 128 * 128; ++i) { rgba[4*i] = 0x11; rgba[4*i+1] = 0x22; rgba[4*i+2] = 0x33; rgba[4*i+3] = 255; } stbi_write_png("wall.png", 128, 128, 4, rgba.data(), 128 * 4); Check(app.ImportTextureImage(1, false, "wall.png"), "first PNG import creates directories and pack"); auto pack = Bytes("../assets/textures/texturepack.bin"); Check(pack.size() == 8 + 16 * 65536, "complete pack layout"); Check(pack[8+65536] == 0x33 && pack[9+65536] == 0x22 && pack[10+65536] == 0x11 && pack[11+65536] == 0, "RGBA converts to engine BGRA"); auto fallback = MakeDefaultTextureSlot(2); Check(std::equal(fallback.begin(), fallback.end(), pack.begin() + 8 + 2 * 65536), "first import retains fallback walls"); auto lamp = MakeDefaultTextureSlot(10); Check(std::equal(lamp.begin(), lamp.end(), pack.begin() + 8 + 10 * 65536), "first import retains sprite defaults"); for (int y = 0; y < 128; ++y) for (int x = 0; x < 64; ++x) rgba[4*(y*128+x)+3] = 0; stbi_write_png("sprite.png", 128, 128, 4, rgba.data(), 128 * 4); Check(app.ImportTextureImage(11, true, "sprite.png"), "import transparent sprite"); auto updated = Bytes("../assets/textures/texturepack.bin"); const auto* sprite = updated.data() + 8 + 11 * 65536; Check(sprite[0] == 255 && sprite[1] == 0 && sprite[2] == 255 && sprite[3] == 0 && sprite[400] == 0x33, "alpha converts to chroma key"); Check(std::equal(pack.begin(), pack.begin() + 8 + 11 * 65536, updated.begin()), "sprite import preserves other slots"); fs::copy_file("../assets/textures/texturepack.bin", "../assets/imported.bin", fs::copy_options::overwrite_existing); auto corrupt = updated; corrupt.resize(100); { std::ofstream out("../assets/textures/texturepack.bin", std::ios::binary); out.write(reinterpret_cast(corrupt.data()), corrupt.size()); } Check(!app.ImportTextureImage(1, false, "wall.png") && Bytes("../assets/textures/texturepack.bin") == corrupt, "invalid existing pack is preserved"); fs::copy_file("../assets/imported.bin", "../assets/textures/texturepack.bin", fs::copy_options::overwrite_existing); Check(app.ResetTextureSlotToDefault(2), "reset uses the same safe pack writer"); auto reset = Bytes("../assets/textures/texturepack.bin"); Check(std::equal(updated.begin() + 8 + 11 * 65536, updated.end(), reset.begin() + 8 + 11 * 65536), "reset preserves imported sprites"); Check(app.ImportSoundAudio("quiet.wav"), "WAV import without scripts"); app.StopAudioSound(); auto manifest = Bytes("../assets/sounds/sounds.inc"); Check(Bytes("quiet.wav") == Bytes("../assets/sounds/quiet.wav") && std::string(manifest.begin(), manifest.end()).find("SND_QUIET") != std::string::npos, "sound file and manifest"); app.InitDefaultLevel(); Check(app.SetCell(2, 2, CELL_PORTAL) && app.transitions.size() == 1, "brush creates portal and transition together"); app.FloodFill(2, 2, CELL_PORTAL, CELL_EMPTY); Check(app.map[2][2] == CELL_EMPTY && app.transitions.empty(), "fill removes transition with portal"); app.map[2][2] = CELL_METAL; app.map[2][3] = CELL_METAL; app.FloodFill(2, 2, CELL_METAL, CELL_PORTAL); Check(app.transitions.size() == 2 && app.map[2][2] == CELL_PORTAL && app.map[2][3] == CELL_PORTAL, "small fill creates every transition"); app.SetCell(2, 2, CELL_EMPTY); app.SetCell(3, 2, CELL_EMPTY); auto fillBefore = app.CaptureSnapshot(); app.FloodFill(2, 2, CELL_EMPTY, CELL_PORTAL); Check(app.transitions.empty() && !std::memcmp(app.map, fillBefore.map, sizeof(app.map)), "oversized portal fill is atomic"); app.PushUndo(); app.SetCell(2, 2, CELL_PORTAL); app.Undo(); Check(app.map[2][2] == CELL_EMPTY && app.transitions.empty(), "undo restores both grid and transitions"); app.Redo(); Check(app.map[2][2] == CELL_PORTAL && app.transitions.size() == 1, "redo restores portal"); app.currentTool = TOOL_PLAYER_SPAWN; app.ApplyTool(2, 2); Check(app.map[2][2] == CELL_EMPTY && app.transitions.empty(), "moving spawn onto a portal removes its trigger"); app.InitDefaultLevel(); for (int i = 0; i < 16; ++i) app.SetCell(i + 1, 20, CELL_PORTAL); Check(!app.SetCell(20, 20, CELL_PORTAL) && app.map[20][20] != CELL_PORTAL && app.transitions.size() == 16, "portal limit cannot create orphan cells"); Check(app.LoadLevel("../assets/sublevel_02.dat"), "open sublevel for F5"); auto originalMain = Bytes("../assets/map.dat"), originalSub = Bytes("../assets/sublevel_02.dat"); auto originalPath = app.currentFilePath; app.map[12][12] = CELL_MARBLE; // unsaved edit included only in preview app.LaunchPlaytest(); Check(app.isEngineRunning && app.currentFilePath == originalPath, "F5 preserves current file path"); Check(Bytes("../assets/map.dat") == originalMain && Bytes("../assets/sublevel_02.dat") == originalSub, "F5 never overwrites source maps"); fs::path sandbox = app.playtestDirectory; BinaryLevel preview; Check(ReadLevelFile((sandbox / "assets/sublevel_02.dat").string(), preview, error) && preview.cells[12*32+12] == CELL_MARBLE, "F5 includes unsaved edits"); Check(Bytes(sandbox / "assets/map.dat") == originalMain, "reverse portal keeps the real main map"); Check(Bytes(sandbox / "assets/textures/texturepack.bin") == Bytes("../assets/textures/texturepack.bin"), "F5 copies imported resources"); if (app.hEngineProcess) WaitForSingleObject(app.hEngineProcess, 5000); auto launched = Bytes(sandbox / "launched.txt"); Check(std::string(launched.begin(), launched.end()) == "assets\\sublevel_02.dat", "engine receives the selected start map"); app.CheckPlaytestProcess(); Check(!app.isEngineRunning && !fs::exists(sandbox), "finished playtest cleans its own temporary directory"); strcpy_s(app.transitions[0].targetMap, "missing.dat"); app.LaunchPlaytest(); Check(!app.isEngineRunning && app.playtestDirectory.empty() && Bytes("../assets/map.dat") == originalMain, "missing linked map aborts F5 without data loss"); RunLightingChecks(app); RunCutsceneChecks(app); if (argc > 1) { Check(ReadLevelFile(std::string(argv[1]) + "/map.dat", preview, error), "existing project main map remains compatible"); Check(ReadLevelFile(std::string(argv[1]) + "/sublevel_02.dat", preview, error), "existing project sublevel remains compatible"); app.LoadLevel(std::string(argv[1]) + "/map.dat"); std::string start, executable; Check(app.PreparePlaytest(start, executable), "existing project graph prepares for playtest"); app.CleanupPlaytest(); app.LoadLevel(std::string(argv[1]) + "/sublevel_02.dat"); start.clear(); executable.clear(); Check(app.PreparePlaytest(start, executable), "existing project sublevel and reverse link prepare for playtest"); app.CleanupPlaytest(); } if(argc>2) RunViewportChecks(app,argv[2]); std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n"; return failures ? 1 : 0; }