diff --git a/source/common/engine/namedef.h b/source/common/engine/namedef.h index cf8b8da62..8ec5d4081 100644 --- a/source/common/engine/namedef.h +++ b/source/common/engine/namedef.h @@ -1107,4 +1107,4 @@ xy(menu_change, "menu/change") xy(menu_advance, "menu/advance") xx(zoomsize) - +xx(ScreenJobRunner) diff --git a/source/core/gamecontrol.cpp b/source/core/gamecontrol.cpp index 396abc5de..0e2435321 100644 --- a/source/core/gamecontrol.cpp +++ b/source/core/gamecontrol.cpp @@ -1507,6 +1507,7 @@ DEFINE_FIELD_X(SummaryInfo, SummaryInfo, maxkills) DEFINE_FIELD_X(SummaryInfo, SummaryInfo, secrets) DEFINE_FIELD_X(SummaryInfo, SummaryInfo, maxsecrets) DEFINE_FIELD_X(SummaryInfo, SummaryInfo, supersecrets) +DEFINE_FIELD_X(SummaryInfo, SummaryInfo, playercount) DEFINE_FIELD_X(SummaryInfo, SummaryInfo, time) DEFINE_FIELD_X(SummaryInfo, SummaryInfo, cheated) DEFINE_FIELD_X(SummaryInfo, SummaryInfo, endofgame) diff --git a/source/core/mainloop.cpp b/source/core/mainloop.cpp index 7a65c950d..da2d99565 100644 --- a/source/core/mainloop.cpp +++ b/source/core/mainloop.cpp @@ -331,7 +331,11 @@ static void GameTicker() break; case GS_INTERMISSION: case GS_INTRO: - ScreenJobTick(); + if (ScreenJobTick()) + { + // synchronize termination with the playsim. + Net_WriteByte(DEM_ENDSCREENJOB); + } break; } diff --git a/source/core/mapinfo.h b/source/core/mapinfo.h index 039d3d264..cdd607daf 100644 --- a/source/core/mapinfo.h +++ b/source/core/mapinfo.h @@ -145,6 +145,7 @@ struct SummaryInfo int maxsecrets; int supersecrets; int time; + int playercount; bool cheated; bool endofgame; }; diff --git a/source/core/parsefuncs.h b/source/core/parsefuncs.h index 778ea2532..888df2157 100644 --- a/source/core/parsefuncs.h +++ b/source/core/parsefuncs.h @@ -63,6 +63,7 @@ static void parseCutscene(FScanner& sc, CutsceneDef& cdef) if (sc.Compare("video")) { sc.GetString(cdef.video); cdef.function = ""; } else if (sc.Compare("function")) { sc.GetString(cdef.function); cdef.video = ""; } else if (sc.Compare("sound")) sc.GetString(sound); + else if (sc.Compare("fps")) sc.GetNumber(cdef.framespersec); else if (sc.Compare("clear")) { cdef.function = "none"; cdef.video = ""; } // this means 'play nothing', not 'not defined'. } if (sound.IsNotEmpty()) diff --git a/source/core/screenjob.cpp b/source/core/screenjob.cpp index 33de0a84e..8f8c5e342 100644 --- a/source/core/screenjob.cpp +++ b/source/core/screenjob.cpp @@ -52,411 +52,186 @@ #include #include "raze_music.h" #include "vm.h" +#include "mapinfo.h" +static DObject* runner; +static SummaryInfo sinfo; +static PClass* runnerclass; +static PType* runnerclasstype; +static PType* maprecordtype; +static PType* summaryinfotype; +static CompletionFunc completion; +static int ticks; -#if 0 -IMPLEMENT_CLASS(DScreenJob, true, false) -IMPLEMENT_CLASS(DSkippableScreenJob, true, false) -IMPLEMENT_CLASS(DBlackScreen, true, false) -IMPLEMENT_CLASS(DImageScreen, true, false) +//============================================================================= +// +// +// +//============================================================================= -DEFINE_FIELD(DScreenJob, flags) -DEFINE_FIELD(DScreenJob, fadetime) -DEFINE_FIELD_NAMED(DScreenJob, state, jobstate) -DEFINE_FIELD(DScreenJob, fadestate) -DEFINE_FIELD(DScreenJob, ticks) -DEFINE_FIELD(DScreenJob, pausable) - -DEFINE_FIELD(DBlackScreen, wait) -DEFINE_FIELD(DBlackScreen, cleared) - -DEFINE_FIELD(DImageScreen, tilenum) -DEFINE_FIELD(DImageScreen, trans) -DEFINE_FIELD(DImageScreen, waittime) -DEFINE_FIELD(DImageScreen, cleared) -DEFINE_FIELD(DImageScreen, texid) - -DEFINE_ACTION_FUNCTION(DScreenJob, Init) +static void Job_Init() { - // todo - return 0; -} - -DEFINE_ACTION_FUNCTION(DScreenJob, ProcessInput) -{ - PARAM_SELF_PROLOGUE(DScreenJob); - ACTION_RETURN_BOOL(self->ProcessInput()); -} - -DEFINE_ACTION_FUNCTION(DScreenJob, Start) -{ - PARAM_SELF_PROLOGUE(DScreenJob); - self->Start(); - return 0; -} - -DEFINE_ACTION_FUNCTION(DScreenJob, OnEvent) -{ - PARAM_SELF_PROLOGUE(DScreenJob); - PARAM_POINTER(evt, FInputEvent); - if (evt->Type != EV_KeyDown) + static bool done = false; + if (!done) { - // not needed in the transition phase - ACTION_RETURN_BOOL(false); + done = true; + GC::AddMarkerFunc([] { GC::Mark(runner); }); } - event_t ev = {}; - ev.type = EV_KeyDown; - ev.data1 = evt->KeyScan; - ACTION_RETURN_BOOL(self->OnEvent(&ev)); + runnerclass = PClass::FindClass("ScreenJobRunner"); + if (!runnerclass) I_FatalError("ScreenJobRunner not defined"); + runnerclasstype = NewPointer(runnerclass); + + maprecordtype = NewPointer(NewStruct("MapRecord", nullptr, true)); + summaryinfotype = NewPointer(NewStruct("SummaryInfo", nullptr, true)); } -DEFINE_ACTION_FUNCTION(DScreenJob, OnTick) +//============================================================================= +// +// +// +//============================================================================= + +static VMFunction* LookupFunction(const char* qname, bool validate = true) { - PARAM_SELF_PROLOGUE(DScreenJob); - self->OnTick(); - return 0; -} + int p = strcspn(qname, "."); + if (p == 0) I_Error("Call to undefined function %s", qname); + FString clsname(qname, p); + FString funcname = qname + p + 1; -DEFINE_ACTION_FUNCTION(DScreenJob, Draw) -{ - PARAM_SELF_PROLOGUE(DScreenJob); - PARAM_FLOAT(smooth); - self->Draw(smooth); - return 0; -} - -DEFINE_ACTION_FUNCTION(DSkippableScreenJob, Init) -{ - // todo - return 0; -} - -DEFINE_ACTION_FUNCTION(DSkippableScreenJob, Skipped) -{ - PARAM_SELF_PROLOGUE(DSkippableScreenJob); - self->Skipped(); - return 0; -} - -DEFINE_ACTION_FUNCTION(DBlackScreen, Init) -{ - // todo - return 0; -} - -DEFINE_ACTION_FUNCTION(DImageScreen, Init) -{ - // todo - return 0; -} - - - - -void DScreenJob::OnDestroy() -{ - if (flags & stopmusic) Mus_Stop(); - if (flags & stopsound) FX_StopAllSounds(); -} - -bool DSkippableScreenJob::OnEvent(event_t* evt) -{ - if (evt->type == EV_KeyDown && !specialKeyEvent(evt)) + auto func = PClass::FindFunction(clsname, funcname); + if (func == nullptr) I_Error("Call to undefined function %s", qname); + if (validate) { - state = skipped; - Skipped(); + // these conditions must be met by all functions for this interface. + if (func->Proto->ReturnTypes.Size() != 0) I_Error("Bad cutscene function %s. Return value not allowed", qname); + if (func->ImplicitArgs != 0) I_Error("Bad cutscene function %s. Must be static", qname); } - return true; + return func; } -void DBlackScreen::OnTick() +//============================================================================= +// +// +// +//============================================================================= + +void CallCreateFunction(const char* qname, DObject* runner) { - if (cleared) + auto func = LookupFunction(qname); + if (func->Proto->ArgumentTypes.Size() != 1) I_Error("Bad cutscene function %s. Must receive precisely one argument.", qname); + if (func->Proto->ArgumentTypes[0] != runnerclasstype) I_Error("Bad cutscene function %s. Must receive ScreenJobRunner reference.", qname); + VMValue val = runner; + VMCall(func, &val, 1, nullptr, 0); +} + +//============================================================================= +// +// +// +//============================================================================= + +void CallCreateMapFunction(const char* qname, DObject* runner, MapRecord* map) +{ + auto func = LookupFunction(qname); + if (func->Proto->ArgumentTypes.Size() != 2) I_Error("Bad map-cutscene function %s. Must receive precisely two arguments.", qname); + if (func->Proto->ArgumentTypes[0] != runnerclasstype && func->Proto->ArgumentTypes[1] != maprecordtype) + I_Error("Bad cutscene function %s. Must receive ScreenJobRunner and MapRecord reference.", qname); + VMValue val[2] = { runner, map }; + VMCall(func, val, 2, nullptr, 0); +} + +//============================================================================= +// +// +// +//============================================================================= + +void CallCreateSummaryFunction(const char* qname, DObject* runner, MapRecord* map, SummaryInfo* info) +{ + auto func = LookupFunction(qname); + if (func->Proto->ArgumentTypes.Size() != 3) I_Error("Bad map-cutscene function %s. Must receive precisely three arguments.", qname); + if (func->Proto->ArgumentTypes[0] != runnerclasstype && func->Proto->ArgumentTypes[1] != maprecordtype && func->Proto->ArgumentTypes[2] != summaryinfotype) + I_Error("Bad cutscene function %s. Must receive ScreenJobRunner, MapRecord and SummaryInfo reference.", qname); + VMValue val[3] = { runner, map, info }; + VMCall(func, val, 3, nullptr, 0); +} + +//============================================================================= +// +// +// +//============================================================================= + +DObject* CreateRunner(bool clearbefore = true) +{ + auto obj = runnerclass->CreateNew(); + auto func = LookupFunction("ScreenJobRunner.Init", false); + VMValue val[3] = { obj, clearbefore, false }; + VMCall(func, val, 3, nullptr, 0); + return obj; +} + +//============================================================================= +// +// +// +//============================================================================= + +void AddGenericVideo(DObject* runner, const FString& fn, int soundid, int fps) +{ + auto obj = runnerclass->CreateNew(); + auto func = LookupFunction("ScreenJobRunner.AddGenericVideo", false); + VMValue val[] = { runner, &fn, soundid, fps }; + VMCall(func, val, 4, nullptr, 0); +} + +//============================================================================= +// +// +// +//============================================================================= + +void CutsceneDef::Create(DObject* runner) +{ + if (function.CompareNoCase("none") != 0) { - int span = ticks * 1000 / GameTicRate; - if (span > wait) state = finished; - } -} - -void DBlackScreen::Draw(double) -{ - cleared = true; - twod->ClearScreen(); -} - -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- - -void DImageScreen::OnTick() -{ - if (cleared) - { - int span = ticks * 1000 / GameTicRate; - if (span > waittime) state = finished; - } -} - - -void DImageScreen::Draw(double smoothratio) -{ - if (tilenum > 0) tex = tileGetTexture(tilenum, true); - twod->ClearScreen(); - if (tex) DrawTexture(twod, tex, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_LegacyRenderStyle, STYLE_Normal, DTA_TranslationIndex, trans, TAG_DONE); - cleared = true; -} - -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- - -ScreenJobRunner::ScreenJobRunner(TArray& jobs_, CompletionFunc completion_, bool clearbefore_, bool skipall_) - : completion(std::move(completion_)), clearbefore(clearbefore_), skipall(skipall_) -{ - jobs = std::move(jobs_); - // Release all jobs from the garbage collector - the code as it is cannot deal with them getting collected. This should be removed later once the GC is working. - for (unsigned i = 0; i < jobs.Size(); i++) - { - jobs[i]->Release(); - } - AdvanceJob(false); -} - -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- - -ScreenJobRunner::~ScreenJobRunner() -{ - DeleteJobs(); -} - -void ScreenJobRunner::DeleteJobs() -{ - for (auto& job : jobs) - { - job->ObjectFlags |= OF_YesReallyDelete; - delete job; - } - jobs.Clear(); -} - -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- - -void ScreenJobRunner::AdvanceJob(bool skip) -{ - if (index >= 0) - { - //if (jobs[index].postAction) jobs[index].postAction(); - jobs[index]->Destroy(); - } - index++; - while (index < jobs.Size() && (jobs[index] == nullptr || (skip && skipall))) - { - if (jobs[index] != nullptr) jobs[index]->Destroy(); - index++; - } - actionState = clearbefore ? State_Clear : State_Run; - if (index < jobs.Size()) - { - jobs[index]->fadestate = !paused && jobs[index]->flags & DScreenJob::fadein? DScreenJob::fadein : DScreenJob::visible; - jobs[index]->Start(); - } - inputState.ClearAllInput(); -} - -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- - -int ScreenJobRunner::DisplayFrame(double smoothratio) -{ - auto& job = jobs[index]; - auto now = I_GetTimeNS(); - bool processed = job->ProcessInput(); - - if (job->fadestate == DScreenJob::fadein) - { - double ms = (job->ticks + smoothratio) * 1000 / GameTicRate / job->fadetime; - float screenfade = (float)clamp(ms, 0., 1.); - twod->SetScreenFade(screenfade); - if (screenfade == 1.f) job->fadestate = DScreenJob::visible; - } - int state = job->DrawFrame(smoothratio); - twod->SetScreenFade(1.f); - return state; -} - -int ScreenJobRunner::FadeoutFrame(double smoothratio) -{ - auto& job = jobs[index]; - double ms = (fadeticks + smoothratio) * 1000 / GameTicRate / job->fadetime; - float screenfade = 1.f - (float)clamp(ms, 0., 1.); - twod->SetScreenFade(screenfade); - job->DrawFrame(1.); - return (screenfade > 0.f); -} - -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- - -bool ScreenJobRunner::OnEvent(event_t* ev) -{ - if (paused || index >= jobs.Size()) return false; - - if (jobs[index]->state != DScreenJob::running) return false; - - return jobs[index]->OnEvent(ev); -} - -void ScreenJobRunner::OnFinished() -{ - if (completion) completion(false); - completion = nullptr; // only finish once. -} - -void ScreenJobRunner::OnTick() -{ - if (paused) return; - if (index >= jobs.Size()) - { - //DeleteJobs(); - //twod->SetScreenFade(1); - //twod->ClearScreen(); // This must not leave the 2d buffer empty. - //if (gamestate == GS_INTRO) OnFinished(); - //else Net_WriteByte(DEM_ENDSCREENJOB); // intermissions must be terminated synchronously. - } - else - { - if (jobs[index]->state == DScreenJob::running) + if (function.IsNotEmpty()) { - jobs[index]->ticks++; - jobs[index]->OnTick(); + CallCreateFunction(function, runner); } - else if (jobs[index]->state == DScreenJob::stopping) + else if (video.IsNotEmpty()) { - fadeticks++; + AddGenericVideo(runner, video, sound, framespersec); } } } - -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- -bool ScreenJobRunner::RunFrame() +//============================================================================= +// +// +// +//============================================================================= + +void StartCutscene(CutsceneDef& cs, int flags, CompletionFunc completion_) { - if (index >= jobs.Size()) - { - DeleteJobs(); - twod->SetScreenFade(1); - twod->ClearScreen(); // This must not leave the 2d buffer empty. - if (completion) completion(false); - return false; - } - - // ensure that we won't go back in time if the menu is dismissed without advancing our ticker - bool menuon = paused; - if (menuon) last_paused_tic = jobs[index]->ticks; - else if (last_paused_tic == jobs[index]->ticks) menuon = true; - double smoothratio = menuon ? 1. : I_GetTimeFrac(); - - if (actionState == State_Clear) - { - actionState = State_Run; - twod->ClearScreen(); - } - else if (actionState == State_Run) - { - terminateState = DisplayFrame(smoothratio); - if (terminateState < 1) - { - // Must lock before displaying. - if (jobs[index]->flags & DScreenJob::fadeout) - { - jobs[index]->fadestate = DScreenJob::fadeout; - jobs[index]->state = DScreenJob::stopping; - actionState = State_Fadeout; - fadeticks = 0; - } - else - { - AdvanceJob(terminateState < 0); - } - } - } - else if (actionState == State_Fadeout) - { - int ended = FadeoutFrame(smoothratio); - if (ended < 1) - { - jobs[index]->state = DScreenJob::stopped; - AdvanceJob(terminateState < 0); - } - } - return true; + completion = completion_; + runner = CreateRunner(); + cs.Create(runner); + gameaction = (flags & SJ_BLOCKUI) ? ga_intro : ga_intermission; } -//--------------------------------------------------------------------------- -// -// -// -//--------------------------------------------------------------------------- - -ScreenJobRunner *runner; - -#endif - - -#if 0 -void RunScreenJob(TArray& jobs, CompletionFunc completion, int flags) -{ - assert(completion != nullptr); - videoclearFade(); - if (jobs.Size()) - { - runner = new ScreenJobRunner(jobs, completion, !(flags & SJ_DONTCLEAR), !!(flags & SJ_SKIPALL)); - gameaction = (flags & SJ_BLOCKUI)? ga_intro : ga_intermission; - } - else - { - completion(false); - } -} -#endif void DeleteScreenJob() { - /* - if (runner) - { - delete runner; - runner = nullptr; - } - twod->SetScreenFade(1);*/ + runner->Destroy(); + runner = nullptr; } void EndScreenJob() { - //if (runner) runner->OnFinished(); DeleteScreenJob(); + if (completion) completion(false); + completion = nullptr; } @@ -472,33 +247,47 @@ bool ScreenJobResponder(event_t* ev) return true; } } - - //if (runner) return runner->OnEvent(ev); + FInputEvent evt = ev; + if (runner) + { + IFVIRTUALPTRNAME(runner, NAME_ScreenJobRunner, OnEvent) + { + int result = 0; + VMValue parm[] = { runner, &evt }; + VMReturn ret(&result); + VMCall(func, parm, 2, &ret, 1); + return result; + } + } return false; } -void ScreenJobTick() +bool ScreenJobTick() { - //if (runner) runner->OnTick(); + ticks++; + if (runner) + { + IFVIRTUALPTRNAME(runner, NAME_ScreenJobRunner, OnTick) + { + int result = 0; + VMValue parm[] = { runner }; + VMCall(func, parm, 1, nullptr, 0); + return result; + } + } + return false; } -bool ScreenJobDraw() +void ScreenJobDraw() { - // we cannot recover from this because we have no completion callback to call. - /* - if (!runner) + double smoothratio = I_GetTimeFrac(); + + if (runner) { - // We can get here before a gameaction has been processed. In that case just draw a black screen and wait. - if (gameaction == ga_nothing) I_Error("Trying to run a non-existent screen job"); - twod->ClearScreen(); - return false; + IFVIRTUALPTRNAME(runner, NAME_ScreenJobRunner, RunFrame) + { + VMValue parm[] = { runner, smoothratio }; + VMCall(func, parm, 2, nullptr, 0); + } } - auto res = runner->RunFrame(); - */ int res = 0; - if (!res) - { - assert((gamestate != GS_INTERMISSION && gamestate != GS_INTRO) || gameaction != ga_nothing); - DeleteScreenJob(); - } - return res; } diff --git a/source/core/screenjob.h b/source/core/screenjob.h index 2f5d2a6d1..91fd47c71 100644 --- a/source/core/screenjob.h +++ b/source/core/screenjob.h @@ -181,23 +181,17 @@ public: #endif -#if 0 enum { - SJ_DONTCLEAR = 1, - SJ_BLOCKUI = 2, - SJ_SKIPALL = 4 + SJ_BLOCKUI = 1, }; +#if 0 void RunScreenJob(TArray& jobs, CompletionFunc completion, int flags = 0); #endif void EndScreenJob(); void DeleteScreenJob(); bool ScreenJobResponder(event_t* ev); -void ScreenJobTick(); -bool ScreenJobDraw(); - -#if 0 -DScreenJob *PlayVideo(const char *filename, const AnimSound *ans = nullptr, const int *frameticks = nullptr, bool nosoundstop = false); -#endif \ No newline at end of file +bool ScreenJobTick(); +void ScreenJobDraw(); diff --git a/wadsrc/static/zscript/games/duke/ui/cutscenes.zs b/wadsrc/static/zscript/games/duke/ui/cutscenes.zs index ed90b8149..6c3796187 100644 --- a/wadsrc/static/zscript/games/duke/ui/cutscenes.zs +++ b/wadsrc/static/zscript/games/duke/ui/cutscenes.zs @@ -203,7 +203,7 @@ struct DukeCutscenes // //--------------------------------------------------------------------------- - static void BuildMPSummary(ScreenJobRunner runner, int playerswhenstarted) + static void BuildMPSummary(ScreenJobRunner runner, MapRecord map, SummaryInfo stats) { runner.Append(new("DukeMultiplayerBonusScreen").Init(playerswhenstarted)); } @@ -301,9 +301,12 @@ struct RRCutscenes static void BuildE1End(ScreenJobRunner runner) { - Array soundinfo; - soundinfo.Pushv(1, RRSnd.CHKAMMO + 1); - runner.Append(MoviePlayerJob.CreateWithSoundinfo("turdmov.anm", soundinfo, 0, 9, 9, 9)); + if (!isRRRA()) + { + Array soundinfo; + soundinfo.Pushv(1, RRSnd.CHKAMMO + 1); + runner.Append(MoviePlayerJob.CreateWithSoundinfo("turdmov.anm", soundinfo, 0, 9, 9, 9)); + } } @@ -315,21 +318,17 @@ struct RRCutscenes static void BuildE2End(ScreenJobRunner runner) { - Array soundinfo; - soundinfo.Pushv(1, RRSnd.LN_FINAL + 1); - runner.Append(MoviePlayerJob.CreateWithSoundinfo("rr_outro.anm", soundinfo, 0, 9, 9, 9)); - runner.Append(ImageScreen.CreateNamed("TENSCREEN")); - } - - //--------------------------------------------------------------------------- - // - // - // - //--------------------------------------------------------------------------- - - static void BuildRRRAEnd(ScreenJobRunner runner) - { - runner.Append(new("RRRAEndOfGame").Init()); + if (!isRRRA()) + { + Array soundinfo; + soundinfo.Pushv(1, RRSnd.LN_FINAL + 1); + runner.Append(MoviePlayerJob.CreateWithSoundinfo("rr_outro.anm", soundinfo, 0, 9, 9, 9)); + runner.Append(ImageScreen.CreateNamed("TENSCREEN")); + } + else + { + runner.Append(new("RRRAEndOfGame").Init()); + } } //--------------------------------------------------------------------------- diff --git a/wadsrc/static/zscript/razebase.zs b/wadsrc/static/zscript/razebase.zs index 6346a4e7a..c2da19342 100644 --- a/wadsrc/static/zscript/razebase.zs +++ b/wadsrc/static/zscript/razebase.zs @@ -95,6 +95,7 @@ struct SummaryInfo native native readonly int maxsecrets; native readonly int supersecrets; native readonly int time; + native readonly int playercount; native readonly bool cheated; native readonly bool endofgame; } diff --git a/wadsrc/static/zscript/screenjob.zs b/wadsrc/static/zscript/screenjob.zs index 8ae6e6f3a..91ccba7dd 100644 --- a/wadsrc/static/zscript/screenjob.zs +++ b/wadsrc/static/zscript/screenjob.zs @@ -326,7 +326,7 @@ class ScreenJobRunner : Object int fadeticks; int last_paused_tic; - void Init(bool clearbefore_ = true, bool skipall_ = false) + void Init(bool clearbefore_, bool skipall_) { clearbefore = clearbefore_; skipall = skipall_; @@ -392,6 +392,11 @@ class ScreenJobRunner : Object virtual int DisplayFrame(double smoothratio) { + if (jobs.Size() == 0) + { + screen.ClearScreen(); + return 1; + } int x = index >= jobs.Size()? jobs.Size()-1 : index; let job = jobs[x]; bool processed = job.ProcessInput(); @@ -448,6 +453,7 @@ class ScreenJobRunner : Object virtual bool OnTick() { if (paused) return false; + if (jobs.Size() == 0) return true; if (advance) { advance = false; @@ -517,4 +523,11 @@ class ScreenJobRunner : Object } return true; } + + void AddGenericVideo(String fn, int snd, int framerate) + { + Array sounds; + if (snd > 0) sounds.Pushv(1, snd); + Append(MoviePlayerJob.CreateWithSoundInfo(fn, sounds, 0, framerate)); + } }