- Stage 1: Eliminate fish-eye distortion with precomputed tangential ray offset table and fractional DDA depth - Stage 2: Full asset management in EblanStudio (stb_image/stb_image_write, 128x128 BGRA texture packaging, WAV sound manager) - Stage 3: Multi-level room streaming, LevelTransition/CELL_PORTAL trigger system, 16-frame screen fade to black - Stage 4: 60Hz cutscene engine in pure x86 ASM (v4 binary track, waypoint camera motion, shortest-arc pan, CP1251 Russian subtitles, letterbox overlay, audio) - Stage 5: EblanStudio cutscene timeline (F6) and windowless Live ASM Viewport over shared-memory IPC directly rendered into DX11 - Tooling & Tests: comprehensive regression test suite (94 editor + 107 engine tests), IPC tests, and cutscene demo
88 lines
5.8 KiB
C++
88 lines
5.8 KiB
C++
static void RunCutsceneChecks(EditorApp& app) {
|
|
Check(app.LoadLevel("../assets/map.dat"), "cutscene test starts from legacy map");
|
|
app.playerStartAngle = 508; app.playerStartY = 3;
|
|
app.cutscene.flags = CUTSCENE_AUTOPLAY;
|
|
app.cutscene.strings = {"Привет, мир!", "quiet.wav"};
|
|
app.cutscene.events = {
|
|
{0,OP_MOVE_CAM_TO,0,288,160,4}, {0,OP_PAN_CAM_TO,0,4,4,0},
|
|
{0,OP_FADE_SCREEN,0,16,4,0}, {0,OP_SHOW_SUBTITLE,0,0,0,0},
|
|
{0,OP_PLAY_SOUND,0,1,0,0}, {1,OP_WAIT,0,2,0,0},
|
|
{2,OP_HIDE_SUBTITLE,0,0,0,0}, {4,OP_FADE_SCREEN,0,0,4,0}, {8,OP_END,0,0,0,0}
|
|
};
|
|
Check(app.SaveLevel("../assets/scene.dat"), "save v4 cutscene, Russian text and sound reference");
|
|
BinaryLevel level; std::string error;
|
|
Check(ReadLevelFile("../assets/scene.dat",level,error) && level.header.version == 4 && level.cutscene.events.size() == 9 &&
|
|
level.cutscene.strings[0] == "Привет, мир!" && level.cutscene.flags == CUTSCENE_AUTOPLAY,
|
|
"v4 track round trip retains UTF8 and autoplay");
|
|
std::string encoded;
|
|
Check(CutsceneEncode("Привет, мир!",encoded) && encoded.size() == 12 && static_cast<unsigned char>(encoded[0]) == 0xCF,
|
|
"Russian subtitles use the runtime CP1251 font");
|
|
Check(!CutsceneEncode("😀",encoded), "unrepresentable subtitle is rejected");
|
|
CutscenePreview preview;
|
|
preview.Start(160,224,508);
|
|
std::ofstream trace("../assets/cutscene_reference.bin",std::ios::binary);
|
|
int audioCount = 0;
|
|
for (int tick=0;tick<=8;++tick) {
|
|
preview.Step(app.cutscene,[&](int id,bool loop) { if(id==1 && !loop) ++audioCount; });
|
|
int state[6] = {preview.x,preview.y,preview.angle,preview.fade,preview.subtitle,preview.active?1:0};
|
|
trace.write(reinterpret_cast<const char*>(state),sizeof(state));
|
|
if (tick==2) Check(preview.x==224 && preview.y==192 && preview.angle==0 && preview.fade==8 && preview.subtitle==0,
|
|
"preview interpolates signed movement, shortest angle, fade and wait");
|
|
if (tick==3) Check(preview.subtitle==-1, "wait delays a following hide command until its deadline");
|
|
}
|
|
trace.close();
|
|
Check(preview.x==288 && preview.y==160 && preview.angle==4 && !preview.active && preview.fade==0 && audioCount==1,
|
|
"preview reaches exact targets and stops cleanly");
|
|
auto invalid = app.cutscene;
|
|
invalid.events[0].dataId=3601;
|
|
Check(!ValidateCutscene(invalid,&app.map[0][0],app.transitions.size(),error), "oversized camera duration rejected");
|
|
invalid=app.cutscene;invalid.events[3].paramA=99;
|
|
Check(!ValidateCutscene(invalid,&app.map[0][0],app.transitions.size(),error), "invalid string reference rejected");
|
|
invalid=app.cutscene;invalid.events.pop_back();
|
|
Check(!ValidateCutscene(invalid,&app.map[0][0],app.transitions.size(),error), "missing end marker rejected");
|
|
invalid=app.cutscene;invalid.events[0].paramA=0;invalid.events[0].paramB=0;
|
|
Check(!ValidateCutscene(invalid,&app.map[0][0],app.transitions.size(),error), "camera point in a wall rejected");
|
|
for (const char* file : {"scene_badop.dat","scene_badref.dat","scene_badtext.dat","scene_badorder.dat","scene_badflags.dat","scene_noend.dat"}) {
|
|
Check(!app.LoadLevel(std::string("../assets/")+file) && app.cutscene.events.size()==9,
|
|
"malformed cutscene load preserves editor track");
|
|
}
|
|
std::string start, engine;
|
|
Check(app.PreparePlaytest(start,engine) && start=="assets\\scene.dat", "F5 packages v4 track and referenced WAV");
|
|
app.CleanupPlaytest();
|
|
app.cutscene.strings[1]="absent.wav";start.clear();engine.clear();
|
|
Check(!app.PreparePlaytest(start,engine), "F5 rejects missing cutscene audio before launch");
|
|
app.CleanupPlaytest();
|
|
app.LoadLevel("../assets/map.dat");
|
|
Check(app.cutscene.events.empty() && app.cutscene.strings.empty(), "legacy map clears previous track");
|
|
app.AddCutsceneEvent(OP_MOVE_CAM_TO);
|
|
Check(app.cutscene.events.size()==2 && app.placeCameraPoint, "timeline camera button inserts an end marker and enables map placement");
|
|
app.Undo();Check(app.cutscene.events.empty(), "undo includes cutscene commands");
|
|
app.Redo();Check(app.cutscene.events.size()==2, "redo restores cutscene commands");
|
|
app.cutscene = {};app.selectedCutsceneEvent=-1;
|
|
for(int i=0;i<40;++i) { app.AddCutsceneEvent(OP_SHOW_SUBTITLE);app.DeleteCutsceneEvent(); }
|
|
Check(app.cutscene.strings.empty(), "deleting commands reclaims string pool slots");
|
|
app.AddCutsceneEvent(OP_CHANGE_SUBLEVEL);
|
|
app.SetCell(4,2,CELL_EMPTY);
|
|
Check(std::none_of(app.cutscene.events.begin(),app.cutscene.events.end(),[](const auto& e){return e.opCode==OP_CHANGE_SUBLEVEL;}),
|
|
"removing a portal repairs cutscene transition references");
|
|
app.LoadLevel("../assets/scene.dat");app.showCutsceneTimeline=true;
|
|
ImGui::CreateContext();
|
|
auto& io=ImGui::GetIO();io.DisplaySize=ImVec2(1360,840);io.DeltaTime=1.0f/60;io.IniFilename=nullptr;
|
|
unsigned char* pixels;int w,h;io.Fonts->GetTexDataAsRGBA32(&pixels,&w,&h);
|
|
ImGui::NewFrame();app.RenderUI();ImGui::Render();
|
|
Check(ImGui::GetDrawData()->TotalVtxCount>0,"editor timeline renders without a native window");
|
|
io.AddKeyEvent(ImGuiKey_F5,true);
|
|
ImGui::NewFrame(); io.WantCaptureKeyboard=true; io.WantTextInput=true;
|
|
app.HandleHotkeys();
|
|
Check(app.isEngineRunning,"F5 launches even when ImGui captures keyboard input");
|
|
ImGui::Render();
|
|
if(app.hEngineProcess) WaitForSingleObject(app.hEngineProcess,5000);
|
|
app.CheckPlaytestProcess();
|
|
io.AddKeyEvent(ImGuiKey_F5,false); io.AddKeyEvent(ImGuiKey_F6,true);
|
|
ImGui::NewFrame(); io.WantCaptureKeyboard=true; io.WantTextInput=true;
|
|
app.HandleHotkeys();
|
|
Check(!app.showCutsceneTimeline,"F6 remains available while a text widget owns focus");
|
|
ImGui::Render();
|
|
ImGui::DestroyContext();
|
|
app.showCutsceneTimeline=false;
|
|
}
|