class ScreenJob : Object { int flags; float fadetime; // in milliseconds int fadestate; int ticks; int jobstate; bool pausable; enum EJobState { running = 1, // normal operation skipped = 2, // finished by user skipping finished = 3, // finished by completing its sequence stopping = 4, // running ending animations / fadeout, etc. Will not accept more input. stopped = 5, // we're done here. }; enum EJobFlags { visible = 0, fadein = 1, fadeout = 2, stopmusic = 4, stopsound = 8, }; void Init(int flags = 0, float fadet = 250.f) { flags = fadet; fadetime = fadet; jobstate = running; pausable = true; } virtual bool ProcessInput() { return false; } virtual void Start() {} virtual bool OnEvent(InputEvent evt) { return false; } virtual void OnTick() {} virtual void Draw(double smoothratio) {} virtual void OnSkip() {} int DrawFrame(double smoothratio) { if (jobstate != running) smoothratio = 1; // this is necessary to avoid having a negative time span because the ticker won't be incremented anymore. Draw(smoothratio); if (jobstate == skipped) return -1; if (jobstate == finished) return 0; return 1; } int GetFadeState() { return fadestate; } override void OnDestroy() { if (flags & stopmusic) Raze.StopMusic(); if (flags & stopsound) Raze.StopAllSounds(); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- class SkippableScreenJob : ScreenJob { void Init(int flags = 0, float fadet = 250.f) { Super.Init(flags, fadet); } override bool OnEvent(InputEvent evt) { if (evt.type == InputEvent.Type_KeyDown && !Raze.specialKeyEvent(evt)) { jobstate = skipped; OnSkip(); } return true; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- class BlackScreen : ScreenJob { int wait; bool cleared; ScreenJob Init(int w, int flags = 0) { Super.Init(flags & ~(fadein|fadeout)); wait = w; cleared = false; return self; } static ScreenJob Create(int w, int flags = 0) { return new("BlackScreen").Init(w, flags); } override void OnTick() { if (cleared) { int span = ticks * 1000 / GameTicRate; if (span > wait) jobstate = finished; } } override void Draw(double smooth) { cleared = true; Screen.ClearScreen(); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- class ImageScreen : SkippableScreenJob { int tilenum; int trans; int waittime; // in ms. bool cleared; TextureID texid; ScreenJob Init(TextureID tile, int fade = fadein | fadeout, int wait = 3000, int translation = 0) { Super.Init(fade); waittime = wait; texid = tile; trans = translation; cleared = false; return self; } ScreenJob InitNamed(String tex, int fade = fadein | fadeout, int wait = 3000, int translation = 0) { Super.Init(fade); waittime = wait; texid = TexMan.CheckForTexture(tex, TexMan.Type_Any, TexMan.TryAny | TexMan.ForceLookup); trans = translation; cleared = false; return self; } static ScreenJob Create(TextureID tile, int fade = fadein | fadeout, int wait = 3000, int translation = 0) { return new("ImageScreen").Init(tile, fade, wait, translation); } static ScreenJob CreateNamed(String tex, int fade = fadein | fadeout, int wait = 3000, int translation = 0) { return new("ImageScreen").InitNamed(tex, fade, wait, translation); } override void OnTick() { if (cleared) { int span = ticks * 1000 / GameTicRate; if (span > waittime) jobstate = finished; } } override void Draw(double smooth) { Screen.ClearScreen(); if (texid.IsValid()) Screen.DrawTexture(texid, true, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_LegacyRenderStyle, STYLE_Normal, DTA_TranslationIndex, trans); cleared = true; } } //--------------------------------------------------------------------------- // // this is to have a unified interface to the summary screens // that can be set up automatically by the games to avoid direct access to game data. // //--------------------------------------------------------------------------- class SummaryScreenBase : ScreenJob { MapRecord level; SummaryInfo stats; void SetParameters(MapRecord map, SummaryInfo thestats) { level = map; stats = thestats; } String FormatTime(int time) { return String.Format("%02d:%02d", (time / (26 * 60)) % 60, (time / 26) % 60); } } //--------------------------------------------------------------------------- // // internal polymorphic movie player object // //--------------------------------------------------------------------------- struct MoviePlayer native { enum EMovieFlags { NOSOUNDCUTOFF = 1, FIXEDVIEWPORT = 2, // Forces fixed 640x480 screen size like for Blood's intros. } native static MoviePlayer Create(String filename, Array soundinfo, int flags, int frametime, int firstframetime, int lastframetime); native void Start(); native bool Frame(double clock); native bool Destroy(); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- class MoviePlayerJob : SkippableScreenJob { MoviePlayer player; bool started; ScreenJob Init(MoviePlayer mp) { player = mp; pausable = false; return self; } static ScreenJob CreateWithSoundInfo(String filename, Array soundinfo, int flags, int frametime, int firstframetime = -1, int lastframetime = -1) { let movie = MoviePlayer.Create(filename, soundinfo, flags, frametime, firstframetime, lastframetime); if (movie) return new("MoviePlayerJob").Init(movie); return BlackScreen.Create(1); // do not return null. } static ScreenJob Create(String filename, int flags, int frametime = -1) { Array empty; return CreateWithSoundInfo(filename, empty, flags, frametime); } static ScreenJob CreateWithSound(String filename, Sound soundname, int flags, int frametime = -1) { Array empty; empty.Push(1); empty.Push(int(soundname)); return CreateWithSoundInfo(filename, empty, flags, frametime); } override void Draw(double smoothratio) { if (!player) { jobstate = stopped; return; } if (!started) { started = true; player.Start(); } double clock = (ticks + smoothratio) * 1000000000. / GameTicRate; if (jobstate == running && !player.Frame(clock)) { jobstate = finished; } } override void OnDestroy() { if (player) { player.Destroy(); } player = null; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- class ScreenJobRunner : Object { enum ERunState { State_Clear, State_Run, State_Fadeout, } Array jobs; //CompletionFunc completion; int index; float screenfade; bool clearbefore; bool skipall; bool advance; int actionState; int terminateState; int fadeticks; int last_paused_tic; void Init(bool clearbefore_, bool skipall_) { clearbefore = clearbefore_; skipall = skipall_; index = -1; fadeticks = 0; last_paused_tic = -1; } override void OnDestroy() { DeleteJobs(); } protected void DeleteJobs() { // Free all allocated resources now. for (int i = 0; i < jobs.Size(); i++) { if (jobs[i]) jobs[i].Destroy(); } jobs.Clear(); } void Append(ScreenJob job) { jobs.Push(job); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- protected void AdvanceJob(bool skip) { if (index == jobs.Size()-1) { index++; return; // we need to retain the last element until the runner is done. } if (index >= 0) jobs[index].Destroy(); index++; while (index < jobs.Size() && (jobs[index] == null || (skip && skipall))) { if (jobs[index] != null) jobs[index].Destroy(); index++; } actionState = clearbefore ? State_Clear : State_Run; if (index < jobs.Size()) { jobs[index].fadestate = !paused && jobs[index].flags & ScreenJob.fadein? ScreenJob.fadein : ScreenJob.visible; jobs[index].Start(); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- 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(); if (job.fadestate == ScreenJob.fadein) { double ms = (job.ticks + smoothratio) * 1000 / GameTicRate / job.fadetime; double screenfade = clamp(ms, 0., 1.); Screen.SetScreenFade(screenfade); if (screenfade == 1.) job.fadestate = ScreenJob.visible; } int state = job.DrawFrame(smoothratio); Screen.SetScreenFade(1.); return state; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- virtual int FadeoutFrame(double smoothratio) { int x = index >= jobs.Size()? jobs.Size()-1 : index; let job = jobs[x]; double ms = (fadeticks + smoothratio) * 1000 / GameTicRate / job.fadetime; float screenfade = 1. - clamp(ms, 0., 1.); Screen.SetScreenFade(screenfade); job.DrawFrame(1.); Screen.SetScreenFade(1.); return (screenfade > 0.); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- virtual bool OnEvent(InputEvent ev) { if (paused || index >= jobs.Size()) return false; if (jobs[index].jobstate != ScreenJob.running) return false; return jobs[index].OnEvent(ev); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- virtual bool OnTick() { if (paused) return false; if (jobs.Size() == 0) return true; if (advance) { advance = false; AdvanceJob(terminateState < 0); if (index >= jobs.Size()) { return true; } } if (jobs[index].jobstate == ScreenJob.running) { jobs[index].ticks++; jobs[index].OnTick(); } else if (jobs[index].jobstate == ScreenJob.stopping) { fadeticks++; } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- virtual bool RunFrame(double smoothratio) { // 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; if (menuon || index >= jobs.Size()) smoothratio = 1.; if (actionState == State_Clear) { actionState = State_Run; Screen.ClearScreen(); } else if (actionState == State_Run) { terminateState = DisplayFrame(smoothratio); if (terminateState < 1 && index < jobs.Size()) { if (jobs[index].flags & ScreenJob.fadeout) { jobs[index].fadestate = ScreenJob.fadeout; jobs[index].jobstate = ScreenJob.stopping; actionState = State_Fadeout; fadeticks = 0; } else { advance = true; } } } else if (actionState == State_Fadeout) { int ended = FadeoutFrame(smoothratio); if (ended < 1 && index < jobs.Size()) { jobs[index].jobstate = ScreenJob.stopped; advance = true; } } 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)); } }