Trace shots against current map geometry instead of stale render depth. Preserve outline counters across volatile pixel calls and freeze gameplay on cinematic, transition and death ticks. Clamp packed HP before narrowing and match legacy archetype rules. Publish IDE saves atomically; failed saves block file switches and builds. Drain compiler output while hidden. Verify runtime/editor builds, 112 editor checks, 148 ASM checks and live preview parity. Record remaining stages in docs/STABILIZATION.md.
159 lines
9.7 KiB
C++
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 "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");
|
|
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;
|
|
}
|