EBLAN_ENGINE/tests/editor_regressions.cpp

159 lines
9.7 KiB
C++

// Compile the real editor implementation in this translation unit so the
// scenarios can exercise tools and playtest preparation without opening a UI.
#include <string>
#include <vector>
#include <deque>
#include <windows.h>
#define private public
#include "../editor/src/editor_app.h"
#undef private
#include "../editor/src/editor_app.cpp"
#include <iostream>
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<uint8_t> Bytes(const fs::path& path) {
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(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<uint8_t> defaults;
ReadTextureSlots("absent-defaults.bin", defaults);
{
std::ofstream out("../assets/defaults.bin", std::ios::binary);
out.write(reinterpret_cast<const char*>(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<uint8_t> 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<char*>(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;
}