Group level edits across inspector and canvas gestures, preserve legacy maps, and expose opt-in switch/key door bindings without changing wire layouts. Refresh studio and pause UI with UE5-inspired surfaces and content drawer. Verify 162 editor and 307 ASM checks plus renderer parity.
163 lines
9.8 KiB
C++
163 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"
|
|
#include "studio_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();
|
|
RunStudioEditorChecks();
|
|
std::cout << "Editor: " << checks - failures << '/' << checks << " passed\n";
|
|
return failures ? 1 : 0;
|
|
}
|