From 0f6de8ada905a3c6886abea0ac1b305dd088d823 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Sat, 6 Mar 2021 18:39:28 +0200 Subject: [PATCH] Added files that were missing from last commit --- .../Events/Missions/OutpostDestroyMission.cs | 18 + .../ClientSource/GameSession/HintManager.cs | 653 ++++++++++++++++++ .../ClientSource/Map/SubmarinePreview.cs | 556 +++++++++++++++ .../ClientSource/Sounds/SoundBuffer.cs | 88 +++ .../ClientSource/Utils/SpriteRecorder.cs | 324 +++++++++ .../Events/Missions/OutpostDestroyMission.cs | 19 + .../Events/EventActions/UnlockPathAction.cs | 67 ++ .../Events/Missions/OutpostDestroyMission.cs | 154 +++++ Barotrauma/BarotraumaShared/hintmanager.xml | 62 ++ 9 files changed, 1941 insertions(+) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/Missions/OutpostDestroyMission.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs create mode 100644 Barotrauma/BarotraumaShared/hintmanager.xml diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs new file mode 100644 index 000000000..e12fc3691 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs @@ -0,0 +1,18 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class OutpostDestroyMission : AbandonedOutpostMission + { + public override void ClientReadInitial(IReadMessage msg) + { + base.ClientReadInitial(msg); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + var item = Item.ReadSpawnData(msg); + items.Add(item); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs new file mode 100644 index 000000000..44377a65b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -0,0 +1,653 @@ +using Barotrauma.IO; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + static class HintManager + { + private const string HintManagerFile = "hintmanager.xml"; + private static HashSet HintIdentifiers { get; set; } + private static Dictionary> HintTags { get; } = new Dictionary>(); + /// + /// Hints that have already been shown this round and shouldn't be shown shown again until the next round + /// + private static HashSet HintsIgnoredThisRound { get; } = new HashSet(); + private static GUIMessageBox ActiveHintMessageBox { get; set; } + private static Action OnUpdate { get; set; } + private static double TimeStoppedInteracting { get; set; } + + /// + /// Seconds before any reminders can be shown + /// + private static int TimeBeforeReminders { get; set; } + + /// + /// Seconds before another reminder can be shown + /// + private static int ReminderCooldown { get; set; } + + private static double TimeReminderLastDisplayed { get; set; } + + private static Queue HintQueue { get; } = new Queue(); + + public struct HintInfo + { + public string Identifier { get; } + public string Text { get; } + public Sprite Icon { get; } + public Color? IconColor { get; } + public Action OnUpdate { get; } + + public HintInfo(string hintIdentifier, string text, Sprite icon, Color? iconColor, Action onUpdate) + { + Identifier = hintIdentifier; + Text = text; + Icon = icon; + IconColor = iconColor; + OnUpdate = onUpdate; + } + } + + private static HashSet BallastHulls { get; } = new HashSet(); + + public static void Init() + { + if (File.Exists(HintManagerFile)) + { + var doc = XMLExtensions.TryLoadXml(HintManagerFile); + if (doc?.Root != null) + { + HintIdentifiers = new HashSet(); + foreach (var element in doc.Root.Elements()) + { + GetHintsRecursive(element, element.Name.ToString()); + } + } + else + { + DebugConsole.ThrowError($"File \"{HintManagerFile}\" is empty - cannot initialize the HintManager!"); + } + } + else + { + DebugConsole.ThrowError($"File \"{HintManagerFile}\" is missing - cannot initialize the HintManager!"); + } + + static void GetHintsRecursive(XElement element, string identifier) + { + if (!element.HasElements) + { + HintIdentifiers.Add(identifier); + if (element.GetAttributeStringArray("tags", null, convertToLowerInvariant: true) is string[] tags) + { + HintTags.TryAdd(identifier, tags.ToHashSet()); + } + return; + } + else if (element.Name.ToString().Equals("reminder")) + { + TimeBeforeReminders = element.GetAttributeInt("timebeforereminders", TimeBeforeReminders); + ReminderCooldown = element.GetAttributeInt("remindercooldown", ReminderCooldown); + } + foreach (var childElement in element.Elements()) + { + GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}"); + } + } + } + + public static void Update() + { + if (HintIdentifiers == null || GameMain.Config.DisableInGameHints) { return; } + if (GameMain.GameSession == null || !GameMain.GameSession.IsRunning) { return; } + + if (ActiveHintMessageBox != null) + { + if (ActiveHintMessageBox.Closed) + { + ActiveHintMessageBox = null; + OnUpdate = null; + } + else + { + OnUpdate?.Invoke(); + return; + } + } + else if (HintQueue.TryDequeue(out var hint)) + { + ActiveHintMessageBox = new GUIMessageBox(hint.Identifier, hint.Text, hint.Icon); + if (hint.IconColor.HasValue) { ActiveHintMessageBox.IconColor = hint.IconColor.Value; } + OnUpdate = hint.OnUpdate; + ActiveHintMessageBox.InnerFrame.Flash(color: hint.IconColor ?? Color.Orange, flashDuration: 0.75f); + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + } + + CheckIsInteracting(); + CheckIfDivingGearOutOfOxygen(); + CheckAdjacentHulls(); + CheckReminders(); + } + + public static void OnSetSelectedConstruction(Character character, Item oldConstruction, Item newConstruction) + { + if (oldConstruction == newConstruction) { return; } + if (Character.Controlled != null && Character.Controlled == character && oldConstruction != null && oldConstruction.GetComponent() == null) + { + TimeStoppedInteracting = Timing.TotalTime; + } + if (newConstruction != null && newConstruction.GetComponent() == null) + { + OnStartedInteracting(character, newConstruction); + } + } + + private static void OnStartedInteracting(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled || item == null) { return; } + + string hintIdentifierBase = "onstartedinteracting"; + + // onstartedinteracting.brokenitem + if (item.Repairables.Any(r => item.ConditionPercentage < r.RepairThreshold)) + { + if (EnqueueHint($"{hintIdentifierBase}.brokenitem")) { return; } + } + + // onstartedinteracting.lootingisstealing + if (item.Submarine?.Info?.Type == SubmarineType.Outpost && + item.ContainedItems.Any(i => i.SpawnedInOutpost)) + { + if (EnqueueHint($"{hintIdentifierBase}.lootingisstealing")) { return; } + } + + // onstartedinteracting.turretperiscope + if (item.HasTag("periscope") && + item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag("turret")) is Turret) + { + if (EnqueueHint($"{hintIdentifierBase}.turretperiscope", + variableTags: new string[] { "[shootkey]", "[deselectkey]", }, + variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect) })) + { return; } + } + + // onstartedinteracting.item... + hintIdentifierBase += ".item"; + foreach (string hintIdentifier in HintIdentifiers) + { + if (!hintIdentifier.StartsWith(hintIdentifierBase)) { continue; } + if (!HintTags.TryGetValue(hintIdentifier, out var hintTags)) { continue; } + if (!item.HasTag(hintTags)) { continue; } + if (EnqueueHint(hintIdentifier)) { return; } + } + } + + private static void CheckIsInteracting() + { + if (!CanDisplayHints()) { return; } + if (Character.Controlled?.SelectedConstruction == null) { return; } + + if (Character.Controlled.SelectedConstruction.GetComponent() is Reactor reactor && reactor.PowerOn && + Character.Controlled.SelectedConstruction.OwnInventory?.AllItems is IEnumerable containedItems && + containedItems.Count(i => i.HasTag("reactorfuel")) > 1) + { + if (EnqueueHint("onisinteracting.reactorwithextrarods")) { return; } + } + } + + public static void OnRoundStarted() + { + // Make sure everything's been reset properly, OnRoundEnded() isn't always called when exiting a game + Reset(); + + var initRoundHandle = CoroutineManager.StartCoroutine(InitRound(), "HintManager.InitRound"); + if (!CanDisplayHints(requireGameScreen: false)) { return; } + CoroutineManager.StartCoroutine(DisplayRoundStartedHints(initRoundHandle), "HintManager.DisplayRoundStartedHints"); + + static IEnumerable InitRound() + { + while (Character.Controlled == null) { yield return CoroutineStatus.Running; } + // Get the ballast hulls on round start not to find them again and again later + BallastHulls.Clear(); + var sub = Submarine.MainSubs.FirstOrDefault(s => s != null && s.TeamID == Character.Controlled.TeamID); + if (sub != null) + { + foreach (var item in sub.GetItems(true)) + { + if (item.CurrentHull == null) { continue; } + if (item.GetComponent() == null) { continue; } + if (!item.HasTag("ballast")) { continue; } + BallastHulls.Add(item.CurrentHull); + } + } + yield return CoroutineStatus.Success; + } + + static IEnumerable DisplayRoundStartedHints(CoroutineHandle initRoundHandle) + { + while (GameMain.Instance.LoadingScreenOpen || Screen.Selected != GameMain.GameScreen || + CoroutineManager.IsCoroutineRunning(initRoundHandle) || + CoroutineManager.IsCoroutineRunning("LevelTransition") || + CoroutineManager.IsCoroutineRunning("SinglePlayerCampaign.DoInitialCameraTransition") || + CoroutineManager.IsCoroutineRunning("MultiPlayerCampaign.DoInitialCameraTransition") || + GUIMessageBox.VisibleBox != null || Character.Controlled == null) + { + yield return CoroutineStatus.Running; + } + + OnStartedControlling(); + + if (!GameMain.GameSession.GameMode.IsSinglePlayer && + GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) + { + EnqueueHint("onroundstarted.voipdisabled", onUpdate: () => + { + if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) { return; } + ActiveHintMessageBox.Close(); + }); + } + + yield return CoroutineStatus.Success; + } + } + + public static void OnRoundEnded() + { + Reset(); + } + + private static void Reset() + { + CoroutineManager.StopCoroutines("HintManager.InitRound"); + CoroutineManager.StopCoroutines("HintManager.DisplayRoundStartedHints"); + if (ActiveHintMessageBox != null) + { + GUIMessageBox.MessageBoxes.Remove(ActiveHintMessageBox); + ActiveHintMessageBox = null; + } + OnUpdate = null; + HintQueue.Clear(); + HintsIgnoredThisRound.Clear(); + } + + public static void OnSonarSpottedCharacter(Item sonar, Character spottedCharacter) + { + if (!CanDisplayHints()) { return; } + if (sonar == null || sonar.Removed) { return; } + if (spottedCharacter == null || spottedCharacter.Removed || spottedCharacter.IsDead) { return; } + if (Character.Controlled == null || Character.Controlled.SelectedConstruction != sonar) { return; } + if (HumanAIController.IsFriendly(Character.Controlled, spottedCharacter)) { return; } + EnqueueHint("onsonarspottedenemy"); + } + + public static void OnAfflictionDisplayed(Character character, Affliction affliction) + { + if (!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled || affliction?.Prefab == null) { return; } + EnqueueHint("onafflictiondisplayed", + variableTags: new string[1] { "[key]" }, + variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Health) }, + icon: affliction.Prefab.Icon, + iconColor: CharacterHealth.GetAfflictionIconColor(affliction), + onUpdate: () => + { + if (CharacterHealth.OpenHealthWindow == null) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnShootWithoutAiming(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled) { return; } + if (character.SelectedConstruction != null || character.FocusedItem != null) { return; } + if (item == null || !item.IsShootable || !item.RequireAimToUse) { return; } + if (GUI.MouseOn != null) { return; } + if (TimeStoppedInteracting + 1 > Timing.TotalTime) { return; } + string hintIdentifier = "onshootwithoutaiming"; + if (!HintTags.TryGetValue(hintIdentifier, out var tags)) { return; } + if (!item.HasTag(tags)) { return; } + EnqueueHint(hintIdentifier, + variableTags: new string[1] { "[key]" }, + variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Aim) }, + onUpdate: () => + { + if (character.SelectedConstruction == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim)) + { + ActiveHintMessageBox.Close(); + } + }); + } + + public static void OnWeldingDoor(Character character) + { + if(!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled) { return; } + EnqueueHint("onweldingdoor"); + } + + public static void OnTryOpenStuckDoor(Character character) + { + if (!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled) { return; } + EnqueueHint("ontryopenstuckdoor"); + } + + public static void OnShowCampaignInterface(CampaignMode.InteractionType interactionType) + { + if (!CanDisplayHints()) { return; } + if (interactionType == CampaignMode.InteractionType.None) { return; } + string hintIdentifier = $"onshowcampaigninterface.{interactionType.ToString().ToLowerInvariant()}"; + EnqueueHint(hintIdentifier, + onUpdate: () => + { + + if (!(GameMain.GameSession?.Campaign is CampaignMode campaign) || + (!campaign.ShowCampaignUI && !campaign.ForceMapUI) || + campaign.CampaignUI?.SelectedTab != CampaignMode.InteractionType.Map) + { + ActiveHintMessageBox.Close(); + } + }); + } + + public static void OnShowCommandInterface() + { + IgnoreReminder("commandinterface"); + if (!CanDisplayHints()) { return; } + EnqueueHint("onshowcommandinterface", + onUpdate: () => + { + if (CrewManager.IsCommandInterfaceOpen) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnShowHealthInterface() + { + if (!CanDisplayHints()) { return; } + if (CharacterHealth.OpenHealthWindow == null) { return; } + EnqueueHint("onshowhealthinterface", onUpdate: () => + { + if (CharacterHealth.OpenHealthWindow != null) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnStoleItem(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled) { return; } + if (item == null || !item.SpawnedInOutpost || !item.StolenDuringRound) { return; } + EnqueueHint("onstoleitem", onUpdate: () => + { + if (item == null || item.Removed || item.GetRootInventoryOwner() != character) + { + ActiveHintMessageBox.Close(); + } + }); + } + + public static void OnHandcuffed(Character character) + { + if (!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled || !character.LockHands) { return; } + EnqueueHint("onhandcuffed", onUpdate: () => + { + if (character != null && !character.Removed && character.LockHands) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnReactorOutOfFuel(Reactor reactor) + { + if (!CanDisplayHints()) { return; } + if (Character.Controlled == null || reactor == null) { return; } + if (reactor.Item.Submarine?.Info?.Type != SubmarineType.Player || reactor.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } + if (!HasValidJob("engineer")) { return; } + EnqueueHint("onreactoroutoffuel", onUpdate: () => + { + if (reactor?.Item != null && !reactor.Item.Removed && reactor.AvailableFuel < 1) { return; } + ActiveHintMessageBox.Close(); + }); + } + + public static void OnAvailableTransition(CampaignMode.TransitionType transitionType) + { + if (!CanDisplayHints()) { return; } + if (transitionType == CampaignMode.TransitionType.None) { return; } + EnqueueHint($"onavailabletransition.{transitionType.ToString().ToLowerInvariant()}"); + } + + public static void OnShowSubInventory(Item item) + { + if (item?.Prefab == null) { return; } + if (item.Prefab.Identifier.Equals("toolbelt", StringComparison.OrdinalIgnoreCase)) + { + IgnoreReminder("toolbelt"); + } + } + + public static void OnChangeCharacter() + { + IgnoreReminder("characterchange"); + if (!CanDisplayHints()) { return; } + OnStartedControlling(); + } + + private static void OnStartedControlling() + { + if (Level.IsLoadedOutpost) { return; } + if (Character.Controlled?.Info?.Job?.Prefab == null) { return; } + EnqueueHint($"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}", + icon: Character.Controlled.Info.Job.Prefab.Icon, + iconColor: Character.Controlled.Info.Job.Prefab.UIColor); + } + + public static void OnAutoPilotPathUpdated(Steering steering) + { + if (!CanDisplayHints()) { return; } + if (Character.Controlled == null) { return; } + if (!HasValidJob("captain")) { return; } + if (steering?.Item?.Submarine?.Info == null) { return; } + if (steering.Item.Submarine.Info.Type != SubmarineType.Player) { return; } + if (steering.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } + if (!steering.AutoPilot || steering.MaintainPos) { return; } + if (steering.SteeringPath?.CurrentNode?.Tunnel?.Type != Level.TunnelType.MainPath) { return; } + if (!steering.SteeringPath.Finished && steering.SteeringPath.NextNode != null) { return; } + if (steering.LevelStartSelected && (Level.Loaded.StartOutpost == null || !steering.Item.Submarine.AtStartPosition)) { return; } + if (steering.LevelEndSelected && (Level.Loaded.EndOutpost == null || !steering.Item.Submarine.AtEndPosition)) { return; } + EnqueueHint("onautopilotreachedoutpost"); + } + + public static void OnStatusEffectApplied(ItemComponent component, ActionType actionType, Character character) + { + if (!CanDisplayHints()) { return; } + if (character == null || character != Character.Controlled) { return; } + // Could make this more generic if there will ever be any other status effect related hints + if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; } + EnqueueHint("onrepairfailed"); + } + + private static void CheckIfDivingGearOutOfOxygen() + { + if (!CanDisplayHints()) { return; } + var divingGear = Character.Controlled?.GetEquippedItem("diving"); + if (divingGear?.OwnInventory == null) { return; } + if (divingGear.GetContainedItemConditionPercentage() > 0.05f) { return; } + EnqueueHint("ondivinggearoutofoxygen", onUpdate: () => + { + if (divingGear == null || divingGear.Removed || + Character.Controlled == null || !Character.Controlled.HasEquippedItem(divingGear) || + divingGear.GetContainedItemConditionPercentage() > 0.05f) + { + ActiveHintMessageBox.Close(); + } + }); + } + + private static void CheckAdjacentHulls() + { + if (!CanDisplayHints()) { return; } + if (Character.Controlled?.CurrentHull == null) { return; } + foreach (var gap in Character.Controlled.CurrentHull.ConnectedGaps) + { + if (!gap.IsRoomToRoom) { continue; } + if (gap.ConnectedDoor == null || gap.ConnectedDoor.Impassable) { continue; } + if (Vector2.DistanceSquared(Character.Controlled.WorldPosition, gap.ConnectedDoor.Item.WorldPosition) > 400 * 400) { continue; } + foreach (var me in gap.linkedTo) + { + if (me == Character.Controlled.CurrentHull) { continue; } + if (!(me is Hull adjacentHull)) { continue; } + if (adjacentHull.LethalPressure > 5.0f && EnqueueHint("onadjacenthull.highpressure")) { return; } + if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && EnqueueHint("onadjacenthull.highwaterpercentage")) { return; } + } + } + } + + private static void CheckReminders() + { + if (!CanDisplayHints()) { return; } + if (Level.Loaded == null) { return; } + if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + TimeBeforeReminders) { return; } + if (Timing.TotalTime < TimeReminderLastDisplayed + ReminderCooldown) { return; } + + string hintIdentifierBase = "reminder"; + + if (GameMain.GameSession.GameMode.IsSinglePlayer) + { + if (EnqueueHint($"{hintIdentifierBase}.characterchange")) + { + TimeReminderLastDisplayed = Timing.TotalTime; + return; + } + } + + if (Level.Loaded.Type != LevelData.LevelType.Outpost) + { + if (EnqueueHint($"{hintIdentifierBase}.commandinterface", + variableTags: new string[] { "[commandkey]" }, + variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Command) }, + onUpdate: () => + { + if (!CrewManager.IsCommandInterfaceOpen) { return; } + ActiveHintMessageBox.Close(); + })) + { + TimeReminderLastDisplayed = Timing.TotalTime; + return; + } + } + + if (Character.Controlled?.Inventory?.GetItemInLimbSlot(InvSlotType.Bag)?.Prefab?.Identifier == "toolbelt") + { + if (EnqueueHint($"{hintIdentifierBase}.toolbelt")) + { + TimeReminderLastDisplayed = Timing.TotalTime; + return; + } + } + } + + private static bool EnqueueHint(string hintIdentifier, string[] variableTags = null, string[] variableValues = null, Sprite icon = null, Color? iconColor = null, Action onUpdate = null) + { + if (string.IsNullOrEmpty(hintIdentifier)) { return false; } + if (!HintIdentifiers.Contains(hintIdentifier)) { return false; } + if (GameMain.Config.IgnoredHints.Contains(hintIdentifier)) { return false; } + if (HintsIgnoredThisRound.Contains(hintIdentifier)) { return false; } + + string text; + string textTag = $"hint.{hintIdentifier}"; + if (variableTags != null && variableTags != null && variableTags.Length > 0 && variableTags.Length == variableValues.Length) + { + text = TextManager.GetWithVariables(textTag, variableTags, variableValues, returnNull: true); + } + else + { + text = TextManager.Get(textTag, returnNull: true); + } + + if (string.IsNullOrEmpty(text)) + { +#if DEBUG + DebugConsole.ThrowError($"No hint text found for text tag \"{textTag}\""); +#endif + return false; + } + + var hint = new HintInfo(hintIdentifier, text, icon, iconColor, onUpdate); + HintQueue.Enqueue(hint); + HintsIgnoredThisRound.Add(hintIdentifier); + return true; + } + + public static bool OnDontShowAgain(GUITickBox tickBox) + { + IgnoreHint((string)tickBox.UserData, ignore: tickBox.Selected); + return true; + } + + private static void IgnoreHint(string hintIdentifier, bool ignore = true) + { + if (string.IsNullOrEmpty(hintIdentifier)) { return; } + if (!HintIdentifiers.Contains(hintIdentifier)) + { +#if DEBUG + DebugConsole.ThrowError($"Tried to ignore a hint not defined in {HintManagerFile}: {hintIdentifier}"); +#endif + return; + } + if (ignore) + { + GameMain.Config.IgnoredHints.Add(hintIdentifier); + } + else + { + GameMain.Config.IgnoredHints.Remove(hintIdentifier); + } + } + + private static void IgnoreReminder(string reminderIdentifier) + { + HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}"); + } + + public static bool OnDisableHints(GUITickBox tickBox) + { + GameMain.Config.DisableInGameHints = tickBox.Selected; + return GameMain.Config.SaveNewPlayerConfig(); + } + + private static bool CanDisplayHints(bool requireGameScreen = true) + { + if (HintIdentifiers == null) { return false; } + if (GameMain.Config.DisableInGameHints) { return false; } + var gameMode = GameMain.GameSession?.GameMode; + if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; } + if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; } + return true; + } + + private static bool HasValidJob(string jobIdentifier) + { + // In singleplayer, we can control all character so we don't care about job restrictions + if (GameMain.GameSession.GameMode.IsSinglePlayer) { return true; } + if (Character.Controlled.HasJob(jobIdentifier)) { return true; } + // In multiplayer, if there are players with the job, display the hint to all players + foreach (var c in GameMain.GameSession.CrewManager.GetCharacters()) + { + if (c == null || !c.IsRemotePlayer) { continue; } + if (c.IsUnconscious || c.IsDead || c.Removed) { continue; } + if (!c.HasJob(jobIdentifier)) { continue; } + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs new file mode 100644 index 000000000..35714282a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -0,0 +1,556 @@ +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Barotrauma +{ + class SubmarinePreview : IDisposable + { + private SpriteRecorder spriteRecorder; + private SubmarineInfo submarineInfo; + private Camera camera; + private Task loadTask; + private volatile bool isDisposed; + + private GUIFrame previewFrame; + + private class HullCollection + { + public readonly List Rects; + public readonly string Name; + + public HullCollection(string identifier) + { + Rects = new List(); + Name = TextManager.Get(identifier, returnNull: true) ?? identifier; + } + + public void AddRect(XElement element) + { + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); + rect.Y = -rect.Y; + Rects.Add(rect); + } + } + + private struct Door + { + public readonly Rectangle Rect; + + public Door(Rectangle rect) + { + rect.Y = -rect.Y; + Rect = rect; + } + } + + private Dictionary hullCollections; + private List doors; + + + private static SubmarinePreview instance = null; + + public static void Create(SubmarineInfo submarineInfo) + { + instance?.Dispose(); + instance = new SubmarinePreview(submarineInfo); + } + + private SubmarinePreview(SubmarineInfo subInfo) + { + camera = new Camera(); + submarineInfo = subInfo; + spriteRecorder = new SpriteRecorder(); + isDisposed = false; + loadTask = null; + + hullCollections = new Dictionary(); + doors = new List(); + + previewFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, previewFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + new GUIButton(new RectTransform(Vector2.One, previewFrame.RectTransform), "", style: null) + { + OnClicked = (btn, obj) => { Dispose(); return false; } + }; + + var innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.9f, previewFrame.RectTransform, Anchor.Center)); + var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, innerFrame.RectTransform, Anchor.Center), isHorizontal: false); + var topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), verticalLayout.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + new GUITextBlock(new RectTransform(new Vector2(0.95f, 1f), topLayout.RectTransform), subInfo.DisplayName, font: GUI.LargeFont); + new GUIButton(new RectTransform(new Vector2(0.05f, 1f), topLayout.RectTransform), TextManager.Get("Close")) + { + OnClicked = (btn, obj) => { Dispose(); return false; } + }; + + new GUICustomComponent(new RectTransform(new Vector2(1f, 0.9f), verticalLayout.RectTransform, Anchor.Center), + (spriteBatch, component) => { camera.UpdateTransform(true); RenderSubmarine(spriteBatch, component.Rect); }, + (deltaTime, component) => { + camera.MoveCamera(deltaTime, overrideMouseOn: component.Rect); + if (component.Rect.Contains(PlayerInput.MousePosition) && + (PlayerInput.MidButtonHeld() || PlayerInput.LeftButtonHeld())) + { + Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / camera.Zoom; + moveSpeed.X = -moveSpeed.X; + camera.Position += moveSpeed; + } + }); + + GeneratePreviewMeshes(); + } + + public static void AddToGUIUpdateList() + { + instance?.previewFrame?.AddToGUIUpdateList(); + } + + public Task GeneratePreviewMeshes() + { + if (loadTask != null) { throw new InvalidOperationException("Tried to start SubmarinePreview loadTask more than once!"); } + loadTask = Task.Run(GeneratePreviewMeshesInternal); + return loadTask; + } + + private async Task GeneratePreviewMeshesInternal() + { + await Task.Yield(); + spriteRecorder.Begin(SpriteSortMode.BackToFront); + + HashSet toIgnore = new HashSet(); + + foreach (var subElement in submarineInfo.SubmarineElement.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "item": + foreach (var component in subElement.Elements()) + { + switch (component.Name.LocalName.ToLowerInvariant()) + { + case "itemcontainer": + ExtractItemContainerIds(component, toIgnore); + break; + case "connectionpanel": + ExtractConnectionPanelLinks(component, toIgnore); + break; + } + } + break; + } + if (isDisposed) { return; } + await Task.Yield(); + } + + foreach (var subElement in submarineInfo.SubmarineElement.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "item": + if (!toIgnore.Contains(subElement.GetAttributeInt("ID", 0))) + { + BakeMapEntity(subElement); + } + break; + case "structure": + BakeMapEntity(subElement); + break; + case "hull": + string identifier = subElement.GetAttributeString("roomname", "").ToLowerInvariant(); + if (!string.IsNullOrEmpty(identifier)) + { + HullCollection hullCollection = null; + if (!hullCollections.TryGetValue(identifier, out hullCollection)) + { + hullCollection = new HullCollection(identifier); + hullCollections.Add(identifier, hullCollection); + } + hullCollection.AddRect(subElement); + } + break; + } + if (isDisposed) { return; } + await Task.Yield(); + } + spriteRecorder.End(); + + camera.Position = (spriteRecorder.Min + spriteRecorder.Max) * 0.5f; + float scaledSpan = (spriteRecorder.Max - spriteRecorder.Min).X / camera.Resolution.X; + camera.Zoom = 0.8f / scaledSpan; + camera.StopMovement(); + } + + private void ExtractItemContainerIds(XElement component, HashSet ids) + { + string containedString = component.GetAttributeString("contained", ""); + string[] itemIdStrings = containedString.Split(','); + for (int i = 0; i < itemIdStrings.Length; i++) + { + foreach (string idStr in itemIdStrings[i].Split(';')) + { + if (!int.TryParse(idStr, out int id)) { continue; } + if (id != 0 && !ids.Contains(id)) { ids.Add(id); } + } + } + } + + private void ExtractConnectionPanelLinks(XElement component, HashSet ids) + { + var pins = component.Elements("input").Concat(component.Elements("output")); + foreach (var pin in pins) + { + var links = pin.Elements("link"); + foreach (var link in links) + { + int id = link.GetAttributeInt("w", 0); + if (id != 0 && !ids.Contains(id)) { ids.Add(id); } + } + } + } + + private void BakeMapEntity(XElement element) + { + string identifier = element.GetAttributeString("identifier", ""); + if (string.IsNullOrEmpty(identifier)) { return; } + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); + if (rect.Equals(Rectangle.Empty)) { return; } + + float depth = element.GetAttributeFloat("spritedepth", 1f); + bool flippedX = element.GetAttributeBool("flippedx", false); + bool flippedY = element.GetAttributeBool("flippedy", false); + + float scale = element.GetAttributeFloat("scale", 1f); + Color color = element.GetAttributeColor("spritecolor", Color.White); + + float rotation = element.GetAttributeFloat("rotation", 0f); + + MapEntityPrefab prefab = MapEntityPrefab.List.First(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + var texture = prefab.sprite.Texture; + var srcRect = prefab.sprite.SourceRect; + + SpriteEffects spriteEffects = SpriteEffects.None; + if (flippedX) + { + spriteEffects |= SpriteEffects.FlipHorizontally; + } + if (flippedY) + { + spriteEffects |= SpriteEffects.FlipVertically; + } + + var prevEffects = prefab.sprite.effects; + prefab.sprite.effects ^= spriteEffects; + + bool overrideSprite = false; + ItemPrefab itemPrefab = prefab as ItemPrefab; + StructurePrefab structurePrefab = prefab as StructurePrefab; + if (itemPrefab != null) + { + BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); + } + + if (!overrideSprite) + { + if (structurePrefab != null) + { + ParseUpgrades(structurePrefab.ConfigElement, ref scale); + + if (!prefab.ResizeVertical) + { + rect.Height = (int)(rect.Height * scale / prefab.Scale); + } + if (!prefab.ResizeHorizontal) + { + rect.Width = (int)(rect.Width * scale / prefab.Scale); + } + var textureScale = element.GetAttributeVector2("texturescale", Vector2.One); + + Vector2 backGroundOffset = Vector2.Zero; + + Vector2 textureOffset = element.GetAttributeVector2("textureoffset", Vector2.Zero); + if (flippedX) { textureOffset.X = -textureOffset.X; } + if (flippedY) { textureOffset.Y = -textureOffset.Y; } + + backGroundOffset = new Vector2( + MathUtils.PositiveModulo((int)-textureOffset.X, prefab.sprite.SourceRect.Width), + MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.sprite.SourceRect.Height)); + + prefab.sprite.DrawTiled( + spriteRecorder, + rect.Location.ToVector2() * new Vector2(1f, -1f), + rect.Size.ToVector2(), + color: color, + startOffset: backGroundOffset, + textureScale: textureScale * scale, + depth: depth); + } + else if (itemPrefab != null) + { + ParseUpgrades(itemPrefab.ConfigElement, ref scale); + + if (prefab.ResizeVertical || prefab.ResizeHorizontal) + { + if (!prefab.ResizeHorizontal) + { + rect.Width = (int)(prefab.sprite.size.X * scale); + } + if (!prefab.ResizeVertical) + { + rect.Height = (int)(prefab.sprite.size.Y * scale); + } + + var spritePos = rect.Center.ToVector2(); + //spritePos.Y = rect.Height - spritePos.Y; + + prefab.sprite.DrawTiled( + spriteRecorder, + rect.Location.ToVector2() * new Vector2(1f, -1f), + rect.Size.ToVector2(), + color: color, + textureScale: Vector2.One * scale, + depth: depth); + + foreach (var decorativeSprite in itemPrefab.DecorativeSprites) + { + float offsetState = 0f; + Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; + if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } + if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + decorativeSprite.Sprite.DrawTiled(spriteRecorder, + new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), + rect.Size.ToVector2(), color: color, + textureScale: Vector2.One * scale, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + } + } + else + { + rect.Width = (int)(rect.Width * scale / prefab.Scale); + rect.Height = (int)(rect.Height * scale / prefab.Scale); + + var spritePos = rect.Center.ToVector2(); + spritePos.Y -= rect.Height; + //spritePos.Y = rect.Height - spritePos.Y; + + prefab.sprite.Draw( + spriteRecorder, + spritePos * new Vector2(1f, -1f), + color, + prefab.sprite.Origin, + rotation, + scale, + prefab.sprite.effects, depth); + + foreach (var decorativeSprite in itemPrefab.DecorativeSprites) + { + float rotationState = 0f; float offsetState = 0f; + float rot = decorativeSprite.GetRotation(ref rotationState, 0f); + Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; + if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } + if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, + MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + } + } + } + } + + prefab.sprite.effects = prevEffects; + } + + private void BakeItemComponents( + ItemPrefab prefab, + Rectangle rect, Color color, + float scale, float rotation, float depth, + out bool overrideSprite) + { + overrideSprite = false; + + foreach (var subElement in prefab.ConfigElement.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "turret": + Sprite barrelSprite = null; + Sprite railSprite = null; + foreach (XElement turretSubElem in subElement.Elements()) + { + switch (turretSubElem.Name.ToString().ToLowerInvariant()) + { + case "barrelsprite": + barrelSprite = new Sprite(turretSubElem); + break; + case "railsprite": + railSprite = new Sprite(turretSubElem); + break; + } + } + + var transformedBarrelPos = MathUtils.RotatePointAroundTarget( + subElement.GetAttributeVector2("barrelpos", Vector2.Zero) * scale, + new Vector2(rect.Width / 2, rect.Height / 2), + MathHelper.ToRadians(rotation)); + + Vector2 drawPos = new Vector2(rect.X + transformedBarrelPos.X, rect.Y - transformedBarrelPos.Y); + drawPos.Y = -drawPos.Y; + + railSprite?.Draw(spriteRecorder, + drawPos, + color, + rotation + MathHelper.PiOver2, scale, + SpriteEffects.None, depth + (railSprite.Depth - prefab.sprite.Depth)); + + barrelSprite?.Draw(spriteRecorder, + drawPos - new Vector2((float)Math.Cos(MathHelper.ToRadians(rotation)), (float)Math.Sin(MathHelper.ToRadians(rotation))) * scale, + color, + rotation + MathHelper.PiOver2, scale, + SpriteEffects.None, depth + (barrelSprite.Depth - prefab.sprite.Depth)); + + break; + case "door": + doors.Add(new Door(rect)); + + var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase)); + if (doorSpriteElem != null) + { + string texturePath = subElement.GetAttributeString("texture", ""); + Vector2 pos = rect.Location.ToVector2() * new Vector2(1f, -1f); + if (subElement.GetAttributeBool("horizontal", false)) + { + pos.Y += (float)rect.Height * 0.5f; + } + else + { + pos.X += (float)rect.Width * 0.5f; + } + Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath)); + spriteRecorder.Draw(doorSprite.Texture, pos, + new Rectangle((int)doorSprite.SourceRect.X, + (int)doorSprite.SourceRect.Y, + (int)doorSprite.size.X, (int)doorSprite.size.Y), + color, 0.0f, doorSprite.Origin, new Vector2(scale), SpriteEffects.None, doorSprite.Depth); + } + break; + case "ladder": + var backgroundSprElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("backgroundsprite", StringComparison.OrdinalIgnoreCase)); + if (backgroundSprElem != null) + { + Sprite backgroundSprite = new Sprite(backgroundSprElem); + backgroundSprite.DrawTiled(spriteRecorder, + new Vector2(rect.Left, -rect.Top) - backgroundSprite.Origin * scale, + new Vector2(backgroundSprite.size.X * scale, rect.Height), color: color, + textureScale: Vector2.One * scale, + depth: depth + 0.1f); + } + break; + } + } + } + + public void ParseUpgrades(XElement prefabConfigElement, ref float scale) + { + foreach (var upgrade in prefabConfigElement.Elements("Upgrade")) + { + var upgradeVersion = new Version(upgrade.GetAttributeString("gameversion", "0.0.0.0")); + if (upgradeVersion >= submarineInfo.GameVersion) + { + string scaleModifier = upgrade.GetAttributeString("scale", "*1"); + + if (scaleModifier.StartsWith("*")) + { + scale *= float.Parse(scaleModifier.Substring(1)); + } + else + { + scale = float.Parse(scaleModifier); + } + } + } + } + + private void RenderSubmarine(SpriteBatch spriteBatch, Rectangle scissorRectangle) + { + if (spriteRecorder == null) { return; } + + GUI.DrawRectangle(spriteBatch, scissorRectangle, new Color(0.051f, 0.149f, 0.271f, 1.0f), isFilled: true); + + if (!spriteRecorder.ReadyToRender) + { + string waitText = "Generating preview..."; + GUI.Font.DrawString( + spriteBatch, + waitText, + scissorRectangle.Center.ToVector2(), + Color.White, + 0f, + GUI.Font.MeasureString(waitText) * 0.5f, + 1f, + SpriteEffects.None, + 0f); + return; + } + spriteBatch.End(); + + var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; + GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; + + spriteRecorder.Render(camera); + + var mousePos = camera.ScreenToWorld(PlayerInput.MousePosition); + mousePos.Y = -mousePos.Y; + + spriteBatch.Begin(SpriteSortMode.BackToFront, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); + GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; + foreach (var hullCollection in hullCollections.Values) + { + bool mouseOver = false; + + foreach (var rect in hullCollection.Rects) + { + mouseOver = rect.Contains(mousePos); + if (mouseOver) { break; } + } + + foreach (var rect in hullCollection.Rects) + { + GUI.DrawRectangle(spriteBatch, rect, mouseOver ? Color.Red : Color.Blue, depth: mouseOver ? 0.45f : 0.5f, thickness: (mouseOver ? 4f : 2f) / camera.Zoom); + } + + if (mouseOver) + { + string str = hullCollection.Name; + Vector2 strSize = GUI.Font.MeasureString(str) / camera.Zoom; + Vector2 padding = new Vector2(30, 30) / camera.Zoom; + Vector2 shift = new Vector2(10, 0) / camera.Zoom; + + GUI.DrawRectangle(spriteBatch, mousePos + shift, strSize + padding, Color.Black, isFilled: true, depth: 0.25f); + GUI.Font.DrawString(spriteBatch, str, mousePos + shift + (strSize + padding) * 0.5f, Color.White, 0f, strSize * camera.Zoom * 0.5f, 1f / camera.Zoom, SpriteEffects.None, 0f); + } + } + foreach (var door in doors) + { + GUI.DrawRectangle(spriteBatch, door.Rect, GUI.Style.Green * 0.5f, isFilled: true, depth: 0.4f); + } + spriteBatch.End(); + + GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred); + } + + public void Dispose() + { + previewFrame = null; + spriteRecorder?.Dispose(); + isDisposed = true; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs new file mode 100644 index 000000000..635b24f0f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundBuffer.cs @@ -0,0 +1,88 @@ +using Barotrauma.Extensions; +using OpenAL; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma.Sounds +{ + public class SoundBuffers : IDisposable + { + private static HashSet bufferPool = new HashSet(); +#if OSX + public const int MaxBuffers = 400; //TODO: check that this value works for macOS +#else + public const int MaxBuffers = 32000; +#endif + public static int BuffersGenerated { get; private set; } = 0; + private Sound sound; + + public uint AlBuffer { get; private set; } = 0; + public uint AlMuffledBuffer { get; private set; } = 0; + + public SoundBuffers(Sound sound) { this.sound = sound; } + public void Dispose() + { + if (AlBuffer != 0) { bufferPool.Add(AlBuffer); } + if (AlMuffledBuffer != 0) { bufferPool.Add(AlMuffledBuffer); } + AlBuffer = 0; + AlMuffledBuffer = 0; + } + + public bool RequestAlBuffers() + { + if (AlBuffer != 0) { return false; } + int alError = 0; + while (bufferPool.Count < 2 && BuffersGenerated < MaxBuffers) + { + Al.GenBuffer(out uint newBuffer); + alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.AddWarning($"Error when generating sound buffer: {Al.GetErrorString(alError)}. {BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated."); + BuffersGenerated = MaxBuffers; + } + else if (!Al.IsBuffer(newBuffer)) + { + DebugConsole.AddWarning($"Error when generating sound buffer: result is not a valid buffer. {BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated."); + BuffersGenerated = MaxBuffers; + } + else + { + bufferPool.Add(newBuffer); + BuffersGenerated++; + if (BuffersGenerated >= MaxBuffers) + { + DebugConsole.AddWarning($"{BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated."); + } + } + } + + if (bufferPool.Count >= 2) + { + AlBuffer = bufferPool.First(); + bufferPool.Remove(AlBuffer); + AlMuffledBuffer = bufferPool.First(); + bufferPool.Remove(AlMuffledBuffer); + return true; + } + + //can't generate any more OpenAL buffers! we'll have to steal a buffer from someone... + foreach (var otherSound in sound.Owner.LoadedSounds) + { + if (otherSound == sound) { continue; } + if (otherSound.IsPlaying()) { continue; } + if (otherSound.Buffers == null) { continue; } + if (otherSound.Buffers.AlBuffer == 0) { continue; } + AlBuffer = otherSound.Buffers.AlBuffer; + AlMuffledBuffer = otherSound.Buffers.AlMuffledBuffer; + otherSound.Buffers.AlBuffer = 0; + otherSound.Buffers.AlMuffledBuffer = 0; + return true; + } + + return false; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs new file mode 100644 index 000000000..ed5a63cac --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -0,0 +1,324 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma +{ + public class SpriteRecorder : ISpriteBatch, IDisposable + { + private struct Command + { + public readonly Texture2D Texture; + public readonly VertexPositionColorTexture VertexBL; + public readonly VertexPositionColorTexture VertexBR; + public readonly VertexPositionColorTexture VertexTL; + public readonly VertexPositionColorTexture VertexTR; + public readonly float Depth; + public readonly Vector2 Min; + public readonly Vector2 Max; + public readonly int Index; + + public bool Overlaps(Command other) + { + return + Min.X <= other.Max.X && Max.X >= other.Min.X && + Min.Y <= other.Max.Y && Max.Y >= other.Min.Y; + } + + public Command( + Texture2D texture, + Vector2 pos, + Rectangle srcRect, + Color color, + float rotation, + Vector2 origin, + Vector2 scale, + SpriteEffects effects, + float depth, + int index) + { + int srcRectLeft = srcRect.Left; + int srcRectRight = srcRect.Right; + int srcRectTop = srcRect.Top; + int srcRectBottom = srcRect.Bottom; + if (effects.HasFlag(SpriteEffects.FlipHorizontally)) + { + var temp = srcRectRight; + srcRectRight = srcRectLeft; + srcRectLeft = temp; + } + if (effects.HasFlag(SpriteEffects.FlipVertically)) + { + var temp = srcRectBottom; + srcRectBottom = srcRectTop; + srcRectTop = temp; + } + + rotation = MathHelper.ToRadians(rotation); + float sin = (float)Math.Sin(rotation); + float cos = (float)Math.Cos(rotation); + + var size = srcRect.Size.ToVector2() * scale; + + Vector2 wAdd = new Vector2(size.X * cos, size.X * sin); + Vector2 hAdd = new Vector2(-size.Y * sin, size.Y * cos); + pos.X -= origin.X * scale.X * cos - origin.Y * scale.Y * sin; + pos.Y -= origin.Y * scale.Y * cos + origin.X * scale.X * sin; + + Texture = texture; + + Depth = depth; + + VertexTL.Color = color; + VertexTR.Color = color; + VertexBL.Color = color; + VertexBR.Color = color; + + VertexTL.Position = new Vector3(pos.X, pos.Y, 0f); + VertexTR.Position = new Vector3(pos.X + wAdd.X, pos.Y + wAdd.Y, 0f); + VertexBL.Position = new Vector3(pos.X + hAdd.X, pos.Y + hAdd.Y, 0f); + VertexBR.Position = new Vector3(pos.X + wAdd.X + hAdd.X, pos.Y + wAdd.Y + hAdd.Y, 0f); + + Min = new Vector2( + MathUtils.Min + ( + VertexTL.Position.X, + VertexTR.Position.X, + VertexBL.Position.X, + VertexBR.Position.X + ), + MathUtils.Min + ( + VertexTL.Position.Y, + VertexTR.Position.Y, + VertexBL.Position.Y, + VertexBR.Position.Y + )); + + Max = new Vector2( + MathUtils.Max + ( + VertexTL.Position.X, + VertexTR.Position.X, + VertexBL.Position.X, + VertexBR.Position.X + ), + MathUtils.Max + ( + VertexTL.Position.Y, + VertexTR.Position.Y, + VertexBL.Position.Y, + VertexBR.Position.Y + )); + + VertexTL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectTop / (float)texture.Height); + VertexTR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectTop / (float)texture.Height); + VertexBL.TextureCoordinate = new Vector2((float)srcRectLeft / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); + VertexBR.TextureCoordinate = new Vector2((float)srcRectRight / (float)texture.Width, (float)srcRectBottom / (float)texture.Height); + + Index = index; + + } + } + + private struct RecordedBuffer + { + public readonly Texture2D Texture; + public readonly VertexBuffer VertexBuffer; + public readonly int PolyCount; + + public RecordedBuffer(List commandList, int startIndex, int count) + { + Texture = commandList[startIndex].Texture; + + VertexBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, count * 4, BufferUsage.WriteOnly); + VertexPositionColorTexture[] vertices = new VertexPositionColorTexture[count * 4]; + for (int i = 0; i < count; i++) + { + vertices[(i * 4) + 0] = commandList[startIndex + i].VertexBL; + vertices[(i * 4) + 1] = commandList[startIndex + i].VertexBR; + vertices[(i * 4) + 2] = commandList[startIndex + i].VertexTL; + vertices[(i * 4) + 3] = commandList[startIndex + i].VertexTR; + } + VertexBuffer.SetData(vertices); + + PolyCount = count * 2; + } + } + + public static BasicEffect BasicEffect = null; + + private List recordedBuffers = new List(); + private List commandList = new List(); + private SpriteSortMode currentSortMode; + + private IndexBuffer indexBuffer = null; + private int maxSpriteCount = 0; + + public volatile bool ReadyToRender = false; + private volatile bool isDisposed = false; + + public Vector2 Min { get; private set; } + public Vector2 Max { get; private set; } + + public void Begin(SpriteSortMode sortMode) + { + ReadyToRender = false; + currentSortMode = sortMode; + } + + public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + { + if (isDisposed) { return; } + + Command command = new Command(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList?.Count ?? 0); + if (commandList.Count == 0) { Min = command.Min; Max = command.Max; } + Min = new Vector2(Math.Min(command.Min.X, Min.X), Math.Min(command.Min.Y, Min.Y)); + Max = new Vector2(Math.Max(command.Max.X, Max.X), Math.Max(command.Max.Y, Max.Y)); + + commandList?.Add(command); + } + + public void End() + { + if (isDisposed) { return; } + //sort commands according to the sorting + //mode given in the last Begin call + switch (currentSortMode) + { + case SpriteSortMode.FrontToBack: + commandList.Sort((c1, c2) => + { + return c1.Depth < c2.Depth ? -1 + : c1.Depth > c2.Depth ? 1 + : c1.Index < c2.Index ? 1 + : c1.Index > c2.Index ? -1 + : 0; + }); + break; + case SpriteSortMode.BackToFront: + commandList.Sort((c1, c2) => + { + return c1.Depth < c2.Depth ? 1 + : c1.Depth > c2.Depth ? -1 + : c1.Index < c2.Index ? 1 + : c1.Index > c2.Index ? -1 + : 0; + }); + break; + } + + //try to place commands of the same texture + //contiguously for optimal buffer generation + //while maintaining the same visual result + for (int i=1;i= 0; j--) + { + if (commandList[j].Texture == commandList[i].Texture) + { + //no commands between i and j overlap with + //i, therefore we can safely sift i down to + //make a contiguous block + commandList.SiftElement(i, j + 1); + break; + } + else if (commandList[j].Overlaps(commandList[i])) + { + //an overlapping command was found, therefore + //attempting to sift this one down would change + //the visual result + break; + } + } + } + } + + if (isDisposed) { return; } + //each contiguous block of commands of the same texture + //requires a vertex buffer to be rendered + CrossThread.RequestExecutionOnMainThread(() => + { + if (commandList.Count == 0) { return; } + int startIndex = 0; + for (int i = 1; i < commandList.Count; i++) + { + if (commandList[i].Texture != commandList[startIndex].Texture) + { + maxSpriteCount = Math.Max(maxSpriteCount, i - startIndex); + recordedBuffers.Add(new RecordedBuffer(commandList, startIndex, i - startIndex)); + startIndex = i; + } + } + recordedBuffers.Add(new RecordedBuffer(commandList, startIndex, commandList.Count - startIndex)); + maxSpriteCount = Math.Max(maxSpriteCount, commandList.Count - startIndex); + }); + + commandList.Clear(); + + ReadyToRender = true; + } + + public void Render(Camera cam) + { + if (!ReadyToRender) { return; } + var gfxDevice = GameMain.Instance.GraphicsDevice; + + BasicEffect ??= new BasicEffect(gfxDevice); + BasicEffect.Projection = Matrix.CreateOrthographicOffCenter(new Rectangle(0, 0, cam.Resolution.X, cam.Resolution.Y), -1f, 1f); + BasicEffect.View = cam.Transform; + BasicEffect.World = Matrix.Identity; + BasicEffect.TextureEnabled = true; + BasicEffect.VertexColorEnabled = true; + BasicEffect.Alpha = 1f; + + int requiredIndexCount = maxSpriteCount * 6; + if (requiredIndexCount > 0 && (indexBuffer == null || indexBuffer.IndexCount < requiredIndexCount)) + { + indexBuffer?.Dispose(); + indexBuffer = new IndexBuffer(gfxDevice, IndexElementSize.SixteenBits, requiredIndexCount * 2, BufferUsage.WriteOnly); + ushort[] indices = new ushort[requiredIndexCount * 2]; + for (int i=0;i spawnedItems = new List(); + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + base.ServerWriteInitial(msg, c); + msg.Write((ushort)spawnedItems.Count); + foreach (Item item in spawnedItems) + { + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs new file mode 100644 index 000000000..1899c5c19 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -0,0 +1,67 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class UnlockPathAction : EventAction + { + public UnlockPathAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + GameMain.GameSession?.Map?.CurrentLocation?.Connections.ForEach(c => c.Locked = false); + if (GameMain.GameSession?.Map?.CurrentLocation?.Connections != null) + { + foreach (LocationConnection connection in GameMain.GameSession?.Map?.CurrentLocation?.Connections) + { + if (!connection.Locked) { continue; } +#if SERVER + NotifyUnlock(connection); +#else + connection.Locked = false; + new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"), + new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); +#endif + } + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(UnlockPathAction)}"; + } + +#if SERVER + private void NotifyUnlock(LocationConnection connection) + { + foreach (Client client in GameMain.Server.ConnectedClients) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.Write((byte)ServerPacketHeader.EVENTACTION); + outmsg.Write((byte)EventManager.NetworkEventType.UNLOCKPATH); + outmsg.Write((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection)); + GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs new file mode 100644 index 000000000..16848864f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs @@ -0,0 +1,154 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class OutpostDestroyMission : AbandonedOutpostMission + { + private readonly string itemTag; + private readonly XElement itemConfig; + private readonly List items = new List(); + + public override IEnumerable SonarPositions + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else + { + return Targets.Select(t => t.WorldPosition); + } + } + } + + private IEnumerable Targets + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else + { + if (items.Any()) + { + return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue); + } + else + { + return requireKill.Concat(requireRescue); + } + } + } + } + + public OutpostDestroyMission(MissionPrefab prefab, Location[] locations) : + base(prefab, locations) + { + itemConfig = prefab.ConfigElement.Element("Items"); + itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); + } + + protected override void StartMissionSpecific(Level level) + { + items.Clear(); +#if SERVER + spawnedItems.Clear(); +#endif + if (!string.IsNullOrEmpty(itemTag)) + { + var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); + if (!itemsToDestroy.Any()) + { + DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\"."); + } + else + { + items.AddRange(itemsToDestroy); + } + } + if (itemConfig != null && !IsClient) + { + foreach (XElement element in itemConfig.Elements()) + { + string itemIdentifier = element.GetAttributeString("identifier", ""); + if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + { + DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found"); + continue; + } + + string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); + string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( + SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, + moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); + if (spawnPoint == null) + { + var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub; + spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + } + Vector2 spawnPos = spawnPoint.WorldPosition; + if (spawnPoint is WayPoint wp && wp.CurrentHull != null) + { + spawnPos = new Vector2( + MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X, wp.CurrentHull.WorldRect.Right), + wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 10.0f); + } + var item = new Item(itemPrefab, spawnPos, null); + items.Add(item); +#if SERVER + spawnedItems.Add(item); +#endif + } + } + + base.StartMissionSpecific(level); + } + + public override void Update(float deltaTime) + { + switch (state) + { + case 0: + if (items.Any()) + { + if (items.All(it => it.Removed || it.Condition <= 0.0f) && + requireKill.All(c => c.Removed || c.IsDead) && + requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) + { + State = 1; + } + } + else + { + if (requireKill.All(c => c.Removed || c.IsDead) && + requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) + { + State = 1; + } + } + break; +#if SERVER + case 1: + if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null) + { + if (!Submarine.MainSub.AtStartPosition || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost))) + { + GameMain.Server.EndGame(); + State = 2; + } + } + break; +#endif + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml new file mode 100644 index 000000000..17f6e0bd9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file