EBLAN_ENGINE/tests/editor_regressions.cpp
Игорь Федосеев 527d92411d chore: integrate main before stage 2
Preserve stage-1 stability fixes alongside v5 lighting, IPC v2, weapons and studio updates. Fix barrel AOE pointer corruption and extend modal gameplay gating.

Verify 137 editor checks, 191 ASM checks, lighting bake and preview parity. Document stage-2 scope without implementing it.
2026-10-31 22:10:07 +03:00

161 lines
9.8 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"
#include "stability_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]);
RunStabilityEditorChecks();
std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n";
return failures ? 1 : 0;
}