using Barotrauma.Extensions; using Barotrauma.Media; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; namespace Barotrauma { sealed class LoadingScreen { private readonly Sprite defaultBackgroundTexture, overlay; private readonly SpriteSheet decorativeGraph, decorativeMap; private Sprite currentBackgroundTexture; private readonly Sprite noiseSprite; private string randText = ""; private Sprite languageSelectionCursor; private ScalableFont languageSelectionFont, languageSelectionFontCJK; private Video currSplashScreen; private DateTime videoStartTime; public struct PendingSplashScreen { public string Filename; public float Gain; public PendingSplashScreen(string filename, float gain) { Filename = filename; Gain = gain; } } /// /// Triplet.first = filepath, Triplet.second = resolution, Triplet.third = audio gain /// public readonly ConcurrentQueue PendingSplashScreens = new ConcurrentQueue(); public bool PlayingSplashScreen { get { return currSplashScreen != null || PendingSplashScreens.Count > 0; } } private RichString selectedTip; private string selectedTipString; private ImmutableArray? selectedTipRichTextData; private void SetSelectedTip(LocalizedString tip) { selectedTip = RichString.Rich(tip); selectedTipString = string.Empty; selectedTipRichTextData = null; } public float LoadState; public bool WaitForLanguageSelection { get; set; } public LanguageIdentifier[] AvailableLanguages = null; public LoadingScreen(GraphicsDevice graphics) { defaultBackgroundTexture = new Sprite("Content/Map/LocationPortraits/MainMenu1.png", Vector2.Zero); decorativeMap = new SpriteSheet("Content/Map/MapHUD.png", 6, 5, Vector2.Zero, sourceRect: new Rectangle(0, 0, 2048, 640)); decorativeGraph = new SpriteSheet("Content/Map/MapHUD.png", 4, 10, Vector2.Zero, sourceRect: new Rectangle(1025, 1259, 1024, 732)); overlay = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); SetSelectedTip(TextManager.Get("LoadingScreenTip")); } public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphics, float deltaTime) { if (GameSettings.CurrentConfig.EnableSplashScreen) { try { DrawSplashScreen(spriteBatch, graphics); if (currSplashScreen != null || PendingSplashScreens.Count > 0) { return; } } catch (Exception e) { DebugConsole.ThrowError("Playing splash screen video failed", e); DisableSplashScreen(); } } drawn = true; currentBackgroundTexture ??= defaultBackgroundTexture; float overlayScale = Math.Min(GameMain.GraphicsWidth / overlay.size.X, GameMain.GraphicsHeight / overlay.size.Y); Rectangle drawArea = new Rectangle( (int)(overlay.size.X * overlayScale / 2), 0, (int)(GameMain.GraphicsWidth - overlay.size.X * overlayScale / 2), GameMain.GraphicsHeight); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea); overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); double noiseT = Timing.TotalTime * 0.02f; float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; noiseSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), startOffset: new Vector2(Rand.Range(0.0f, noiseSprite.SourceRect.Width), Rand.Range(0.0f, noiseSprite.SourceRect.Height)), color: Color.White * noiseStrength * 0.1f, textureScale: Vector2.One * noiseScale); Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.75f)); if (WaitForLanguageSelection) { DrawLanguageSelectionPrompt(spriteBatch, graphics); } else { LocalizedString loadText; var loadState = LoadState; // avoid multiple reads here to prevent jank if (loadState >= 100.0f) { #if DEBUG if (GameSettings.CurrentConfig.AutomaticQuickStartEnabled || GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled || (GameSettings.CurrentConfig.TestScreenEnabled && GameMain.FirstLoad)) { loadText = "QUICKSTARTING ..."; } else { #endif loadText = TextManager.Get("PressAnyKey"); #if DEBUG } #endif } else { loadText = TextManager.Get("Loading"); if (loadState >= 0f) { loadText += $" {loadState:N0} %"; } #if DEBUG if (GameMain.FirstLoad && GameMain.CancelQuickStart) { loadText += " (Quickstart aborted)"; } #endif } if (GUIStyle.LargeFont.HasValue) { GUIStyle.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), textPos, Color.White); textPos.Y += GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).Y * 1.2f; } if (GUIStyle.Font.HasValue && selectedTip != null && !selectedTip.SanitizedValue.IsNullOrEmpty()) { //store the string value of the LocalizedString to prevent the text from changing if/when new text packs are loaded during the loading screen if (selectedTipString.IsNullOrEmpty()) { selectedTipString = selectedTip.SanitizedValue; selectedTipRichTextData = selectedTip.RichTextData; } string wrappedTip = ToolBox.WrapText(selectedTipString, GameMain.GraphicsWidth * 0.3f, GUIStyle.Font.Value); string[] lines = wrappedTip.Split('\n'); float lineHeight = GUIStyle.Font.MeasureString(selectedTipString).Y; if (selectedTipRichTextData != null) { int rtdOffset = 0; for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawStringWithColors(spriteBatch, lines[i], new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTipRichTextData.Value, rtdOffset); rtdOffset += lines[i].Length; } } else { for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawString(spriteBatch, lines[i], new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), new Color(228, 217, 167, 255)); } } } } GUI.DrawMessageBoxesOnly(spriteBatch); spriteBatch.End(); spriteBatch.Begin(blendState: BlendState.Additive); Vector2 decorativeScale = new Vector2(GameMain.GraphicsHeight / 1080.0f); float noiseVal = (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.25f, Timing.TotalTime * 0.5f, 0); if (!WaitForLanguageSelection) { decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), new Vector2(GameMain.GraphicsWidth * 0.001f, textPos.Y), Color.White, new Vector2(0, decorativeMap.FrameSize.Y), 0.0f, decorativeScale, SpriteEffects.FlipVertically); } decorativeMap.Draw(spriteBatch, (int)(decorativeMap.FrameCount * noiseVal), new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.01f), Color.White, new Vector2(decorativeMap.FrameSize.X, 0), 0.0f, decorativeScale, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically); if (noiseVal < 0.2f) { //SCP-CB reference randText = (new string[] { "NIL", "black white gray", "Sometimes we would have had time to scream", "e8m106]af", "NO" }).GetRandomUnsynced(); } else if (noiseVal < 0.3f) { randText = ToolBox.RandomSeed(9); } else if (noiseVal < 0.5f) { randText = Rand.Int(100).ToString().PadLeft(2, '0') + " " + Rand.Int(100).ToString().PadLeft(2, '0') + " " + Rand.Int(100).ToString().PadLeft(2, '0') + " " + Rand.Int(100).ToString().PadLeft(2, '0'); } if (GUIStyle.LargeFont.HasValue) { Vector2 textSize = GUIStyle.LargeFont.MeasureString(randText); GUIStyle.LargeFont.DrawString(spriteBatch, randText, new Vector2(GameMain.GraphicsWidth * 0.95f - textSize.X, GameMain.GraphicsHeight * 0.06f), Color.White * (1.0f - noiseVal)); } spriteBatch.End(); } private void DrawLanguageSelectionPrompt(SpriteBatch spriteBatch, GraphicsDevice graphicsDevice) { if (AvailableLanguages is null) { return; } if (languageSelectionFont == null) { languageSelectionFont = new ScalableFont("Content/Fonts/NotoSans/NotoSans-Bold.ttf", (uint)(30 * (GameMain.GraphicsHeight / 1080.0f)), graphicsDevice); } if (languageSelectionFontCJK == null) { languageSelectionFontCJK = new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", (uint)(30 * (GameMain.GraphicsHeight / 1080.0f)), graphicsDevice, dynamicLoading: true); } if (languageSelectionCursor == null) { languageSelectionCursor = new Sprite("Content/UI/cursor.png", Vector2.Zero); } Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.3f)); Vector2 textSpacing = new Vector2(0.0f, GameMain.GraphicsHeight * 0.5f / AvailableLanguages.Length); foreach (LanguageIdentifier language in AvailableLanguages) { string localizedLanguageName = TextManager.GetTranslatedLanguageName(language); var font = TextManager.IsCJK(localizedLanguageName) ? languageSelectionFontCJK : languageSelectionFont; Vector2 textSize = font.MeasureString(localizedLanguageName); bool hover = PlayerInput.MousePosition.X > textPos.X && PlayerInput.MousePosition.X < textPos.X + textSize.X && PlayerInput.MousePosition.Y > textPos.Y && PlayerInput.MousePosition.Y < textPos.Y + textSize.Y; font.DrawString(spriteBatch, localizedLanguageName, textPos, hover ? Color.White : Color.White * 0.6f); if (hover && PlayerInput.PrimaryMouseButtonClicked()) { var config = GameSettings.CurrentConfig; config.Language = language; GameSettings.SetCurrentConfig(config); //reload tip in the selected language SetSelectedTip(TextManager.Get("LoadingScreenTip")); WaitForLanguageSelection = false; languageSelectionFont?.Dispose(); languageSelectionFont = null; languageSelectionFontCJK?.Dispose(); languageSelectionFontCJK = null; break; } textPos += textSpacing; } languageSelectionCursor.Draw(spriteBatch, PlayerInput.LatestMousePosition, scale: 0.5f); } private void DrawSplashScreen(SpriteBatch spriteBatch, GraphicsDevice graphics) { if (currSplashScreen == null) { if (!PendingSplashScreens.TryDequeue(out var newSplashScreen)) { return; } string fileName = newSplashScreen.Filename; try { currSplashScreen = Video.Load(graphics, GameMain.SoundManager, fileName); currSplashScreen.AudioGain = newSplashScreen.Gain; videoStartTime = DateTime.Now; } catch (Exception e) { DisableSplashScreen(); DebugConsole.ThrowError("Playing the splash screen \"" + fileName + "\" failed.", e); PendingSplashScreens.Clear(); currSplashScreen = null; } } if (currSplashScreen == null) { return; } if (currSplashScreen.IsPlaying) { graphics.Clear(Color.Black); float videoAspectRatio = (float)currSplashScreen.Width / (float)currSplashScreen.Height; int width; int height; if (GameMain.GraphicsHeight * videoAspectRatio > GameMain.GraphicsWidth) { width = GameMain.GraphicsWidth; height = (int)(GameMain.GraphicsWidth / videoAspectRatio); } else { width = (int)(GameMain.GraphicsHeight * videoAspectRatio); height = GameMain.GraphicsHeight; } spriteBatch.Begin(); spriteBatch.Draw( currSplashScreen.GetTexture(), destinationRectangle: new Rectangle( GameMain.GraphicsWidth / 2 - width / 2, GameMain.GraphicsHeight / 2 - height / 2, width, height), sourceRectangle: new Rectangle(0, 0, currSplashScreen.Width, currSplashScreen.Height), Color.White, rotation: 0.0f, origin: Vector2.Zero, SpriteEffects.None, layerDepth: 0.0f); spriteBatch.End(); if (DateTime.Now > videoStartTime + new TimeSpan(0, 0, 0, 0, milliseconds: 500) && GameMain.WindowActive && (PlayerInput.KeyHit(Keys.Escape) || PlayerInput.KeyHit(Keys.Space) || PlayerInput.KeyHit(Keys.Enter) || PlayerInput.PrimaryMouseButtonDown())) { currSplashScreen.Dispose(); currSplashScreen = null; } } else if (DateTime.Now > videoStartTime + new TimeSpan(0, 0, 0, 0, milliseconds: 1500)) { currSplashScreen.Dispose(); currSplashScreen = null; } } private void DisableSplashScreen() { var config = GameSettings.CurrentConfig; config.EnableSplashScreen = false; GameSettings.SetCurrentConfig(config); } bool drawn; public IEnumerable DoLoading(IEnumerable loader) { drawn = false; LoadState = -1f; SetSelectedTip(TextManager.Get("LoadingScreenTip")); currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue)); if (GameMain.GameSession?.GameMode?.Missions is { } missions && missions.Any(m => m.Prefab.HasPortraits)) { currentBackgroundTexture = missions.Where(m => m.Prefab.HasPortraits).First().Prefab.GetPortrait(Rand.Int(int.MaxValue)); } while (!drawn) { yield return CoroutineStatus.Running; } CoroutineManager.StartCoroutine(loader); yield return CoroutineStatus.Running; while (CoroutineManager.IsCoroutineRunning(loader.ToString())) { yield return CoroutineStatus.Running; } LoadState = 100.0f; yield return CoroutineStatus.Success; } } }