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 == MAP_FORMAT_VERSION && 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(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(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; }