diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index fd9e93f7e..f674cc079 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -17,7 +17,7 @@ namespace Barotrauma fadeOutRoutine = CoroutineManager.StartCoroutine(FadeOutColors(Config.DeadEntityColorFadeOutTime)); } - private IEnumerable FadeOutColors(float time) + private IEnumerable FadeOutColors(float time) { float timer = 0; while (timer < time) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 37f6c91ed..d8d022397 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -36,7 +36,7 @@ namespace Barotrauma CharacterStateInfo serverPos = character.MemState.Last(); if (!character.isSynced) { - SetPosition(serverPos.Position, false); + SetPosition(serverPos.Position, lerp: false); Collider.LinearVelocity = Vector2.Zero; character.MemLocalState.Clear(); character.LastNetworkUpdateID = serverPos.ID; @@ -159,7 +159,7 @@ namespace Barotrauma if (!character.isSynced) { - SetPosition(serverPos.Position, false); + SetPosition(serverPos.Position, lerp: false); Collider.LinearVelocity = Vector2.Zero; character.MemLocalState.Clear(); character.LastNetworkUpdateID = serverPos.ID; @@ -581,10 +581,10 @@ namespace Barotrauma if (this is HumanoidAnimController humanoid) { Vector2 pos = ConvertUnits.ToDisplayUnits(humanoid.RightHandIKPos); - if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.Position; } + if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); pos = ConvertUnits.ToDisplayUnits(humanoid.LeftHandIKPos); - if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.Position; } + if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); Vector2 aimPos = humanoid.AimSourceWorldPos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index fb1cd7d6b..f1dd4470c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -447,28 +447,7 @@ namespace Barotrauma if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); } - if (GameMain.NetworkMember.RespawnManager?.UseRespawnPrompt ?? false) - { - CoroutineManager.Invoke(() => - { - if (controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } - var respawnPrompt = new GUIMessageBox( - TextManager.Get("tutorial.tryagainheader"), TextManager.Get("respawnquestionprompt"), - new string[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }); - respawnPrompt.Buttons[0].OnClicked += (btn, userdata) => - { - GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false); - respawnPrompt.Close(); - return true; - }; - respawnPrompt.Buttons[1].OnClicked += (btn, userdata) => - { - GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true); - respawnPrompt.Close(); - return true; - }; - }, delay: 5.0f); - } + GameMain.NetworkMember.RespawnManager?.ShowRespawnPromptIfNeeded(); GameMain.NetworkMember.AddChatMessage(chatMessage, ChatMessageType.Dead); GameMain.LightManager.LosEnabled = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 3c8d3d5a0..26704a982 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -235,7 +235,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine == null || item.Submarine.TeamID != character.TeamID || item.Submarine.Info.IsWreck) { continue; } - if (!item.Repairables.Any(r => item.ConditionPercentage <= r.RepairIconThreshold)) { continue; } + if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; } if (Submarine.VisibleEntities != null && !Submarine.VisibleEntities.Contains(item)) { continue; } Vector2 diff = item.WorldPosition - character.WorldPosition; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 24ca41abe..3d42e6e3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -790,10 +790,7 @@ namespace Barotrauma return false; } }; - //force update twice because the listbox is insanely janky - //TODO: fix all of the UI :) - listBox.ForceUpdate(); - listBox.ForceUpdate(); + listBox.ForceLayoutRecalculation(); foreach (var childLayoutGroup in listBox.Content.GetAllChildren()) { childLayoutGroup.Recalculate(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 1c618e46e..dd4873f66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -371,21 +371,22 @@ namespace Barotrauma if (attackLimbIndex == 255 || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid SetAttack/ExecuteAttack message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); + DebugConsole.ThrowError($"Received invalid {(eventType == 4 ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; Limb targetLimb = null; - if (!(FindEntityByID(targetEntityID) is IDamageable targetEntity)) + IDamageable targetEntity = FindEntityByID(targetEntityID) as IDamageable; + if (targetEntity == null && eventType == 4) { - DebugConsole.ThrowError($"Received invalid SetAttack/ExecuteAttack message. Target entity not found (ID {targetEntityID})"); + DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); break; } if (targetEntity is Character targetCharacter) { if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid SetAttack/ExecuteAttack message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); + DebugConsole.ThrowError($"Received invalid {(eventType == 4 ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); break; } targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 9ee0fa4ba..c2e1b8461 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -805,7 +805,27 @@ namespace Barotrauma { var treatmentButton = component.GetChild(); if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; } - treatmentButton.Enabled = Character.Controlled.Inventory.AllItems.Any(it => it.prefab == itemPrefab); + var matchingItem = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); + treatmentButton.Enabled = matchingItem != null; + if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) + { + //highlight the slot the treatment item is in + var rootContainer = matchingItem.GetRootContainer() ?? matchingItem; + var index = Character.Controlled.Inventory.FindIndex(rootContainer); + if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length && + Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f) + { + Character.Controlled.Inventory.visualSlots[index].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f); + } + } + if (matchingItem != null && !string.IsNullOrEmpty(treatmentButton.ToolTip)) { continue; } + treatmentButton.ToolTip = $"‖color:255,255,255,255‖{itemPrefab.Name}‖color:end‖" + '\n' + itemPrefab.Description; + if (treatmentButton.Enabled) + { + treatmentButton.ToolTip = + $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] {TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + + treatmentButton.RawToolTip; + } foreach (GUIComponent child in treatmentButton.Children) { child.Enabled = treatmentButton.Enabled; @@ -1249,7 +1269,7 @@ namespace Barotrauma foreach (string treatment in treatmentSuitability.Keys.ToList()) { //prefer suggestions for items the player has - if (Character.Controlled.Inventory.FindItemByIdentifier(treatment) != null) + if (Character.Controlled.Inventory.FindItemByIdentifier(treatment, recursive: true) != null) { treatmentSuitability[treatment] *= 10.0f; } @@ -1288,12 +1308,11 @@ namespace Barotrauma var innerFrame = new GUIButton(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "SubtreeHeader") { UserData = item, - ToolTip = $"‖color:255,255,255,255‖{item.Name}‖color:end‖" + '\n' + item.Description, DisabledColor = Color.White * 0.1f, OnClicked = (btn, userdata) => { if (!(userdata is ItemPrefab itemPrefab)) { return false; } - var item = Character.Controlled.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + var item = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); @@ -1445,7 +1464,7 @@ namespace Barotrauma var potentialTreatment = Inventory.DraggingItems.FirstOrDefault(); if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab) { - potentialTreatment = Character.Controlled.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); } potentialTreatment ??= Inventory.SelectedSlot?.Item; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index 248bbe71d..6e041eeac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -30,7 +30,7 @@ namespace Barotrauma foreach (SkillPrefab skill in Skills) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform), - " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), (int)skill.LevelRange.X + " - " + (int)skill.LevelRange.Y), + " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), (int)skill.LevelRange.Start + " - " + (int)skill.LevelRange.End), font: GUI.SmallFont); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 2e544f692..8f5f6b791 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -730,8 +730,6 @@ namespace Barotrauma } } - body.Dir = Dir; - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); bool hideLimb = Hide || diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 0bb2d3ce3..7c6c7965d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -138,7 +138,7 @@ namespace Barotrauma var newMsg = queuedMessages.Dequeue(); AddMessage(newMsg); - if (GameSettings.SaveDebugConsoleLogs) + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { unsavedMessages.Add(newMsg); if (unsavedMessages.Count >= messagesPerFile) @@ -274,7 +274,10 @@ namespace Barotrauma AddMessage(newMsg); } - if (GameSettings.SaveDebugConsoleLogs) unsavedMessages.Add(newMsg); + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + { + unsavedMessages.Add(newMsg); + } } } } @@ -537,7 +540,21 @@ namespace Barotrauma return; } - GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName); + float difficulty = 40; + if (args.Length > 1) + { + float.TryParse(args[1], out difficulty); + } + + LevelGenerationParams levelGenerationParams = null; + if (args.Length > 2) + { + string levelGenerationIdentifier = args[2]; + levelGenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(p => p.Identifier == levelGenerationIdentifier); + } + + GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName, difficulty, levelGenerationParams); + }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().ToArray() })); commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => @@ -734,13 +751,10 @@ namespace Barotrauma AssignOnExecute("teleportcharacter|teleport", (string[] args) => { Character tpCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false); - if (tpCharacter == null) return; - - var cam = GameMain.GameScreen.Cam; - tpCharacter.AnimController.CurrentHull = null; - tpCharacter.Submarine = null; - tpCharacter.AnimController.SetPosition(ConvertUnits.ToSimUnits(cam.ScreenToWorld(PlayerInput.MousePosition))); - tpCharacter.AnimController.FindHull(cam.ScreenToWorld(PlayerInput.MousePosition), true); + if (tpCharacter != null) + { + tpCharacter.TeleportTo(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); + } }); AssignOnExecute("spawn|spawncharacter", (string[] args) => @@ -1413,7 +1427,7 @@ namespace Barotrauma commands.Add(new Command("analyzeitem", "analyzeitem: Analyzes one item for exploits.", (string[] args) => { - if (args.Length < 1) return; + if (args.Length < 1) { return; } List fabricableItems = new List(); foreach (ItemPrefab iPrefab in ItemPrefab.Prefabs) @@ -1792,7 +1806,7 @@ namespace Barotrauma foreach (var talentTree in TalentTree.JobTalentTrees) { - foreach (var talentSubTree in talentTree.Value.TalentSubTrees) + foreach (var talentSubTree in talentTree.TalentSubTrees) { string nameIdentifier = "talenttree." + talentSubTree.Identifier; if (!tags[language].Contains(nameIdentifier)) @@ -1857,7 +1871,21 @@ namespace Barotrauma commands.Add(new Command("eventstats", "", (string[] args) => { - var debugLines = EventSet.GetDebugStatistics(); + List debugLines; + if (args.Length > 0) + { + if (!Enum.TryParse(args[0], ignoreCase: true, out Level.PositionType spawnType)) + { + var enums = Enum.GetNames(typeof(Level.PositionType)); + ThrowError($"\"{args[0]}\" is not a valid Level.PositionType. Available options are: {string.Join(", ", enums)}"); + return; + } + debugLines = EventSet.GetDebugStatistics(filter: monsterEvent => monsterEvent.SpawnPosType.HasFlag(spawnType)); + } + else + { + debugLines = EventSet.GetDebugStatistics(); + } string filePath = "eventstats.txt"; Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, debugLines); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 4d456d911..238445418 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -14,7 +14,8 @@ namespace Barotrauma { private Graph intensityGraph; private Graph targetIntensityGraph; - private float intensityGraphUpdateInterval; + private Graph monsterStrengthGraph; + private const float intensityGraphUpdateInterval = 10; private float lastIntensityUpdate; private Vector2 pinnedPosition = new Vector2(256, 128); @@ -22,6 +23,8 @@ namespace Barotrauma public Event? PinnedEvent { get; set; } + private bool isGraphSelected; + public void DebugDraw(SpriteBatch spriteBatch) { foreach (Event ev in activeEvents) @@ -42,17 +45,25 @@ namespace Barotrauma DrawEventTargetTags(spriteBatch, scriptedEvent); } + float theoreticalMaxMonsterStrength = 10000; + float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * GameMain.GameSession.LevelData.Difficulty / 100; + float absoluteMonsterStrength = monsterStrength / theoreticalMaxMonsterStrength; + float relativeMonsterStrength = monsterStrength / relativeMaxMonsterStrength; GUI.DrawString(spriteBatch, new Vector2(10, y), "EventManager", Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); GUI.DrawString(spriteBatch, new Vector2(15, y + 20), "Event cooldown: " + (int)Math.Max(eventCoolDown, 0), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); GUI.DrawString(spriteBatch, new Vector2(15, y + 35), "Current intensity: " + (int)Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, currentIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); GUI.DrawString(spriteBatch, new Vector2(15, y + 50), "Target intensity: " + (int)Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, targetIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "AvgHealth: " + (int)Math.Round(avgCrewHealth * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "AvgHullIntegrity: " + (int)Math.Round(avgHullIntegrity * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "FloodingAmount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, floodingAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "FireAmount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, fireAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "EnemyDanger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 140), "MonsterTotalStrength: " + (int)Math.Round(monsterTotalStrength), Color.Lerp(GUI.Style.Green, GUI.Style.Red, monsterTotalStrength / 5000f), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "Crew health: " + (int)Math.Round(avgCrewHealth * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "Hull integrity: " + (int)Math.Round(avgHullIntegrity * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "Flooding amount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, floodingAmount), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "Fire amount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, fireAmount), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "Enemy danger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 140), "Current monster strength (total): " + (int)Math.Round(monsterStrength), Color.Lerp(GUI.Style.Green, GUI.Style.Red, relativeMonsterStrength), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 155), "Main events: " + (int)Math.Round(CumulativeMonsterStrengthMain), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 170), "Ruin events: " + (int)Math.Round(CumulativeMonsterStrengthRuins), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 185), "Wreck events: " + (int)Math.Round(CumulativeMonsterStrengthWrecks), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 200), "Cave events: " + (int)Math.Round(CumulativeMonsterStrengthCaves), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); #if DEBUG if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) && @@ -64,29 +75,103 @@ namespace Barotrauma if (intensityGraph == null) { - intensityGraph = new Graph(); - targetIntensityGraph = new Graph(); + int graphDensity = 360; // 60 min + intensityGraph = new Graph(graphDensity); + targetIntensityGraph = new Graph(graphDensity); + monsterStrengthGraph = new Graph(graphDensity); } - intensityGraphUpdateInterval = 5.0f; if (Timing.TotalTime > lastIntensityUpdate + intensityGraphUpdateInterval) { intensityGraph.Update(currentIntensity); targetIntensityGraph.Update(targetIntensity); - lastIntensityUpdate = (float) Timing.TotalTime; + monsterStrengthGraph.Update(relativeMonsterStrength); + lastIntensityUpdate = (float)Timing.TotalTime; } - Rectangle graphRect = new Rectangle(15, y + 165, 150, 50); + Rectangle graphRect = new Rectangle(15, y + 240, (int)(200 * GUI.xScale), (int)(100 * GUI.yScale)); + bool isGraphHovered = graphRect.Contains(PlayerInput.MousePosition); + bool leftMousePressed = PlayerInput.PrimaryMouseButtonDown() || PlayerInput.PrimaryMouseButtonHeld(); + bool rightMousePressed = PlayerInput.SecondaryMouseButtonHeld() || PlayerInput.SecondaryMouseButtonDown(); + if (!isGraphSelected && isGraphHovered && leftMousePressed) + { + isGraphSelected = true; + } + if (isGraphSelected && rightMousePressed) + { + isGraphSelected = false; + } + Color intensityColor = Color.Lerp(Color.White, GUI.Style.Red, currentIntensity); + if (isGraphHovered || isGraphSelected) + { + graphRect.Size = new Point(GameMain.GraphicsWidth - 30, (int)(GameMain.GraphicsHeight * 0.35f)); + intensityColor = Color.Red; + GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.95f, isFilled: true); + } + else + { + GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.6f, isFilled: true); + } + intensityGraph.Draw(spriteBatch, graphRect, maxValue: 1.0f, xOffset: 0, intensityColor, (sBatch, value, order, pos) => + { + if (isGraphHovered || isGraphSelected) + { + Vector2 bottomPoint = new Vector2(pos.X, graphRect.Bottom); + float height = 3 * GUI.yScale; + if (order % 6 == 0) + { + height *= 3; + string text = (order / 6).ToString(); + var font = GUI.SmallFont; + Vector2 textSize = font.MeasureString(text); + Vector2 textPos = new Vector2(bottomPoint.X - textSize.X / 2, bottomPoint.Y + height * 1.5f); + GUI.DrawString(sBatch, textPos, text, Color.White, font: font); + } + GUI.DrawLine(sBatch, bottomPoint, bottomPoint + Vector2.UnitY * height, Color.White, width: Math.Max(GUI.Scale, 1)); + DrawTimeStamps(sBatch, Color.Red, pos, order); + } + }); + targetIntensityGraph.Draw(spriteBatch, graphRect, maxValue: 1.0f, xOffset: 0, intensityColor * 0.5f); + if (isGraphHovered || isGraphSelected) + { + float? maxValue = 1; + Color color = Color.White; + if (relativeMonsterStrength > 1) + { + maxValue = null; + color = Color.Yellow; + } + monsterStrengthGraph.Draw(spriteBatch, graphRect, maxValue, color: color, doForEachValue: (sBatch, value, order, pos) => DrawTimeStamps(sBatch, color, pos, order)); + } - GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.5f, true); - intensityGraph.Draw(spriteBatch, graphRect, 1.0f, 0.0f, Color.Lerp(Color.White, GUI.Style.Red, currentIntensity)); - targetIntensityGraph.Draw(spriteBatch, graphRect, 1.0f, 0.0f, Color.Lerp(Color.White, GUI.Style.Red, targetIntensity) * 0.5f); + void DrawTimeStamps(SpriteBatch sBatch, Color color, Vector2 pos, int order) + { + if (isGraphHovered || isGraphSelected) + { + foreach (var timeStamp in timeStamps) + { + int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval)); + if (t == order) + { + float size = 6; + Vector2 p = new Vector2(pos.X - size / 2, pos.Y - size / 2); + ShapeExtensions.DrawPoint(sBatch, p, color, size); + break; + } + } + } + } GUI.DrawLine(spriteBatch, new Vector2(graphRect.Right, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)), - new Vector2(graphRect.Right + 5, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)), Color.Orange, 0, 1); + new Vector2(graphRect.Right + 5, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)), Color.Orange, width: 3); - y = graphRect.Bottom + 20; + int yStep = (int)(20 * GUI.yScale); + y = graphRect.Bottom + yStep; + if (isGraphHovered || isGraphSelected) + { + y += yStep; + } int x = graphRect.X; if (isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) { @@ -143,7 +228,7 @@ namespace Barotrauma if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity) { GUI.DrawString(spriteBatch, new Vector2(x, y), - " intensity between " + ((int) eventSet.MinIntensity) + " and " + ((int) eventSet.MaxIntensity), + " intensity between " + eventSet.MinIntensity.FormatDoubleDecimal() + " and " + eventSet.MaxIntensity.FormatDoubleDecimal(), Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; } @@ -159,13 +244,13 @@ namespace Barotrauma if (y > GameMain.GraphicsHeight * 0.9f) { - y = graphRect.Bottom + 35; - x += 250; + y = graphRect.Bottom + yStep * 2; + x += 300; } } GUI.DrawString(spriteBatch, new Vector2(x, y), "Current events: ", Color.White * 0.9f, null, 0, GUI.SmallFont); - y += 15; + y += yStep; foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) { @@ -182,17 +267,15 @@ namespace Barotrauma { GUI.MouseCursor = CursorState.Hand; GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); - if (ev != PinnedEvent) { DrawEvent(spriteBatch, ev, rect); } - else if (PlayerInput.SecondaryMouseButtonHeld() || PlayerInput.SecondaryMouseButtonDown()) + else if (rightMousePressed) { PinnedEvent = null; } - - if (PlayerInput.PrimaryMouseButtonHeld() || PlayerInput.PrimaryMouseButtonDown()) + if (leftMousePressed) { PinnedEvent = ev; } @@ -201,8 +284,8 @@ namespace Barotrauma y += 18; if (y > GameMain.GraphicsHeight * 0.9f) { - y = graphRect.Bottom + 35; - x += 250; + y = graphRect.Bottom + yStep * 2; + x += 300; } } } @@ -352,9 +435,11 @@ namespace Barotrauma return DrawInfoRectangle(spriteBatch, scriptedEvent, text, parentRect, positions); } + private readonly List debugPositions = new List(); + private Rectangle DrawArtifactEvent(SpriteBatch spriteBatch, ArtifactEvent artifactEvent, Rectangle? parentRect = null) { - List positions = new List(); + debugPositions.Clear(); string text = $"Finished: {artifactEvent.IsFinished.ColorizeObject()}\n" + $"Item: {artifactEvent.Item.ColorizeObject()}\n" + @@ -364,15 +449,15 @@ namespace Barotrauma if (artifactEvent.Item != null && !artifactEvent.Item.Removed) { Vector2 pos = artifactEvent.Item.WorldPosition; - positions.Add(new DebugLine(pos, Color.White)); + debugPositions.Add(new DebugLine(pos, Color.White)); } - return DrawInfoRectangle(spriteBatch, artifactEvent, text, parentRect, positions); + return DrawInfoRectangle(spriteBatch, artifactEvent, text, parentRect, debugPositions); } private Rectangle DrawMonsterEvent(SpriteBatch spriteBatch, MonsterEvent monsterEvent, Rectangle? parentRect = null) { - List positions = new List(); + debugPositions.Clear(); string text = $"Finished: {monsterEvent.IsFinished.ColorizeObject()}\n" + $"Amount: {monsterEvent.MinAmount.ColorizeObject()} - {monsterEvent.MaxAmount.ColorizeObject()}\n" + @@ -383,7 +468,7 @@ namespace Barotrauma { Vector2 pos = monsterEvent.SpawnPos.Value; text += $"Distance from submarine: {Vector2.Distance(pos, Submarine.MainSub.WorldPosition).ColorizeObject()}\n"; - positions.Add(new DebugLine(pos, Color.White)); + debugPositions.Add(new DebugLine(pos, Color.White)); } if (monsterEvent.Monsters != null) @@ -394,11 +479,10 @@ namespace Barotrauma { text += $" {monster.ColorizeObject()} -> (Dead: {monster.IsDead.ColorizeObject()}, Health: {monster.HealthPercentage.ColorizeObject()}%, AIState: {(monster.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle ).ColorizeObject()})\n"; if (monster.Removed) { continue; } - positions.Add(new DebugLine(monster.WorldPosition, Color.Red)); + debugPositions.Add(new DebugLine(monster.WorldPosition, Color.Red)); } } - - return DrawInfoRectangle(spriteBatch, monsterEvent, text, parentRect, positions); + return DrawInfoRectangle(spriteBatch, monsterEvent, text, parentRect, debugPositions); } private Rectangle DrawInfoRectangle(SpriteBatch spriteBatch, Event @event, string text, Rectangle? parentRect = null, List? drawPoints = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index e8b3064d4..0dc6284b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -78,7 +78,7 @@ namespace Barotrauma CoroutineManager.StartCoroutine(ShowMessageBoxAfterRoundSummary(header, message)); } - private IEnumerable ShowMessageBoxAfterRoundSummary(string header, string message) + private IEnumerable ShowMessageBoxAfterRoundSummary(string header, string message) { while (GUIMessageBox.VisibleBox?.UserData is RoundSummary) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 2c6ab92ac..aff1e4c05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -51,16 +51,30 @@ namespace Barotrauma } } + public float LineHeight => baseHeight * 1.8f; + private uint[] charRanges; private int texDims; private uint baseChar; - private struct GlyphData + private readonly struct GlyphData { - public int texIndex; - public Vector2 drawOffset; - public float advance; - public Rectangle texCoords; + public readonly int TexIndex; + public readonly Vector2 DrawOffset; + public readonly float Advance; + public readonly Rectangle TexCoords; + + public GlyphData( + int texIndex = default, + Vector2 drawOffset = default, + float advance = default, + Rectangle texCoords = default) + { + TexIndex = texIndex; + DrawOffset = drawOffset; + Advance = advance; + TexCoords = texCoords; + } } public ScalableFont(XElement element, GraphicsDevice gd = null) @@ -167,9 +181,10 @@ namespace Barotrauma if (face.Glyph.Metrics.HorizontalAdvance > 0) { //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData(); - blankData.advance = (float)face.Glyph.Metrics.HorizontalAdvance; - blankData.texIndex = -1; //indicates no texture because the glyph is empty + GlyphData blankData = new GlyphData( + advance: (float)face.Glyph.Metrics.HorizontalAdvance, + texIndex: -1); //indicates no texture because the glyph is empty + texCoords.Add(j, blankData); } continue; @@ -211,13 +226,12 @@ namespace Barotrauma } } - GlyphData newData = new GlyphData - { - advance = (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex = texIndex, - texCoords = new Rectangle((int)currentCoords.X, (int)currentCoords.Y, glyphWidth, glyphHeight), - drawOffset = new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop) - }; + GlyphData newData = new GlyphData( + advance: (float)face.Glyph.Metrics.HorizontalAdvance, + texIndex: texIndex, + texCoords: new Rectangle((int)currentCoords.X, (int)currentCoords.Y, glyphWidth, glyphHeight), + drawOffset: new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop) + ); texCoords.Add(j, newData); for (int y = 0; y < glyphHeight; y++) @@ -278,9 +292,9 @@ namespace Barotrauma if (face.Glyph.Metrics.HorizontalAdvance > 0) { //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData(); - blankData.advance = (float)face.Glyph.Metrics.HorizontalAdvance; - blankData.texIndex = -1; //indicates no texture because the glyph is empty + GlyphData blankData = new GlyphData( + advance: (float)face.Glyph.Metrics.HorizontalAdvance, + texIndex: -1); //indicates no texture because the glyph is empty texCoords.Add(character, blankData); } return; @@ -316,19 +330,18 @@ namespace Barotrauma currentDynamicPixelBuffer = null; } - GlyphData newData = new GlyphData - { - advance = (float)horizontalAdvance, - texIndex = textures.Count - 1, - texCoords = new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), - drawOffset = drawOffset - }; + GlyphData newData = new GlyphData( + advance: (float)horizontalAdvance, + texIndex: textures.Count - 1, + texCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), + drawOffset: drawOffset + ); texCoords.Add(character, newData); if (currentDynamicPixelBuffer == null) { currentDynamicPixelBuffer = new uint[texDims * texDims]; - textures[newData.texIndex].GetData(currentDynamicPixelBuffer, 0, texDims * texDims); + textures[newData.TexIndex].GetData(currentDynamicPixelBuffer, 0, texDims * texDims); } for (int y = 0; y < glyphHeight; y++) @@ -339,12 +352,25 @@ namespace Barotrauma currentDynamicPixelBuffer[((int)currentDynamicAtlasCoords.X + x) + ((int)currentDynamicAtlasCoords.Y + y) * texDims] = (uint)(byteColor << 24 | 0x00ffffff); } } - textures[newData.texIndex].SetData(currentDynamicPixelBuffer); + textures[newData.TexIndex].SetData(currentDynamicPixelBuffer); currentDynamicAtlasCoords.X += glyphWidth + 2; } } + private GlyphData GetGlyphData(uint charIndex) + { + const uint DEFAULT_INDEX = 0x25A1; //U+25A1 = white square + + if (texCoords.TryGetValue(charIndex, out GlyphData gd) || + texCoords.TryGetValue(DEFAULT_INDEX, out gd)) + { + return gd; + } + + return new GlyphData(texIndex: -1); + } + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) { if (textures.Count == 0 && !DynamicLoading) { return; } @@ -358,8 +384,8 @@ namespace Barotrauma { lineNum++; currentPos = position; - currentPos.X -= baseHeight * 1.8f * lineNum * advanceUnit.Y * scale.Y; - currentPos.Y += baseHeight * 1.8f * lineNum * advanceUnit.X * scale.Y; + currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; + currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; continue; } @@ -369,19 +395,17 @@ namespace Barotrauma DynamicRenderAtlas(graphicsDevice, charIndex); } - if (texCoords.TryGetValue(charIndex, out GlyphData gd) || texCoords.TryGetValue(9633, out gd)) //9633 = white square + GlyphData gd = GetGlyphData(charIndex); + if (gd.TexIndex >= 0) { - if (gd.texIndex >= 0) - { - Texture2D tex = textures[gd.texIndex]; - Vector2 drawOffset; - drawOffset.X = gd.drawOffset.X * advanceUnit.X * scale.X - gd.drawOffset.Y * advanceUnit.Y * scale.Y; - drawOffset.Y = gd.drawOffset.X * advanceUnit.Y * scale.Y + gd.drawOffset.Y * advanceUnit.X * scale.X; + Texture2D tex = textures[gd.TexIndex]; + Vector2 drawOffset; + drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; + drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X; - sb.Draw(tex, currentPos + drawOffset, gd.texCoords, color, rotation, origin, scale, se, layerDepth); - } - currentPos += gd.advance * advanceUnit * scale.X; + sb.Draw(tex, currentPos + drawOffset, gd.TexCoords, color, rotation, origin, scale, se, layerDepth); } + currentPos += gd.Advance * advanceUnit * scale.X; } } @@ -400,7 +424,7 @@ namespace Barotrauma if (text[i] == '\n') { currentPos.X = position.X; - currentPos.Y += baseHeight * 1.8f; + currentPos.Y += LineHeight; continue; } @@ -410,15 +434,13 @@ namespace Barotrauma DynamicRenderAtlas(graphicsDevice, charIndex); } - if (texCoords.TryGetValue(charIndex, out GlyphData gd) || texCoords.TryGetValue(9633, out gd)) //9633 = white square + GlyphData gd = GetGlyphData(charIndex); + if (gd.TexIndex >= 0) { - if (gd.texIndex >= 0) - { - Texture2D tex = textures[gd.texIndex]; - sb.Draw(tex, currentPos + gd.drawOffset, gd.texCoords, color); - } - currentPos.X += gd.advance; + Texture2D tex = textures[gd.TexIndex]; + sb.Draw(tex, currentPos + gd.DrawOffset, gd.TexCoords, color); } + currentPos.X += gd.Advance; } } @@ -444,8 +466,8 @@ namespace Barotrauma { lineNum++; currentPos = position; - currentPos.X -= baseHeight * 1.8f * lineNum * advanceUnit.Y * scale.Y; - currentPos.Y += baseHeight * 1.8f * lineNum * advanceUnit.X * scale.Y; + currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; + currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; continue; } @@ -476,22 +498,116 @@ namespace Barotrauma currentTextColor = color; } - if (texCoords.TryGetValue(charIndex, out GlyphData gd) || texCoords.TryGetValue(9633, out gd)) //9633 = white square + GlyphData gd = GetGlyphData(charIndex); + if (gd.TexIndex >= 0) { - if (gd.texIndex >= 0) - { - Texture2D tex = textures[gd.texIndex]; - Vector2 drawOffset; - drawOffset.X = gd.drawOffset.X * advanceUnit.X * scale.X - gd.drawOffset.Y * advanceUnit.Y * scale.Y; - drawOffset.Y = gd.drawOffset.X * advanceUnit.Y * scale.Y + gd.drawOffset.Y * advanceUnit.X * scale.X; + Texture2D tex = textures[gd.TexIndex]; + Vector2 drawOffset; + drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; + drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X; - sb.Draw(tex, currentPos + drawOffset, gd.texCoords, currentTextColor, rotation, origin, scale, se, layerDepth); - } - currentPos += gd.advance * advanceUnit * scale.X; + sb.Draw(tex, currentPos + drawOffset, gd.TexCoords, currentTextColor, rotation, origin, scale, se, layerDepth); } + currentPos += gd.Advance * advanceUnit * scale.X; } } + public string WrapText(string text, float width) + => WrapText(text, width, requestCharPos: 0, out _, returnAllCharPositions: false, out _); + + public string WrapText(string text, float width, int requestCharPos, out Vector2 requestedCharPos) + => WrapText(text, width, requestCharPos, out requestedCharPos, returnAllCharPositions: false, out _); + + public string WrapText(string text, float width, out Vector2[] allCharPositions) + => WrapText(text, width, requestCharPos: 0, out _, returnAllCharPositions: true, out allCharPositions); + + /// + /// Wraps a string of text to fit within a given width. + /// Optionally returns the caret position of a certain character, + /// or all of them. + /// + private string WrapText(string text, + float width, + int requestCharPos, + out Vector2 requestedCharPos, + bool returnAllCharPositions, + out Vector2[] allCharPositions) + { + int currLineStart = 0; + Vector2 currentPos = Vector2.Zero; + Vector2 foundCharPos = Vector2.Zero; + int? lastBreakerIndex = null; + string result = ""; + var allCharPos = returnAllCharPositions ? new Vector2[text.Length+1] : null; + for (int i = 0; i < text.Length; i++) + { + //Records the caret position of the current character + void recordCurrentPos() + { + if (i == requestCharPos) { foundCharPos = currentPos; } + + if (allCharPos != null) { allCharPos[i] = currentPos; } + } + recordCurrentPos(); + + //Appends a newline to the result and resets the caret position's X value + void nextLine() + { + result += text[currLineStart..i].Remove("\n") + "\n"; + lastBreakerIndex = null; + currentPos.X = 0.0f; + currentPos.Y += LineHeight; + currLineStart = i; + } + + //If a newline is found in the source, split immediately + if (text[i] == '\n') + { + nextLine(); + continue; + } + + //Otherwise, advance based on the width of the current character + GlyphData gd = GetGlyphData(text[i]); + float advance = gd.Advance; + if (currentPos.X + advance >= width) + { + //Advancing based on the last character + //would put us past the max width! + if (i > 0 && char.IsWhiteSpace(text[i]) && !char.IsWhiteSpace(text[i - 1])) + { + //Whitespace immediately after a visible + //character can be shrunk down to fit + advance = width - currentPos.X; + } + else + { + if (lastBreakerIndex.HasValue) + { + //A breaker (whitespace or CJK) was found earlier + //in this line, so let's break the line there + i = lastBreakerIndex.Value + 1; + } + + nextLine(); + recordCurrentPos(); //must re-record current caret position since we are on a new line now + } + } + currentPos.X += advance; + + if (char.IsWhiteSpace(text[i]) || TextManager.IsCJK($"{text[i]}")) + { + lastBreakerIndex = i; + } + } + if (requestCharPos >= text.Length) { foundCharPos = currentPos; } + if (allCharPos != null) { allCharPos[text.Length] = currentPos; } + allCharPositions = allCharPos; + result += text[currLineStart..].Remove("\n"); + requestedCharPos = foundCharPos; + return result; + } + public Vector2 MeasureString(string text, bool removeExtraSpacing = false) { if (text == null) @@ -504,7 +620,7 @@ namespace Barotrauma if (!removeExtraSpacing) { - retVal.Y = baseHeight * 1.8f; + retVal.Y = LineHeight; } else { @@ -516,7 +632,7 @@ namespace Barotrauma if (text[i] == '\n') { currentLineX = 0.0f; - retVal.Y += baseHeight * 1.8f; + retVal.Y += LineHeight; continue; } uint charIndex = text[i]; @@ -524,10 +640,9 @@ namespace Barotrauma { DynamicRenderAtlas(graphicsDevice, charIndex); } - if (texCoords.TryGetValue(charIndex, out GlyphData gd)) - { - currentLineX += gd.advance; - } + + GlyphData gd = GetGlyphData(charIndex); + currentLineX += gd.Advance; retVal.X = Math.Max(retVal.X, currentLineX); } return retVal; @@ -536,15 +651,14 @@ namespace Barotrauma public Vector2 MeasureChar(char c) { Vector2 retVal = Vector2.Zero; - retVal.Y = baseHeight * 1.8f; + retVal.Y = LineHeight; if (DynamicLoading && !texCoords.ContainsKey(c)) { DynamicRenderAtlas(graphicsDevice, c); } - if (texCoords.TryGetValue(c, out GlyphData gd)) - { - retVal.X = gd.advance; - } + + GlyphData gd = GetGlyphData(c); + retVal.X = gd.Advance; return retVal; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index a9b3e4e50..abe80ac1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -494,7 +494,7 @@ namespace Barotrauma GUIFrame.Parent.Visible = visible; } - private IEnumerable UpdateMessageAnimation(GUIComponent message, float animDuration) + private IEnumerable UpdateMessageAnimation(GUIComponent message, float animDuration) { float timer = 0.0f; while (timer < animDuration) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 18c65a9f7..976820f90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -399,7 +399,7 @@ namespace Barotrauma " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", GUI.Style.Green, Color.Black * 0.8f, font: SmallFont); y += 15; - GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), null, 0, GUI.Style.Green); + GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Style.Green); y += 50; DrawString(spriteBatch, new Vector2(300, y), @@ -407,8 +407,8 @@ namespace Barotrauma " Max: " + GameMain.PerformanceCounter.UpdateTimeGraph.LargestValue().ToString("0.00") + " ms", Color.LightBlue, Color.Black * 0.8f, font: SmallFont); y += 15; - GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), null, 0, Color.LightBlue); - GameMain.PerformanceCounter.UpdateIterationsGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), 20, 0, GUI.Style.Red); + GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Color.LightBlue); + GameMain.PerformanceCounter.UpdateIterationsGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), maxValue: 20, color: Style.Red); y += 50; foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) { @@ -941,7 +941,8 @@ namespace Barotrauma inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame); } - if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || (prevMouseOn == null && !PlayerInput.SecondaryMouseButtonHeld())) + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || + (prevMouseOn == null && !PlayerInput.SecondaryMouseButtonHeld() && !Inventory.DraggingItems.Any())) { for (var i = updateList.Count - 1; i > inventoryIndex; i--) { @@ -1052,10 +1053,10 @@ namespace Barotrauma // Children in list boxes can be interacted with despite not having // a GUIButton inside of them so instead of hard coding we check if // the children can be interacted with by checking their hover state - if (parent is GUIListBox listBox) + if (parent is GUIListBox listBox && c.Parent == listBox.Content) { if (listBox.DraggedElement != null) { return CursorState.Dragging; } - if (listBox.CanDragElements) { return CursorState.Move; } + if (listBox.CurrentDragMode != GUIListBox.DragMode.NoDragging) { return CursorState.Move; } if (listBox.HoverCursor != CursorState.Default) { @@ -1148,7 +1149,7 @@ namespace Barotrauma { CoroutineManager.StartCoroutine(WaitCursorCoroutine(), "WaitCursorTimeout"); - IEnumerable WaitCursorCoroutine() + IEnumerable WaitCursorCoroutine() { MouseCursor = CursorState.Waiting; var timeOut = DateTime.Now + new TimeSpan(0, 0, waitSeconds); @@ -1360,7 +1361,7 @@ namespace Barotrauma float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale; - if (overrideAlpha.HasValue || (dist > visibleRange.Start && dist < visibleRange.End)) + if (overrideAlpha.HasValue || visibleRange.Contains(dist)) { float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.Start) / 100.0f, 1.0f - ((dist - visibleRange.End + 100f) / 100.0f), 1.0f); Vector2 targetScreenPos = cam.WorldToScreen(worldPosition); @@ -2254,8 +2255,8 @@ namespace Barotrauma #region Misc public static void TogglePauseMenu() { - if (Screen.Selected == GameMain.MainMenuScreen) return; - if (PreventPauseMenuToggle) return; + if (Screen.Selected == GameMain.MainMenuScreen) { return; } + if (PreventPauseMenuToggle) { return; } settingsMenuOpen = false; @@ -2276,162 +2277,121 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.05f }; - - new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) }, + + new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) }, "", style: "GUIBugButton") { IgnoreLayoutGroups = true, - ToolTip = TextManager.Get("bugreportbutton"), + ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})", OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; } }; - new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuResume")) - { - OnClicked = TogglePauseMenu - }; - - new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSettings")) - { - OnClicked = (btn, userData) => - { - TogglePauseMenu(); - settingsMenuOpen = !settingsMenuOpen; - return true; - } - }; + CreateButton("PauseMenuResume", buttonContainer, null); + CreateButton("PauseMenuSettings", buttonContainer, () => { settingsMenuOpen = !settingsMenuOpen; }); bool IsOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedOutpost; if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null) { if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode) { - var retryButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuRetry")); - retryButton.OnClicked += (btn, userData) => + CreateButton("PauseMenuRetry", buttonContainer, verificationTextTag: "PauseMenuRetryVerification", action: () => { - var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuRetryVerification"), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + if (GameMain.GameSession.RoundSummary?.Frame != null) { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (_, userdata) => - { - if (GameMain.GameSession.RoundSummary?.Frame != null) - { - GUIMessageBox.MessageBoxes.Remove(GameMain.GameSession.RoundSummary.Frame); - } + GUIMessageBox.MessageBoxes.Remove(GameMain.GameSession.RoundSummary.Frame); + } + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction"); + GameMain.GameSession.LoadPreviousSave(); + }); - GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction"); - TogglePauseMenu(btn, userData); - GameMain.GameSession.LoadPreviousSave(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = (_, userdata) => - { - TogglePauseMenu(btn, userData); - msgBox.Close(); - return true; - }; - return true; - }; if (IsOutpostLevel()) { - var saveAndQuitButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSaveQuit")) + CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () => { - UserData = "save", - OnClicked = (btn, userData) => - { - pauseMenuOpen = false; - if (IsOutpostLevel()) - { - GameMain.QuitToMainMenu(save: true); - } - return true; - } - }; + if (IsOutpostLevel()) { GameMain.QuitToMainMenu(save: true); } + }); } } else if (GameMain.GameSession.GameMode is TestGameMode) { - new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("PauseMenuReturnToEditor")) + CreateButton("PauseMenuReturnToEditor", buttonContainer, action: () => { - OnClicked = (btn, userdata) => - { - GameMain.GameSession.EndRound(""); - pauseMenuOpen = false; - return true; - } - }; + GameMain.GameSession?.EndRound(""); + }); } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { - new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), - text: TextManager.Get(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby": "EndRound")) + bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsOutpostLevel(); + if (canSave) { - OnClicked = (btn, userdata) => + CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () => { - if (!GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { return false; } - if (GameMain.GameSession.GameMode is CampaignMode && !IsOutpostLevel() || (!Submarine.MainSub.AtStartExit && !Submarine.MainSub.AtEndExit)) - { - var msgBox = new GUIMessageBox("", - TextManager.Get(GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (_, __) => - { - pauseMenuOpen = false; - GameMain.Client.RequestRoundEnd(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; - } - else - { - pauseMenuOpen = false; - GameMain.Client.RequestRoundEnd(); - } - return true; - } - }; + GameMain.Client?.RequestRoundEnd(save: true); + }); + } + + CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer, + verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd", + action: () => + { + GameMain.Client?.RequestRoundEnd(save: false); + }); } } - - var quitButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuQuit")); - quitButton.OnClicked += (btn, userData) => + + if (GameMain.GameSession != null || Screen.Selected is CharacterEditorScreen || Screen.Selected is SubEditorScreen) { - if (GameMain.GameSession != null || (Screen.Selected is CharacterEditorScreen || Screen.Selected is SubEditorScreen)) - { - string text = GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification"; - var msgBox = new GUIMessageBox("", TextManager.Get(text), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => + CreateButton("PauseMenuQuit", buttonContainer, + verificationTextTag: GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification", + action: () => { GameMain.QuitToMainMenu(save: false); - pauseMenuOpen = false; - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = (_, userdata) => - { - pauseMenuOpen = false; - msgBox.Close(); - return true; - }; - } - else - { - GameMain.QuitToMainMenu(save: false); - pauseMenuOpen = false; - } - return true; - }; + }); + } + else + { + CreateButton("PauseMenuQuit", buttonContainer, action: () => { GameMain.QuitToMainMenu(save: false); }); + } GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock)); } + + void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null) + { + new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), TextManager.Get(textTag)) + { + OnClicked = (btn, userData) => + { + if (string.IsNullOrEmpty(verificationTextTag)) + { + pauseMenuOpen = false; + action?.Invoke(); + } + else + { + CreateVerificationPrompt(verificationTextTag, action); + } + return true; + } + }; + } + + void CreateVerificationPrompt(string textTag, Action confirmAction) + { + var msgBox = new GUIMessageBox("", TextManager.Get(textTag), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (_, __) => + { + pauseMenuOpen = false; + confirmAction?.Invoke(); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; + } } private static bool TogglePauseMenu(GUIButton button, object obj) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 34e99bc03..f03ad241a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -531,6 +531,17 @@ namespace Barotrauma } } + public virtual void ForceLayoutRecalculation() + { + //This is very ugly but it gets the job done, it + //would be real nice to un-jank this some day + ForceUpdate(); + ForceUpdate(); + foreach (var child in Children) { child.ForceLayoutRecalculation(); } + } + + public void ForceUpdate() => Update((float)Timing.Step); + /// /// Updates all the children manually. /// @@ -831,7 +842,7 @@ namespace Barotrauma CoroutineManager.StartCoroutine(SlideToPosition(duration, 0.0f, targetPos)); } - private IEnumerable SlideToPosition(float duration, float wait, Vector2 target) + private IEnumerable SlideToPosition(float duration, float wait, Vector2 target) { float t = 0.0f; var (startX, startY) = RectTransform.ScreenSpaceOffset.ToVector2(); @@ -855,7 +866,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - private IEnumerable LerpAlpha(float to, float duration, bool removeAfter, float wait = 0.0f) + private IEnumerable LerpAlpha(float to, float duration, bool removeAfter, float wait = 0.0f) { State = ComponentState.None; float t = 0.0f; @@ -894,7 +905,7 @@ namespace Barotrauma pulsateCoroutine = CoroutineManager.StartCoroutine(DoPulsate(startScale, endScale, duration), "Pulsate" + ToString()); } - private IEnumerable DoPulsate(Vector2 startScale, Vector2 endScale, float duration) + private IEnumerable DoPulsate(Vector2 startScale, Vector2 endScale, float duration) { float t = 0.0f; while (t < duration) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index e58a115fc..51c0b2d04 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -34,7 +34,7 @@ namespace Barotrauma public GUIScrollBar ScrollBar { get; private set; } private readonly Dictionary childVisible = new Dictionary(); - + private int totalSize; private bool childrenNeedsRecalculation; private bool scrollBarNeedsRecalculation; @@ -53,7 +53,23 @@ namespace Barotrauma } } - public bool SelectMultiple; + public enum SelectMode + { + SelectSingle, + SelectMultiple, + RequireShiftToSelectMultiple + } + + public SelectMode CurrentSelectMode = SelectMode.SelectSingle; + + public bool SelectMultiple + { + get { return CurrentSelectMode != SelectMode.SelectSingle; } + set + { + CurrentSelectMode = value ? SelectMode.SelectMultiple : SelectMode.SelectSingle; + } + } public bool HideChildrenOutsideFrame = true; @@ -103,7 +119,7 @@ namespace Barotrauma /// /// true if mouse down should select elements instead of mouse up /// - private bool useMouseDownToSelect = false; + private readonly bool useMouseDownToSelect = false; private Vector4? overridePadding; public Vector4 Padding @@ -132,10 +148,7 @@ namespace Barotrauma // TODO: fix implicit hiding public bool Selected { get; set; } - public List AllSelected - { - get { return selected; } - } + public IReadOnlyList AllSelected => selected; public object SelectedData { @@ -214,25 +227,34 @@ namespace Barotrauma public bool AutoHideScrollBar { get; set; } = true; private bool IsScrollBarOnDefaultSide { get; set; } - public bool CanDragElements + public enum DragMode + { + NoDragging, + DragWithinBox, + DragOutsideBox + } + + private DragMode currentDragMode = DragMode.NoDragging; + public DragMode CurrentDragMode { get { - return canDragElements; + return currentDragMode; } set { - if (value == false && canDragElements && draggedElement != null) + if (value == DragMode.NoDragging && currentDragMode != DragMode.NoDragging && isDraggingElement) { DraggedElement = null; } - canDragElements = value; + currentDragMode = value; } } - private bool canDragElements = false; + private GUIComponent draggedElement; - private Rectangle draggedReferenceRectangle; - private Point draggedReferenceOffset; + private Point dragMousePosRelativeToTopLeftCorner; + private bool isDraggingElement => draggedElement != null; + public bool HasDraggedElementIndexChanged { get; private set; } public GUIComponent DraggedElement @@ -246,8 +268,24 @@ namespace Barotrauma if (value == draggedElement) { return; } draggedElement = value; HasDraggedElementIndexChanged = false; + + if (value == null) { return; } + + dragMousePosRelativeToTopLeftCorner = PlayerInput.MousePosition.ToPoint() - value.Rect.Location; + + if (SelectMultiple) + { + if (!AllSelected.Contains(DraggedElement)) + { + Select(DraggedElement.ToEnumerable()); + } + } } } + + //This exists to work around the fact that rendering child + //elements on top of the listbox's siblings is a clusterfuck. + public bool HideDraggedElement = false; private readonly bool isHorizontal; @@ -354,7 +392,7 @@ namespace Barotrauma (child.UserData == null && userData == null)) { Select(i, force, autoScroll); - if (!SelectMultiple) return; + if (!SelectMultiple) { return; } } i++; } @@ -363,9 +401,10 @@ namespace Barotrauma private Point CalculateFrameSize(bool isHorizontal, int scrollBarSize) => isHorizontal ? new Point(Rect.Width, Rect.Height - scrollBarSize) : new Point(Rect.Width - scrollBarSize, Rect.Height); - private void RepositionChildren() + public Vector2 CalculateTopOffset() { - int x = 0, y = 0; + int x = 0; + int y = 0; if (ScrollBar.BarSize < 1.0f) { if (ScrollBar.IsHorizontal) @@ -378,53 +417,59 @@ namespace Barotrauma } } + return new Vector2(x, y); + } + + private void CalculateChildrenOffsets(Action callback) + { + Vector2 topOffset = CalculateTopOffset(); + int x = (int)topOffset.X; + int y = (int)topOffset.Y; + for (int i = 0; i < Content.CountChildren; i++) { GUIComponent child = Content.GetChild(i); if (!child.Visible) { continue; } if (RectTransform != null) { - if (child != draggedElement && (child.RectTransform.AbsoluteOffset.X != x || child.RectTransform.AbsoluteOffset.Y != y)) - { - child.RectTransform.AbsoluteOffset = new Point(x, y); - } + callback(i, new Point(x, y)); } if (useGridLayout) { + void advanceGridLayout( + ref int primaryCoord, + ref int secondaryCoord, + int primaryChildDimension, + int secondaryChildDimension, + int primaryParentDimension) + { + if (primaryCoord + primaryChildDimension + Spacing > primaryParentDimension) + { + primaryCoord = 0; + secondaryCoord += secondaryChildDimension + Spacing; + callback(i, new Point(x, y)); + } + primaryCoord += primaryChildDimension + Spacing; + } + if (ScrollBar.IsHorizontal) { - if (y + child.Rect.Height + Spacing > Content.Rect.Height) - { - y = 0; - x += child.Rect.Width + Spacing; - if (child != draggedElement && (child.RectTransform.AbsoluteOffset.X != x || child.RectTransform.AbsoluteOffset.Y != y)) - { - child.RectTransform.AbsoluteOffset = new Point(x, y); - } - y += child.Rect.Height + Spacing; - } - else - { - y += child.Rect.Height + Spacing; - } + advanceGridLayout( + primaryCoord: ref y, + secondaryCoord: ref x, + primaryChildDimension: child.Rect.Height, + secondaryChildDimension: child.Rect.Width, + primaryParentDimension: Content.Rect.Height); } else { - if (x + child.Rect.Width + Spacing > Content.Rect.Width) - { - x = 0; - y += child.Rect.Height + Spacing; - if (child != draggedElement && (child.RectTransform.AbsoluteOffset.X != x || child.RectTransform.AbsoluteOffset.Y != y)) - { - child.RectTransform.AbsoluteOffset = new Point(x, y); - } - x += child.Rect.Width + Spacing; - } - else - { - x += child.Rect.Width + Spacing; - } + advanceGridLayout( + primaryCoord: ref x, + secondaryCoord: ref y, + primaryChildDimension: child.Rect.Width, + secondaryChildDimension: child.Rect.Height, + primaryParentDimension: Content.Rect.Width); } } else @@ -440,6 +485,18 @@ namespace Barotrauma } } } + + private void RepositionChildren() + { + CalculateChildrenOffsets((index, offset) => + { + var child = Content.GetChild(index); + if (child != draggedElement && child.RectTransform.AbsoluteOffset != offset) + { + child.RectTransform.AbsoluteOffset = offset; + } + }); + } /// /// Scrolls the list to the specific element, currently only works when smooth scrolling and PadBottom are enabled. @@ -466,7 +523,7 @@ namespace Barotrauma { CoroutineManager.StartCoroutine(ScrollCoroutine()); - IEnumerable ScrollCoroutine() + IEnumerable ScrollCoroutine() { if (BarSize >= 1.0f) { @@ -490,68 +547,122 @@ namespace Barotrauma } } - - private void UpdateChildrenRect() + private void StartDraggingElement(GUIComponent child) { - //dragging - if (CanDragElements && draggedElement != null) + DraggedElement = child; + } + + private bool UpdateDragging() + { + if (CurrentDragMode == DragMode.NoDragging || !isDraggingElement) { return false; } + if (!PlayerInput.PrimaryMouseButtonHeld()) { - if (!PlayerInput.PrimaryMouseButtonHeld()) + var draggedElem = draggedElement; + OnRearranged?.Invoke(this, draggedElem.UserData); + DraggedElement = null; + RepositionChildren(); + if (AllSelected.Contains(draggedElem)) { return true; } + } + else + { + Vector2 topOffset = CalculateTopOffset(); + var mousePos = PlayerInput.MousePosition.ToPoint(); + draggedElement.RectTransform.AbsoluteOffset = mousePos - Content.Rect.Location - dragMousePosRelativeToTopLeftCorner; + if (CurrentDragMode != DragMode.DragOutsideBox) { - OnRearranged?.Invoke(this, draggedElement.UserData); - DraggedElement = null; - RepositionChildren(); + var offset = draggedElement.RectTransform.AbsoluteOffset; + draggedElement.RectTransform.AbsoluteOffset = + isHorizontal ? new Point(offset.X, 0) : new Point(0, offset.Y); + } + + int index = Content.RectTransform.GetChildIndex(draggedElement.RectTransform); + int newIndex = index; + + Point draggedOffsetWhenReleased = Point.Zero; + CalculateChildrenOffsets((i, offset) => + { + if (index != i) { return; } + draggedOffsetWhenReleased = offset; + }); + Rectangle draggedRectWhenReleased = new Rectangle(Content.Rect.Location + draggedOffsetWhenReleased, draggedElement.Rect.Size); + + void shiftIndices( + float mousePos, + ref int draggedRectWhenReleasedLocation, + int draggedRectWhenReleasedSize) + { + while (mousePos > (draggedRectWhenReleasedLocation + draggedRectWhenReleasedSize) && newIndex < Content.CountChildren-1) + { + newIndex++; + draggedRectWhenReleasedLocation += draggedRectWhenReleasedSize; + } + while (mousePos < draggedRectWhenReleasedLocation && newIndex > 0) + { + newIndex--; + draggedRectWhenReleasedLocation -= draggedRectWhenReleasedSize; + } + + if (newIndex != index && AllSelected.Count > 1) + { + this.selected.Sort((a, b) => Content.GetChildIndex(a) - Content.GetChildIndex(b)); + int draggedPos = AllSelected.IndexOf(draggedElement); + if (newIndex < draggedPos) + { + newIndex = draggedPos; + } + if (newIndex >= Content.CountChildren - (AllSelected.Count - draggedPos)) + { + int max = Content.CountChildren - (AllSelected.Count - draggedPos); + newIndex = max; + } + } + } + + if (isHorizontal) + { + shiftIndices( + mousePos.X, + ref draggedRectWhenReleased.X, + draggedRectWhenReleased.Width); } else { - draggedElement.RectTransform.AbsoluteOffset = isHorizontal ? - draggedReferenceOffset + new Point((int)PlayerInput.MousePosition.X - draggedReferenceRectangle.Center.X, 0) : - draggedReferenceOffset + new Point(0, (int)PlayerInput.MousePosition.Y - draggedReferenceRectangle.Center.Y); + shiftIndices( + mousePos.Y, + ref draggedRectWhenReleased.Y, + draggedRectWhenReleased.Height); + } - int index = Content.RectTransform.GetChildIndex(draggedElement.RectTransform); - int currIndex = index; - - if (isHorizontal) + if (newIndex != index) + { + if (AllSelected.Count > 1) { - while (currIndex > 0 && PlayerInput.MousePosition.X < draggedReferenceRectangle.Left) + this.selected.Sort((a, b) => Content.GetChildIndex(a) - Content.GetChildIndex(b)); + int indexOfDraggedElem = AllSelected.IndexOf(draggedElement); + IEnumerable allSelected = AllSelected; + if (newIndex > index) { allSelected = allSelected.Reverse(); } + foreach (var elem in allSelected) { - currIndex--; - draggedReferenceRectangle.X -= draggedReferenceRectangle.Width; - draggedReferenceOffset.X -= draggedReferenceRectangle.Width; - } - while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.X > draggedReferenceRectangle.Right) - { - currIndex++; - draggedReferenceRectangle.X += draggedReferenceRectangle.Width; - draggedReferenceOffset.X += draggedReferenceRectangle.Width; + elem.RectTransform.RepositionChildInHierarchy(newIndex + AllSelected.IndexOf(elem) - indexOfDraggedElem); } } else { - while (currIndex > 0 && PlayerInput.MousePosition.Y < draggedReferenceRectangle.Top) - { - currIndex--; - draggedReferenceRectangle.Y -= draggedReferenceRectangle.Height; - draggedReferenceOffset.Y -= draggedReferenceRectangle.Height; - } - while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.Y > draggedReferenceRectangle.Bottom) - { - currIndex++; - draggedReferenceRectangle.Y += draggedReferenceRectangle.Height; - draggedReferenceOffset.Y += draggedReferenceRectangle.Height; - } + draggedElement.RectTransform.RepositionChildInHierarchy(newIndex); } - - if (currIndex != index) - { - draggedElement.RectTransform.RepositionChildInHierarchy(currIndex); - HasDraggedElementIndexChanged = true; - } - - return; + HasDraggedElementIndexChanged = true; } + + return true; } + return false; + } + + private void UpdateChildrenRect() + { + if (UpdateDragging()) { return; } + if (SelectTop) { foreach (GUIComponent child in Content.Children) @@ -581,7 +692,7 @@ namespace Barotrauma for (int i = 0; i < Content.CountChildren; i++) { var child = Content.RectTransform.GetChild(i)?.GUIComponent; - if (child == null || !child.Visible) { continue; } + if (!(child is { Visible: true })) { continue; } // selecting if (Enabled && (CanBeFocused || CanInteractWhenUnfocusable) && child.CanBeFocused && child.Rect.Contains(PlayerInput.MousePosition) && GUI.IsMouseOn(child)) @@ -595,19 +706,15 @@ namespace Barotrauma if (SelectTop) { ScrollToElement(child); - Select(i, autoScroll: false, takeKeyBoardFocus: true); - } - else - { - Select(i, autoScroll: false, takeKeyBoardFocus: true); } + Select(i, autoScroll: false, takeKeyBoardFocus: true); } - if (CanDragElements && PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == child) + if (CurrentDragMode != DragMode.NoDragging + && (CurrentSelectMode != SelectMode.RequireShiftToSelectMultiple || (!PlayerInput.IsShiftDown() && !PlayerInput.IsCtrlDown())) + && PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == child) { - DraggedElement = child; - draggedReferenceRectangle = child.Rect; - draggedReferenceOffset = child.RectTransform.AbsoluteOffset; + StartDraggingElement(child); } } else if (selected.Contains(child)) @@ -686,6 +793,13 @@ namespace Barotrauma OnAddedToGUIUpdateList?.Invoke(this); } + public override void ForceLayoutRecalculation() + { + base.ForceLayoutRecalculation(); + Content.ForceLayoutRecalculation(); + ScrollBar.ForceLayoutRecalculation(); + } + public void RecalculateChildren() { foreach (GUIComponent child in Content.Children) @@ -709,8 +823,6 @@ namespace Barotrauma } } - public void ForceUpdate() => Update((float)Timing.Step); - protected override void Update(float deltaTime) { if (!Visible) { return; } @@ -805,7 +917,7 @@ namespace Barotrauma } else { - ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; + ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * ScrollBar.UnclampedBarSize; } } @@ -870,6 +982,7 @@ namespace Barotrauma if (childIndex >= Content.CountChildren || childIndex < 0) { return; } GUIComponent child = Content.GetChild(childIndex); + if (child is null) { return; } bool wasSelected = true; if (OnSelected != null) @@ -880,7 +993,8 @@ namespace Barotrauma if (!wasSelected) { return; } - if (SelectMultiple) + if (CurrentSelectMode == SelectMode.SelectMultiple || + (CurrentSelectMode == SelectMode.RequireShiftToSelectMultiple && PlayerInput.IsCtrlDown())) { if (selected.Contains(child)) { @@ -891,6 +1005,23 @@ namespace Barotrauma selected.Add(child); } } + else if (CurrentSelectMode == SelectMode.RequireShiftToSelectMultiple && PlayerInput.IsShiftDown()) + { + var first = SelectedComponent ?? child; + var last = child; + int firstIndex = Content.GetChildIndex(first); + int lastIndex = Content.GetChildIndex(last); + int sgn = Math.Sign(lastIndex - firstIndex); + selected.Clear(); selected.Add(first); + for (int i = firstIndex + sgn; i != lastIndex; i += sgn) + { + if (Content.GetChild(i) is { Visible: true } interChild) + { + selected.Add(interChild); + } + } + if (first != last) { selected.Add(last); } + } else { selected.Clear(); @@ -937,6 +1068,14 @@ namespace Barotrauma } } + public void Select(IEnumerable children) + { + Selected = true; + selected.Clear(); + selected.AddRange(children.Where(c => Content.Children.Contains(c))); + foreach (var child in selected) { OnSelected?.Invoke(child, child.UserData); } + } + public void Deselect() { Selected = false; @@ -1007,9 +1146,12 @@ namespace Barotrauma } float minScrollBarSize = 20.0f; + ScrollBar.UnclampedBarSize = ScrollBar.IsHorizontal ? + Math.Min(Content.Rect.Width / (float)totalSize, 1.0f) : + Math.Min(Content.Rect.Height / (float)totalSize, 1.0f); ScrollBar.BarSize = ScrollBar.IsHorizontal ? - Math.Max(Math.Min(Content.Rect.Width / (float)totalSize, 1.0f), minScrollBarSize / Content.Rect.Width) : - Math.Max(Math.Min(Content.Rect.Height / (float)totalSize, 1.0f), minScrollBarSize / Content.Rect.Height); + Math.Max(ScrollBar.UnclampedBarSize, minScrollBarSize / Content.Rect.Width) : + Math.Max(ScrollBar.UnclampedBarSize, minScrollBarSize / Content.Rect.Height); } public override void ClearChildren() @@ -1052,10 +1194,11 @@ namespace Barotrauma int i = 0; foreach (GUIComponent child in Content.Children) { - if (!child.Visible) continue; + if (!child.Visible) { continue; } + if (child == draggedElement && CurrentDragMode == DragMode.DragOutsideBox) { continue; } if (!IsChildInsideFrame(child)) { - if (lastVisible > 0) break; + if (lastVisible > 0) { break; } continue; } lastVisible = i; @@ -1070,6 +1213,11 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } + if (isDraggingElement && CurrentDragMode == DragMode.DragOutsideBox && !HideDraggedElement) + { + draggedElement.DrawManually(spriteBatch, alsoChildren: true, recursive: true); + } + if (ScrollBarVisible) { ScrollBar.DrawManually(spriteBatch, alsoChildren: true, recursive: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index 55a2d545f..aa44f2ffc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -183,6 +183,11 @@ namespace Barotrauma } } + /// + /// ListBoxes with lots of content in them clamp the size of the scrollbar above a certain minimum size; this is the relative bar size without the clamping applied. + /// + public float UnclampedBarSize; + public float BarSize { get { return barSize; } @@ -299,9 +304,15 @@ namespace Barotrauma } else { + float barScale = 1.0f; + if (UnclampedBarSize > 0.0f) + { + barScale = (UnclampedBarSize / BarSize); + } + MoveButton(new Vector2( - Math.Sign(PlayerInput.MousePosition.X - Bar.Rect.Center.X) * Bar.Rect.Width, - Math.Sign(PlayerInput.MousePosition.Y - Bar.Rect.Center.Y) * Bar.Rect.Height)); + Math.Sign(PlayerInput.MousePosition.X - Bar.Rect.Center.X) * Bar.Rect.Width * barScale, + Math.Sign(PlayerInput.MousePosition.Y - Bar.Rect.Center.Y) * Bar.Rect.Height * barScale)); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 6c6224e8f..f8cbd5414 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -44,6 +44,8 @@ namespace Barotrauma public UISprite PingCircle { get; private set; } + public UISprite YouAreHereCircle { get; private set; } + public UISprite UIGlowCircular { get; private set; } public UISprite UIGlowSolidCircular { get; private set; } @@ -253,6 +255,9 @@ namespace Barotrauma case "pingcircle": PingCircle = new UISprite(subElement); break; + case "youareherecircle": + YouAreHereCircle = new UISprite(subElement); + break; case "radiation": RadiationSprite = new UISprite(subElement); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 86e2a84e3..fbd7b2c77 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma @@ -376,6 +377,7 @@ namespace Barotrauma public void SetTextPos() { + cachedCaretPositions = ImmutableArray.Empty; if (text == null) { return; } censoredText = string.IsNullOrEmpty(text) ? "" : new string('\u2022', text.Length); @@ -389,7 +391,7 @@ namespace Barotrauma if (Wrap && rect.Width > 0) { - wrappedText = ToolBox.WrapText(text, rect.Width - padding.X - padding.Z, Font, textScale, playerInput); + wrappedText = ToolBox.WrapText(text, rect.Width - padding.X - padding.Z, Font, textScale); TextSize = MeasureText(wrappedText); } else if (OverflowClip) @@ -477,108 +479,49 @@ namespace Barotrauma disabledTextColor = color; } - protected List> GetAllPositions() + private ImmutableArray cachedCaretPositions = ImmutableArray.Empty; + + public ImmutableArray GetAllCaretPositions() { - float halfHeight = Font.MeasureString("T").Y * 0.5f * textScale; - string textDrawn = Censor ? CensoredText : WrappedText; - var positions = new List>(); - if (textDrawn.Contains("\n")) + if (cachedCaretPositions.Any()) { - string[] lines = textDrawn.Split('\n'); - int index = 0; - int totalIndex = 0; - for (int i = 0; i < lines.Length; i++) - { - string line = lines[i]; - totalIndex += line.Length; - float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y * textScale; - for (int j = 0; j <= line.Length; j++) - { - Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)) * textScale; - Vector2 indexPos = new Vector2(lineTextSize.X, totalTextHeight - halfHeight) + TextPos - Origin * textScale; - //DebugConsole.NewMessage($"index: {index}, pos: {indexPos}", Color.AliceBlue); - positions.Add(new Tuple(indexPos, index + j)); - } - index = totalIndex; - } + return cachedCaretPositions; } - else - { - textDrawn = Censor ? CensoredText : Text; - for (int i = 0; i <= Text.Length; i++) - { - Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)) * textScale; - Vector2 indexPos = new Vector2(textSize.X, textSize.Y - halfHeight) + TextPos - Origin * textScale; - //DebugConsole.NewMessage($"index: {i}, pos: {indexPos}", Color.WhiteSmoke); - positions.Add(new Tuple(indexPos, i)); - } - } - return positions; + string textDrawn = Censor ? CensoredText : Text; + float w = Wrap + ? (Rect.Width - Padding.X - Padding.Z) / TextScale + : float.PositiveInfinity; + Font.WrapText(textDrawn, w, out Vector2[] positions); + cachedCaretPositions = positions.Select(p => p * TextScale + TextPos - Origin * TextScale).ToImmutableArray(); + return cachedCaretPositions; } - public int GetCaretIndexFromScreenPos(Vector2 pos) + public int GetCaretIndexFromScreenPos(in Vector2 pos) { return GetCaretIndexFromLocalPos(pos - Rect.Location.ToVector2()); } - public int GetCaretIndexFromLocalPos(Vector2 pos) + public int GetCaretIndexFromLocalPos(in Vector2 pos) { - var positions = GetAllPositions(); - if (positions.Count == 0) { return 0; } - float halfHeight = Font.MeasureString("T").Y * 0.5f * textScale; + var positions = GetAllCaretPositions(); + if (positions.Length == 0) { return 0; } - var currPosition = positions[0]; - - float topY = positions.Min(p => p.Item1.Y); - - for (int i = 1; i < positions.Count; i++) + float closestXDist = float.PositiveInfinity; + float closestYDist = float.PositiveInfinity; + int closestIndex = -1; + for (int i = 0; i < positions.Length; i++) { - var p1 = positions[i]; - var p2 = currPosition; - - float diffY = Math.Abs(p1.Item1.Y - pos.Y) - Math.Abs(p2.Item1.Y - pos.Y); - if (diffY < -3.0f) + float xDist = Math.Abs(pos.X - positions[i].X); + float yDist = Math.Abs(pos.Y - (positions[i].Y + Font.LineHeight * 0.5f)); + if (yDist < closestYDist || (MathUtils.NearlyEqual(yDist, closestYDist) && xDist < closestXDist)) { - currPosition = p1; - continue; - } - else if (diffY > 3.0f) - { - continue; - } - else - { - diffY = Math.Abs(p1.Item1.Y - pos.Y); - if (diffY < halfHeight || (p1.Item1.Y == topY && pos.Y < topY)) - { - //we are on this line, select the nearest character - float diffX = Math.Abs(p1.Item1.X - pos.X) - Math.Abs(p2.Item1.X - pos.X); - if (diffX < -1.0f) - { - currPosition = p1; continue; - } - else - { - continue; - } - } - else - { - //we are on a different line, preserve order - if (p1.Item2 < p2.Item2) - { - if (p1.Item1.Y > pos.Y) { currPosition = p1; } - } - else if (p1.Item2 > p2.Item2) - { - if (p1.Item1.Y < pos.Y) { currPosition = p1; } - } - continue; - } + closestIndex = i; + closestXDist = xDist; + closestYDist = yDist; } } - //GUI.AddMessage($"index: {posIndex.Item2}, pos: {posIndex.Item1}", Color.WhiteSmoke); - return currPosition != null ? currPosition.Item2 : Text.Length; + + return closestIndex >= 0 ? closestIndex : Text.Length; } protected override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 4d8a2feae..d09434b5d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -44,7 +44,7 @@ namespace Barotrauma private int? maxTextLength; private int _caretIndex; - private int CaretIndex + public int CaretIndex { get { return _caretIndex; } set @@ -343,34 +343,23 @@ namespace Barotrauma private void CalculateCaretPos() { - string textDrawn = Censor ? textBlock.CensoredText : textBlock.WrappedText; - if (textDrawn.Contains("\n")) + if (Censor || !Wrap) { - string[] lines = textDrawn.Split('\n'); - int totalIndex = 0; - for (int i = 0; i < lines.Length; i++) - { - int currentLineLength = lines[i].Length; - totalIndex += currentLineLength; - // The caret is on this line - if (CaretIndex < totalIndex || totalIndex == textBlock.Text.Length) - { - int diff = totalIndex - CaretIndex; - int index = currentLineLength - diff; - Vector2 lineTextSize = Font.MeasureString(lines[i].Substring(0, index)) * TextBlock.TextScale; - Vector2 lastLineSize = Font.MeasureString(lines[i]) * TextBlock.TextScale; - float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y * TextBlock.TextScale; - caretPos = new Vector2(lineTextSize.X, totalTextHeight - lastLineSize.Y) + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; - break; - } - } + string textDrawn = textBlock.CensoredText; + CaretIndex = Math.Min(CaretIndex, textDrawn.Length); + textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; + Vector2 textSize = Font.MeasureString(textDrawn[..CaretIndex]) * TextBlock.TextScale; + caretPos = new Vector2(textSize.X, 0) + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; } else { - CaretIndex = Math.Min(CaretIndex, textDrawn.Length); - textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; - Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, CaretIndex)) * TextBlock.TextScale; - caretPos = new Vector2(textSize.X, 0) + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; + CaretIndex = Math.Min(CaretIndex, textBlock.Text.Length); + textBlock.Font.WrapText( + textBlock.Text, + (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, + CaretIndex, + out Vector2 requestedCharPos); + caretPos = requestedCharPos * TextBlock.TextScale + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; } caretPosDirty = false; } @@ -383,6 +372,7 @@ namespace Barotrauma memento.Store(Text); } CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; + CalculateCaretPos(); ClearSelection(); selected = true; GUI.KeyboardDispatcher.Subscriber = this; @@ -538,59 +528,37 @@ namespace Barotrauma if (textBlock.WrappedText.Contains("\n")) { // Multiline selection - string[] lines = textBlock.WrappedText.Split('\n'); - int totalIndex = 0; - int previousCharacters = 0; - Vector2 offset = textBlock.TextPos - textBlock.Origin; - for (int i = 0; i < lines.Length; i++) + var characterPositions = textBlock.GetAllCaretPositions(); + (int startIndex, int endIndex) = selectionStartIndex < selectionEndIndex + ? (selectionStartIndex, selectionEndIndex) + : (selectionEndIndex, selectionStartIndex); + endIndex--; + + void drawRect(Vector2 topLeft, Vector2 bottomRight) { - string currentLine = lines[i]; - int currentLineLength = currentLine.Length; - totalIndex += currentLineLength; - bool containsSelection = IsLeftToRight - ? selectionStartIndex < totalIndex && selectionEndIndex > previousCharacters - : selectionEndIndex < totalIndex && selectionStartIndex > previousCharacters; - if (containsSelection) - { - Vector2 currentLineSize = Font.MeasureString(currentLine) * TextBlock.TextScale; - if ((IsLeftToRight && selectionStartIndex < previousCharacters && selectionEndIndex > totalIndex) - || !IsLeftToRight && selectionEndIndex < previousCharacters && selectionStartIndex > totalIndex) - { - // select the whole line - Vector2 topLeft = offset + new Vector2(0, currentLineSize.Y * i); - GUI.DrawRectangle(spriteBatch, Rect.Location.ToVector2() + topLeft, currentLineSize, SelectionColor, isFilled: true); - } - else - { - if (IsLeftToRight) - { - bool selectFromTheBeginning = selectionStartIndex <= previousCharacters; - int startIndex = selectFromTheBeginning ? 0 : Math.Abs(selectionStartIndex - previousCharacters); - int endIndex = Math.Abs(selectionEndIndex - previousCharacters); - int characters = Math.Min(endIndex - startIndex, currentLineLength - startIndex); - Vector2 selectedTextSize = Font.MeasureString(currentLine.Substring(startIndex, characters)) * TextBlock.TextScale; - Vector2 topLeft = selectFromTheBeginning - ? new Vector2(offset.X, offset.Y + currentLineSize.Y * i) - : new Vector2(selectionStartPos.X, offset.Y + currentLineSize.Y * i); - GUI.DrawRectangle(spriteBatch, Rect.Location.ToVector2() + topLeft, selectedTextSize, SelectionColor, isFilled: true); - } - else - { - bool selectFromTheBeginning = selectionStartIndex >= totalIndex; - bool selectFromTheStart = selectionEndIndex <= previousCharacters; - int startIndex = selectFromTheBeginning ? currentLineLength : Math.Abs(selectionStartIndex - previousCharacters); - int endIndex = selectFromTheStart ? 0 : Math.Abs(selectionEndIndex - previousCharacters); - int characters = Math.Min(Math.Abs(endIndex - startIndex), currentLineLength); - Vector2 selectedTextSize = Font.MeasureString(currentLine.Substring(endIndex, characters)) * TextBlock.TextScale; - Vector2 topLeft = selectFromTheBeginning - ? new Vector2(offset.X + currentLineSize.X - selectedTextSize.X, offset.Y + currentLineSize.Y * i) - : new Vector2(selectionStartPos.X - selectedTextSize.X, offset.Y + currentLineSize.Y * i); - GUI.DrawRectangle(spriteBatch, Rect.Location.ToVector2() + topLeft, selectedTextSize, SelectionColor, isFilled: true); - } - } - } - previousCharacters = totalIndex; + int minWidth = GUI.IntScale(5); + if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; } + GUI.DrawRectangle(spriteBatch, + Rect.Location.ToVector2() + topLeft, + bottomRight - topLeft, + SelectionColor, isFilled: true); } + + Vector2 topLeft = characterPositions[startIndex]; + for (int i = startIndex+1; i <= endIndex; i++) + { + Vector2 currPos = characterPositions[i]; + if (!MathUtils.NearlyEqual(topLeft.Y, currPos.Y)) + { + Vector2 bottomRight = characterPositions[i - 1]; + bottomRight += Font.MeasureChar(Text[i - 1]); + drawRect(topLeft, bottomRight); + topLeft = currPos; + } + } + Vector2 finalBottomRight = characterPositions[endIndex]; + finalBottomRight += Font.MeasureChar(Text[endIndex]); + drawRect(topLeft, finalBottomRight); } else { @@ -728,8 +696,15 @@ namespace Barotrauma { InitSelectionStart(); } - float lineHeight = Font.MeasureString("T").Y * TextBlock.TextScale; - int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y - lineHeight)); + float lineHeight = Font.LineHeight * TextBlock.TextScale; + int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y - lineHeight * 0.5f)); + textBlock.Font.WrapText( + textBlock.Text, + (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, + newIndex, + out Vector2 requestedCharPos); + requestedCharPos *= TextBlock.TextScale; + if (MathUtils.NearlyEqual(requestedCharPos.Y, caretPos.Y)) { newIndex = 0; } CaretIndex = newIndex; caretTimer = 0; HandleSelection(); @@ -739,8 +714,15 @@ namespace Barotrauma { InitSelectionStart(); } - lineHeight = Font.MeasureString("T").Y * TextBlock.TextScale; - newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y + lineHeight)); + lineHeight = Font.LineHeight * TextBlock.TextScale; + newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y + lineHeight * 1.5f)); + textBlock.Font.WrapText( + textBlock.Text, + (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, + newIndex, + out Vector2 requestedCharPos2); + requestedCharPos2 *= TextBlock.TextScale; + if (MathUtils.NearlyEqual(requestedCharPos2.Y, caretPos.Y)) { newIndex = Text.Length; } CaretIndex = newIndex; caretTimer = 0; HandleSelection(); @@ -803,6 +785,7 @@ namespace Barotrauma } break; } + if (caretPosDirty) { CalculateCaretPos(); } OnKeyHit?.Invoke(this, key); void HandleSelection() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Graph.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Graph.cs index 4a37aea4c..a4732f9ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Graph.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Graph.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; using System.Linq; namespace Barotrauma @@ -37,39 +38,44 @@ namespace Barotrauma values[0] = newValue; } - public void Draw(SpriteBatch spriteBatch, Rectangle rect, float? maxVal, float xOffset, Color color) + public delegate void GraphDelegate(SpriteBatch spriteBatch, float value, int order, Vector2 position); + + public void Draw(SpriteBatch spriteBatch, Rectangle rect, float? maxValue = null, float xOffset = 0, Color? color = null, GraphDelegate doForEachValue = null) { + color ??= Color.White; float graphMaxVal = 1.0f; - if (maxVal == null) + if (maxValue == null) { graphMaxVal = LargestValue(); } - else if (maxVal > 0.0f) + else if (maxValue > 0.0f) { - graphMaxVal = (float)maxVal; + graphMaxVal = (float)maxValue; } GUI.DrawRectangle(spriteBatch, rect, Color.White); - if (values.Length == 0) return; + if (values.Length == 0) { return; } - float lineWidth = (float)rect.Width / (float)(values.Length - 2); - float yScale = (float)rect.Height / graphMaxVal; + float lineWidth = rect.Width / (float)(values.Length - 2); + float yScale = rect.Height / graphMaxVal; Vector2 prevPoint = new Vector2(rect.Right, rect.Bottom - (values[1] + (values[0] - values[1]) * xOffset) * yScale); float currX = rect.Right - ((xOffset - 1.0f) * lineWidth); for (int i = 1; i < values.Length - 1; i++) { + float value = values[i]; currX -= lineWidth; - Vector2 newPoint = new Vector2(currX, rect.Bottom - values[i] * yScale); - GUI.DrawLine(spriteBatch, prevPoint, newPoint - new Vector2(1.0f, 0), color); + Vector2 newPoint = new Vector2(currX, rect.Bottom - value * yScale); + GUI.DrawLine(spriteBatch, prevPoint, newPoint - new Vector2(1.0f, 0), color.Value); prevPoint = newPoint; + doForEachValue?.Invoke(spriteBatch, value, i, newPoint); } - - Vector2 lastPoint = new Vector2(rect.X, - rect.Bottom - (values[values.Length - 1] + (values[values.Length - 2] - values[values.Length - 1]) * xOffset) * yScale); - - GUI.DrawLine(spriteBatch, prevPoint, lastPoint, color); + int lastIndex = values.Length - 1; + float lastValue = values[lastIndex]; + Vector2 lastPoint = new Vector2(rect.X, rect.Bottom - (lastValue + (values[values.Length - 2] - lastValue) * xOffset) * yScale); + GUI.DrawLine(spriteBatch, prevPoint, lastPoint, color.Value); + doForEachValue?.Invoke(spriteBatch, lastValue, lastIndex, lastPoint); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 3e7e06550..8d83781a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -379,11 +379,42 @@ namespace Barotrauma 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(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); + 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())) + 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; } @@ -395,7 +426,7 @@ namespace Barotrauma } bool drawn; - public IEnumerable DoLoading(IEnumerable loader) + public IEnumerable DoLoading(IEnumerable loader) { drawn = false; LoadState = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index a00a820a9..04bbaa485 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -732,7 +732,7 @@ namespace Barotrauma CoroutineManager.StartCoroutine(DoScaleAnimation(targetSize, duration)); } - private IEnumerable DoMoveAnimation(Point targetPos, float duration) + private IEnumerable DoMoveAnimation(Point targetPos, float duration) { Vector2 startPos = AbsoluteOffset.ToVector2(); float t = 0.0f; @@ -746,7 +746,7 @@ namespace Barotrauma animTargetPos = null; yield return CoroutineStatus.Success; } - private IEnumerable DoScaleAnimation(Point targetSize, float duration) + private IEnumerable DoScaleAnimation(Point targetSize, float duration) { Vector2 startSize = NonScaledSize.ToVector2(); float t = 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs index afb00b206..3648f41af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs @@ -151,9 +151,8 @@ namespace Barotrauma /// public static void DrawPoint(this SpriteBatch spriteBatch, Vector2 position, Color color, float size = 1f) { - var scale = Vector2.One * size; var offset = new Vector2(0.5f) - new Vector2(size * 0.5f); - spriteBatch.Draw(GetTexture(spriteBatch), position + offset, null, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0); + spriteBatch.Draw(GetTexture(spriteBatch), position + offset, null, color, 0.0f, Vector2.Zero, new Vector2(size), SpriteEffects.None, 0); } public static void DrawCircle(this SpriteBatch spriteBatch, Vector2 center, float radius, int sides, Color color, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 114c2282e..191a8392d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -266,7 +266,7 @@ namespace Barotrauma }; if (balanceAfterTransaction != CurrentLocation.StoreCurrentBalance) { - var newStatus = Location.GetStoreBalanceStatus(balanceAfterTransaction); + var newStatus = CurrentLocation.GetStoreBalanceStatus(balanceAfterTransaction); if (CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) { string tooltipTag = newStatus.SellPriceModifier > CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier ? diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 409bb15fb..848bfc4b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -471,7 +471,7 @@ namespace Barotrauma } // Initial submarine selection needs a slight wait to allow the layoutgroups to place content properly - private IEnumerable SelectOwnSubmarineWithDelay(SubmarineInfo info, SubmarineDisplayContent display) + private IEnumerable SelectOwnSubmarineWithDelay(SubmarineInfo info, SubmarineDisplayContent display) { yield return new WaitForSeconds(0.05f); SelectSubmarine(info, display.background.Rect); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 6ed653ddc..ee5cefd50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1541,13 +1541,41 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(skillNames); } + private bool HasUnlockedAllTalents(Character controlledCharacter) + { + if (TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) + { + foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) + { + foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) + { + if (talentOption.Talents.None(t => controlledCharacter.HasTalent(t.Identifier))) + { + return false; + } + } + } + } + return true; + } + private void UpdateTalentButtons() { Character controlledCharacter = Character.Controlled; + if (controlledCharacter?.Info == null) { return; } - experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; - experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel(); - //experienceBar.ToolTip = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; + bool unlockedAllTalents = HasUnlockedAllTalents(controlledCharacter); + + if (unlockedAllTalents) + { + experienceText.Text = string.Empty; + experienceBar.BarSize = 1f; + } + else + { + experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; + experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel(); + } selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); @@ -1555,7 +1583,11 @@ namespace Barotrauma int talentCount = selectedTalents.Count - controlledCharacter.Info.GetUnlockedTalentsInTree().Count(); - if (talentCount > 0) + if (unlockedAllTalents) + { + talentPointText.SetRichText($"‖color:{XMLExtensions.ToStringHex(Color.Gray)}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖"); + } + else if (talentCount > 0) { string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUI.Style.Red)}‖{-talentCount}‖color:end‖"; string localizedString = TextManager.GetWithVariables("talentmenu.points.spending", new []{ "[amount]", "[used]" }, new []{ pointsLeft, pointsUsed}); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 9134bfff2..9d4e6e7e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -436,7 +436,7 @@ namespace Barotrauma } } - private IEnumerable Load(bool isSeparateThread) + private IEnumerable Load(bool isSeparateThread) { if (GameSettings.VerboseLogging) { @@ -534,9 +534,8 @@ namespace Barotrauma Debug.WriteLine("sounds"); int i = 0; - foreach (object crObj in SoundPlayer.Init()) + foreach (CoroutineStatus status in SoundPlayer.Init()) { - CoroutineStatus status = (CoroutineStatus)crObj; if (status == CoroutineStatus.Success) break; i++; @@ -1000,11 +999,11 @@ namespace Barotrauma } } - NetworkMember?.Update((float)Timing.Step); - GUI.Update((float)Timing.Step); } + NetworkMember?.Update((float)Timing.Step); + CoroutineManager.Update((float)Timing.Step, Paused ? 0.0f : (float)Timing.Step); SteamManager.Update((float)Timing.Step); @@ -1231,7 +1230,7 @@ namespace Barotrauma } static bool waitForKeyHit = true; - public CoroutineHandle ShowLoading(IEnumerable loader, bool waitKeyHit = true) + public CoroutineHandle ShowLoading(IEnumerable loader, bool waitKeyHit = true) { waitForKeyHit = waitKeyHit; loadingScreenOpen = true; @@ -1256,7 +1255,7 @@ namespace Barotrauma } if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } - if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); } + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } base.OnExiting(sender, args); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 3010c608a..a92ac9d93 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -40,12 +40,13 @@ namespace Barotrauma private List SoldEntities { get; } = new List(); + // The bag slot is intentionally left out since we want to be able to sell items from there + private readonly List equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; + public IEnumerable GetSellableItems(Character character) { if (character == null) { return new List(); } var confirmedSoldEntities = GetConfirmedSoldEntities(); - // The bag slot is intentionally left out since we want to be able to sell items from there - var equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; return character.Inventory.FindAllItems(item => { if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } @@ -73,6 +74,7 @@ namespace Barotrauma return Submarine.MainSub.GetItems(true).FindAll(item => { if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } + if (item.GetRootInventoryOwner() is Character) { return false; } if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } @@ -101,7 +103,7 @@ namespace Barotrauma private bool IsItemSellable(Item item, IEnumerable confirmedSoldEntities) { if (!item.Prefab.CanBeSold) { return false; } - if (item.SpawnedInOutpost) { return false; } + if (item.SpawnedInCurrentOutpost) { return false; } if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } if (item.OwnInventory?.Container is ItemContainer itemContainer) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 09fe317e9..2ef81f846 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -100,7 +100,7 @@ namespace Barotrauma { AutoHideScrollBar = false, CanBeFocused = false, - CanDragElements = true, + CurrentDragMode = GUIListBox.DragMode.DragWithinBox, CanInteractWhenUnfocusable = true, OnSelected = (component, userData) => false, SelectMultiple = false, @@ -359,34 +359,41 @@ namespace Barotrauma CanBeFocused = false }; - var jobIconBackground = new GUIImage( + // Hide the icon to make more space for the name if the crew list's width is small enough + bool isJobIconVisible = crewListEntrySize.X >= 220; + + if (isJobIconVisible) + { + var jobIconBackground = new GUIImage( new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), jobIndicatorBackground, scaleToFit: true) - { - CanBeFocused = false, - UserData = "job" - }; - if (character?.Info?.Job.Prefab?.Icon != null) - { - new GUIImage( - new RectTransform(Vector2.One, jobIconBackground.RectTransform), - character.Info.Job.Prefab.Icon, - scaleToFit: true) { CanBeFocused = false, - Color = character.Info.Job.Prefab.UIColor, - HoverColor = character.Info.Job.Prefab.UIColor, - PressedColor = character.Info.Job.Prefab.UIColor, - SelectedColor = character.Info.Job.Prefab.UIColor + UserData = "job" }; + if (character?.Info?.Job.Prefab?.Icon != null) + { + new GUIImage( + new RectTransform(Vector2.One, jobIconBackground.RectTransform), + character.Info.Job.Prefab.Icon, + scaleToFit: true) + { + CanBeFocused = false, + Color = character.Info.Job.Prefab.UIColor, + HoverColor = character.Info.Job.Prefab.UIColor, + PressedColor = character.Info.Job.Prefab.UIColor, + SelectedColor = character.Info.Job.Prefab.UIColor + }; + } } + int iconsVisible = isJobIconVisible ? 5 : 4; var nameRelativeWidth = 1.0f // Start padding - paddingRelativeWidth - // 5 icons (job, 3 orders, sound) - - (5 * 0.8f * iconRelativeWidth) + // icons (job, active orders, current task / voip) + - (iconsVisible * 0.8f * iconRelativeWidth) // Vertical line - (0.1f * iconRelativeWidth) // Spacing @@ -425,7 +432,7 @@ namespace Barotrauma var currentOrderList = new GUIListBox(new RectTransform(new Vector2(0.0f, 1.0f), parent: orderGroup.RectTransform), isHorizontal: true, style: null) { AllowMouseWheelScroll = false, - CanDragElements = true, + CurrentDragMode = GUIListBox.DragMode.DragWithinBox, HideChildrenOutsideFrame = false, KeepSpaceForScrollBar = false, OnRearranged = OnOrdersRearranged, @@ -439,7 +446,9 @@ namespace Barotrauma if (component is GUIListBox list) { list.CanBeFocused = CanIssueOrders; - list.CanDragElements = CanIssueOrders && list.Content.CountChildren > 1; + list.CurrentDragMode = CanIssueOrders && list.Content.CountChildren > 1 + ? GUIListBox.DragMode.DragWithinBox + : GUIListBox.DragMode.NoDragging; } }; @@ -507,8 +516,11 @@ namespace Barotrauma { if (!(characterComponent?.UserData is Character character)) { return; } if (character.Info?.Job?.Prefab == null) { return; } + string tooltip = TextManager.GetWithVariables("crewlistelementtooltip", + new string[] { "[name]", "[job]" }, + new string[] { character.Name, character.Info.Job.Name }); string color = XMLExtensions.ColorToString(character.Info.Job.Prefab.UIColor); - string tooltip = $"‖color:{color}‖{character.Name} ({character.Info.Job.Name})‖color:end‖"; + tooltip = $"‖color:{color}‖{tooltip}‖color:end‖"; var richTextData = RichTextData.GetRichTextData(tooltip, out string sanitizedTooltip); characterComponent.ToolTip = sanitizedTooltip; characterComponent.TooltipRichTextData = richTextData; @@ -546,7 +558,7 @@ namespace Barotrauma RemoveCharacter(killedCharacter); } - private IEnumerable KillCharacterAnim(GUIComponent component) + private IEnumerable KillCharacterAnim(GUIComponent component) { List components = component.GetAllChildren().ToList(); components.Add(component); @@ -1648,7 +1660,7 @@ namespace Barotrauma } if (characterComponent.Visible) { - if (character == Character.Controlled && characterComponent.State != GUIComponent.ComponentState.Selected) + if (character == Character.Controlled && crewList.SelectedComponent != characterComponent) { crewList.Select(character, force: true); } @@ -2637,7 +2649,7 @@ namespace Barotrauma // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order orderIdentifier = "repairsystems"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any(r => itemContext.ConditionPercentage < r.RepairThreshold)) + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) { if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { @@ -2758,11 +2770,11 @@ namespace Barotrauma if (AIObjectiveCleanupItems.IsValidTarget(item, Character.Controlled, checkInventory: false)) { return true; } if (AIObjectiveCleanupItems.IsValidContainer(item, Character.Controlled)) { return true; } - if (item.Repairables.Any(r => item.ConditionPercentage < r.RepairThreshold)) { return true; } + if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { return true; } var operateWeaponsPrefab = Order.GetPrefab("operateweapons"); return item.Components.Any(c => c is Controller) && (item.GetConnectedComponents().Any(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems)) || - item.GetConnectedComponents(recursive: true).Any(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems))); + item.GetConnectedComponents(recursive: true).Any(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems))); } /// Use a negative value (e.g. -1) if there should be no hotkey associated with the node @@ -2781,7 +2793,7 @@ namespace Barotrauma disableNode = !CanCharacterBeHeard(); } - var mustSetOptionOrTarget = order.HasOptions; + bool mustSetOptionOrTarget = order.HasOptions; Item orderTargetEntity = null; // If the order doesn't have options, but must set a target, @@ -2804,14 +2816,14 @@ namespace Barotrauma { if (disableNode || !CanIssueOrders) { return false; } var o = userData as Order; - if (o.MustManuallyAssign && characterContext == null) - { - CreateAssignmentNodes(node); - } - else if (mustSetOptionOrTarget) + if (mustSetOptionOrTarget) { NavigateForward(button, userData); } + else if (o.MustManuallyAssign && characterContext == null) + { + CreateAssignmentNodes(node); + } else { if (orderTargetEntity != null) @@ -2925,6 +2937,10 @@ namespace Barotrauma { NavigateForward(button, userData); } + else if (o.Item1.MustManuallyAssign && characterContext == null) + { + CreateAssignmentNodes(button); + } else { SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); @@ -2987,12 +3003,19 @@ namespace Barotrauma var node = new GUIButton(new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) { UserData = new Tuple(order, option), - OnClicked = (_, userData) => + OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); - DisableCommandUI(); + if (o.Item1.MustManuallyAssign && characterContext == null) + { + CreateAssignmentNodes(button); + } + else + { + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + DisableCommandUI(); + } return true; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 32605d848..e88f312a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -188,7 +188,7 @@ namespace Barotrauma } - private IEnumerable DoInitialCameraTransition() + private IEnumerable DoInitialCameraTransition() { while (GameMain.Instance.LoadingScreenOpen) { @@ -310,12 +310,12 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) { yield return CoroutineStatus.Success; } - private IEnumerable DoLevelTransition() + private IEnumerable DoLevelTransition() { SoundPlayer.OverrideMusicType = CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead"; SoundPlayer.OverrideMusicDuration = 18.0f; @@ -361,7 +361,7 @@ namespace Barotrauma //-------------------------------------- //wait for the new level to be loaded - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 30); + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 60); while (Level.Loaded == prevLevel || Level.Loaded == null) { if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; } @@ -480,8 +480,6 @@ namespace Barotrauma { IsFirstRound = false; CoroutineManager.StartCoroutine(DoLevelTransition(), "LevelTransition"); - bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); - GUI.SetSavingIndicatorState(success && (Level.IsLoadedOutpost || transitionType != TransitionType.None)); } } @@ -500,7 +498,7 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() + private IEnumerable DoEndCampaignCameraTransition() { Character controlled = Character.Controlled; if (controlled != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 3aa3f47ea..3c376f453 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -243,7 +243,7 @@ namespace Barotrauma mirror: map.CurrentLocation != map.SelectedConnection?.Locations[0])); } - private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) + private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { GameMain.GameSession.StartRound(level, mirrorLevel: mirror); @@ -254,7 +254,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - private IEnumerable DoInitialCameraTransition() + private IEnumerable DoInitialCameraTransition() { while (GameMain.Instance.LoadingScreenOpen) { @@ -378,7 +378,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null) { NextLevel = newLevel; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); @@ -515,7 +515,7 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() + private IEnumerable DoEndCampaignCameraTransition() { if (Character.Controlled != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs index 7372def70..4431dbabb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Tutorials { } - public override IEnumerable UpdateState() + public override IEnumerable UpdateState() { Character Controlled = Character.Controlled; if (Controlled == null) yield return CoroutineStatus.Success; @@ -634,7 +634,7 @@ namespace Barotrauma.Tutorials return Character.Controlled.Inventory.FindItemByIdentifier(itemIdentifier) != null; } - protected IEnumerable KeepReactorRunning(Reactor reactor) + protected IEnumerable KeepReactorRunning(Reactor reactor) { do { @@ -652,7 +652,7 @@ namespace Barotrauma.Tutorials /// /// keeps the enemy away from the sub until the capacitors are loaded /// - private IEnumerable KeepEnemyAway(Character enemy, PowerContainer[] capacitors) + private IEnumerable KeepEnemyAway(Character enemy, PowerContainer[] capacitors) { do { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 60e4b869a..8f22864c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -141,7 +141,7 @@ namespace Barotrauma.Tutorials captain_mechanic.AIController.Enabled = captain_security.AIController.Enabled = captain_engineer.AIController.Enabled = false; } - public override IEnumerable UpdateState() + public override IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen) yield return null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 1b10f65fd..641b28ad8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -139,7 +139,7 @@ namespace Barotrauma.Tutorials reactorItem.GetComponent().AutoTemp = true; } - public override IEnumerable UpdateState() + public override IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen) yield return null; @@ -446,7 +446,7 @@ namespace Barotrauma.Tutorials CoroutineManager.StartCoroutine(TutorialCompleted()); } - public IEnumerable KeepPatientAlive(Character patient) + public IEnumerable KeepPatientAlive(Character patient) { while (patient != null && !patient.Removed) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs index 19fc82001..d2591a2b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Tutorials { } - public override IEnumerable UpdateState() + public override IEnumerable UpdateState() { /*infoBox = CreateInfoFrame("Use the mouse wheel to zoom in and out, and WASD to move the camera around.", true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index 31ea40a37..a380ef67e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -206,7 +206,7 @@ namespace Barotrauma.Tutorials engineer_submarineJunctionBox_3.Condition = 0f; } - public override IEnumerable UpdateState() + public override IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen) yield return null; @@ -378,7 +378,7 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (engineer_brokenJunctionBox.Condition < repairableJunctionBoxComponent.RepairThreshold); // Wait until repaired + } while (repairableJunctionBoxComponent.IsBelowRepairThreshold); // Wait until repaired SetHighlight(engineer_brokenJunctionBox, false); RemoveCompletedObjective(segments[3]); SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true); @@ -422,7 +422,7 @@ namespace Barotrauma.Tutorials Repairable repairableJunctionBoxComponent3 = engineer_submarineJunctionBox_3.GetComponent(); // Remove highlights when each individual machine is repaired - do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (engineer_submarineJunctionBox_1.Condition < repairableJunctionBoxComponent1.RepairThreshold || engineer_submarineJunctionBox_2.Condition < repairableJunctionBoxComponent2.RepairThreshold || engineer_submarineJunctionBox_3.Condition < repairableJunctionBoxComponent3.RepairThreshold); + do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (repairableJunctionBoxComponent1.IsBelowRepairThreshold || repairableJunctionBoxComponent2.IsBelowRepairThreshold || repairableJunctionBoxComponent3.IsBelowRepairThreshold); CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); RemoveCompletedObjective(segments[5]); yield return new WaitForSeconds(2f, false); @@ -462,7 +462,7 @@ namespace Barotrauma.Tutorials return engineer?.SelectedConstruction == item; } - private IEnumerable ReactorOperatedProperly() + private IEnumerable ReactorOperatedProperly() { float timer; @@ -566,17 +566,17 @@ namespace Barotrauma.Tutorials private void CheckJunctionBoxHighlights(Repairable comp1, Repairable comp2, Repairable comp3) { - if (engineer_submarineJunctionBox_1.Condition > comp1.RepairThreshold && engineer_submarineJunctionBox_1.ExternalHighlight) + if (!comp1.IsBelowRepairThreshold && engineer_submarineJunctionBox_1.ExternalHighlight) { SetHighlight(engineer_submarineJunctionBox_1, false); engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_1); } - if (engineer_submarineJunctionBox_2.Condition > comp2.RepairThreshold && engineer_submarineJunctionBox_2.ExternalHighlight) + if (!comp2.IsBelowRepairThreshold && engineer_submarineJunctionBox_2.ExternalHighlight) { SetHighlight(engineer_submarineJunctionBox_2, false); engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_2); } - if (engineer_submarineJunctionBox_3.Condition > comp3.RepairThreshold && engineer_submarineJunctionBox_3.ExternalHighlight) + if (!comp3.IsBelowRepairThreshold && engineer_submarineJunctionBox_3.ExternalHighlight) { SetHighlight(engineer_submarineJunctionBox_3, false); engineer.RemoveActiveObjectiveEntity(engineer_submarineJunctionBox_3); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index fe62c6228..11227ac0e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -225,7 +225,7 @@ namespace Barotrauma.Tutorials base.Update(deltaTime); } - public override IEnumerable UpdateState() + public override IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen) yield return null; @@ -550,7 +550,7 @@ namespace Barotrauma.Tutorials do { yield return null; - if (mechanic_brokenPump.Item.Condition < repairablePumpComponent.RepairThreshold) + if (repairablePumpComponent.IsBelowRepairThreshold) { if (!mechanic.HasEquippedItem("wrench")) { @@ -574,7 +574,7 @@ namespace Barotrauma.Tutorials } } } - } while (mechanic_brokenPump.Item.Condition < repairablePumpComponent.RepairThreshold || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); + } while (repairablePumpComponent.IsBelowRepairThreshold || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); RemoveCompletedObjective(segments[9]); SetHighlight(mechanic_brokenPump.Item, false); do { yield return null; } while (mechanic_brokenhull_2.WaterPercentage > waterVolumeBeforeOpening); @@ -597,7 +597,7 @@ namespace Barotrauma.Tutorials Repairable repairableEngineComponent = mechanic_submarineEngine.Item.GetComponent(); // Remove highlights when each individual machine is repaired - do { CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); yield return null; } while (mechanic_ballastPump_1.Item.Condition < repairablePumpComponent1.RepairThreshold || mechanic_ballastPump_2.Item.Condition < repairablePumpComponent2.RepairThreshold || mechanic_submarineEngine.Item.Condition < repairableEngineComponent.RepairThreshold); + do { CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); yield return null; } while (repairablePumpComponent1.IsBelowRepairThreshold || repairablePumpComponent2.IsBelowRepairThreshold || repairableEngineComponent.IsBelowRepairThreshold); CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); RemoveCompletedObjective(segments[10]); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Complete"), ChatMessageType.Radio, null); @@ -623,17 +623,17 @@ namespace Barotrauma.Tutorials private void CheckHighlights(Repairable comp1, Repairable comp2, Repairable comp3) { - if (mechanic_ballastPump_1.Item.Condition > comp1.RepairThreshold && mechanic_ballastPump_1.Item.ExternalHighlight) + if (!comp1.IsBelowRepairThreshold && mechanic_ballastPump_1.Item.ExternalHighlight) { SetHighlight(mechanic_ballastPump_1.Item, false); mechanic.RemoveActiveObjectiveEntity(mechanic_ballastPump_1.Item); } - if (mechanic_ballastPump_2.Item.Condition > comp2.RepairThreshold && mechanic_ballastPump_2.Item.ExternalHighlight) + if (!comp2.IsBelowRepairThreshold && mechanic_ballastPump_2.Item.ExternalHighlight) { SetHighlight(mechanic_ballastPump_2.Item, false); mechanic.RemoveActiveObjectiveEntity(mechanic_ballastPump_2.Item); } - if (mechanic_submarineEngine.Item.Condition > comp3.RepairThreshold && mechanic_submarineEngine.Item.ExternalHighlight) + if (!comp3.IsBelowRepairThreshold && mechanic_submarineEngine.Item.ExternalHighlight) { SetHighlight(mechanic_submarineEngine.Item, false); mechanic.RemoveActiveObjectiveEntity(mechanic_submarineEngine.Item); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 2a68e6611..594e847cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -201,7 +201,7 @@ namespace Barotrauma.Tutorials SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); } - public override IEnumerable UpdateState() + public override IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen) yield return null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 40ec19062..4cb49e6fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Tutorials GameMain.Instance.ShowLoading(Loading()); } - private IEnumerable Loading() + private IEnumerable Loading() { SubmarineInfo subInfo = new SubmarineInfo(submarinePath); @@ -259,7 +259,7 @@ namespace Barotrauma.Tutorials base.Stop(); } - private IEnumerable Dead() + private IEnumerable Dead() { GUI.PreventPauseMenuToggle = true; Character.Controlled = character = null; @@ -279,7 +279,7 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Success; } - protected IEnumerable TutorialCompleted() + protected IEnumerable TutorialCompleted() { GUI.PreventPauseMenuToggle = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 989889b19..87f3c404e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -247,7 +247,7 @@ namespace Barotrauma.Tutorials } } - public virtual IEnumerable UpdateState() + public virtual IEnumerable UpdateState() { yield return CoroutineStatus.Success; } @@ -470,7 +470,7 @@ namespace Barotrauma.Tutorials CoroutineManager.StartCoroutine(WaitForObjectiveEnd(segment)); } - private IEnumerable WaitForObjectiveEnd(TutorialSegment objective) + private IEnumerable WaitForObjectiveEnd(TutorialSegment objective) { yield return new WaitForSeconds(2.0f); objectiveFrame.RemoveChild(objective.ReplayButton); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index ac23b8a31..d9ec85f14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -24,7 +24,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { //don't consider the items to belong in the outpost to prevent the stealing icon from showing - item.SpawnedInOutpost = false; + item.SpawnedInCurrentOutpost = false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index b5246980c..007bef266 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -106,7 +106,8 @@ namespace Barotrauma respawnButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnInfoFrame.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) { AbsoluteSpacing = HUDLayoutSettings.Padding, - Stretch = true + Stretch = true, + Visible = false }; respawnTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, respawnButtonContainer.RectTransform, Anchor.Center), TextManager.Get("respawnquestionpromptrespawn")) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index b58477409..0112281b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -133,7 +133,7 @@ namespace Barotrauma string hintIdentifierBase = "onstartedinteracting"; // onstartedinteracting.brokenitem - if (item.Repairables.Any(r => item.ConditionPercentage < r.RepairThreshold)) + if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { if (DisplayHint($"{hintIdentifierBase}.brokenitem")) { return; } } @@ -192,7 +192,7 @@ namespace Barotrauma if (!CanDisplayHints(requireGameScreen: false, requireControllingCharacter: false)) { return; } CoroutineManager.StartCoroutine(DisplayRoundStartedHints(initRoundHandle), "HintManager.DisplayRoundStartedHints"); - static IEnumerable InitRound() + 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 @@ -211,7 +211,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - static IEnumerable DisplayRoundStartedHints(CoroutineHandle initRoundHandle) + static IEnumerable DisplayRoundStartedHints(CoroutineHandle initRoundHandle) { while (GameMain.Instance.LoadingScreenOpen || Screen.Selected != GameMain.GameScreen || CoroutineManager.IsCoroutineRunning(initRoundHandle) || diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 49b528e7d..7260479fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -480,7 +480,7 @@ namespace Barotrauma "\n" + string.Join("\n", contentPackage.ErrorMessages); } } - contentPackageList.CanDragElements = CanHotswapPackages(false); + contentPackageList.CurrentDragMode = CanHotswapPackages(false) ? GUIListBox.DragMode.DragWithinBox : GUIListBox.DragMode.NoDragging; contentPackageList.CanBeFocused = CanHotswapPackages(false); contentPackageList.OnRearranged = OnContentPackagesRearranged; @@ -1767,7 +1767,7 @@ namespace Barotrauma return true; } - private IEnumerable WaitForKeyPress(GUITextBox keyBox, KeyOrMouse[] keyArray) + private IEnumerable WaitForKeyPress(GUITextBox keyBox, KeyOrMouse[] keyArray) { yield return CoroutineStatus.Running; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 9257d4209..0fe573f07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -949,16 +949,17 @@ namespace Barotrauma //player has selected the inventory of another item -> attempt to move the item there return QuickUseAction.PutToContainer; } - else if (character.SelectedCharacter != null && - character.SelectedCharacter.Inventory != null && + else if (character.SelectedCharacter?.Inventory != null && !character.SelectedCharacter.Inventory.Locked && allowInventorySwap) { //player has selected the inventory of another character -> attempt to move the item there return QuickUseAction.PutToCharacter; } - else if (character.SelectedBy != null && Character.Controlled == character.SelectedBy && - character.SelectedBy.Inventory != null && !character.SelectedBy.Inventory.Locked && allowInventorySwap) + else if (character.SelectedBy?.Inventory != null && + Character.Controlled == character.SelectedBy && + !character.SelectedBy.Inventory.Locked && + allowInventorySwap) { return QuickUseAction.TakeFromCharacter; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index b6eca6599..0a3f1ddb7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components float sizeMultiplier = Math.Clamp(chargeRatio, 0.1f, 1f); foreach (ParticleEmitter emitter in particleEmitterCharges) { - emitter.Emit(deltaTime, particlePos, hullGuess: null, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier); + emitter.Emit(deltaTime, particlePos, hullGuess: item.CurrentHull, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier); } if (chargeSoundChannel == null || !chargeSoundChannel.IsPlaying) @@ -157,6 +157,14 @@ namespace Barotrauma.Items.Components crosshairSprite?.Draw(spriteBatch, crosshairPos, Color.White, 0, currentCrossHairScale); crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, currentCrossHairPointerScale); } + + if (GameMain.DebugDraw) + { + Vector2 barrelPos = item.DrawPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); + barrelPos = Screen.Selected.Cam.WorldToScreen(barrelPos); + GUI.DrawLine(spriteBatch, barrelPos - Vector2.UnitY * 3, barrelPos + Vector2.UnitY * 3, Color.Red); + GUI.DrawLine(spriteBatch, barrelPos - Vector2.UnitX * 3, barrelPos + Vector2.UnitX * 3, Color.Red); + } } partial void LaunchProjSpecific() @@ -166,7 +174,7 @@ namespace Barotrauma.Items.Components if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } foreach (ParticleEmitter emitter in particleEmitters) { - emitter.Emit(1.0f, particlePos, hullGuess: null, angle: rotation, particleRotation: rotation); + emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: rotation); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index cb663cdcf..f68a28184 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -576,7 +576,7 @@ namespace Barotrauma.Items.Components delayedCorrectionCoroutine = CoroutineManager.StartCoroutine(DoDelayedCorrection(type, buffer, sendingTime, waitForMidRoundSync)); } - private IEnumerable DoDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync) + private IEnumerable DoDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync) { while (GameMain.Client != null && (correctionTimer > 0.0f || (waitForMidRoundSync && GameMain.Client.MidRoundSyncing))) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index bff562d65..ec2074a32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -14,6 +14,8 @@ namespace Barotrauma.Items.Components private CoroutineHandle resetPredictionCoroutine; private float resetPredictionTimer; + private float currentBrightness; + public Vector2 DrawSize { get { return new Vector2(Light.Range * 2, Light.Range * 2); } @@ -31,12 +33,40 @@ namespace Barotrauma.Items.Components { if (Light == null) { return; } Light.Enabled = enabled; + currentBrightness = brightness; if (enabled) { Light.Color = LightColor.Multiply(brightness); } } + partial void SetLightSourceTransform() + { + if (ParentBody != null) + { + Light.Position = ParentBody.Position; + } + else if (turret != null) + { + Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y); + } + else + { + Light.Position = item.Position; + } + PhysicsBody body = ParentBody ?? item.body; + if (body != null) + { + Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; + Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically; + } + else + { + Light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation); + Light.LightSpriteEffect = item.SpriteEffects; + } + } + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (Light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) @@ -71,7 +101,7 @@ namespace Barotrauma.Items.Components /// /// Reset client-side prediction of the light's state to the last known state sent by the server after resetPredictionTimer runs out /// - private IEnumerable ResetPredictionAfterDelay() + private IEnumerable ResetPredictionAfterDelay() { while (resetPredictionTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 6e6fbe63e..a5901c440 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -259,7 +259,9 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - SetActive(msg.ReadBoolean()); + ushort userID = msg.ReadUInt16(); + Character user = userID == Entity.NullEntityID ? null : Entity.FindEntityByID(userID) as Character; + SetActive(msg.ReadBoolean(), user); progressTimer = msg.ReadSingle(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 7da3659a6..e839046dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -329,8 +329,6 @@ namespace Barotrauma.Items.Components var missingCounts = missingItems.GroupBy(missingItem => missingItem).ToDictionary(x => x.Key, x => x.Count()); missingItems = missingItems.Distinct().ToList(); - var availableIngredients = GetAvailableIngredients(); - foreach (FabricationRecipe.RequiredItem requiredItem in missingItems) { while (slotIndex < inputContainer.Capacity && inputContainer.Inventory.GetItemAt(slotIndex) != null) @@ -341,23 +339,23 @@ namespace Barotrauma.Items.Components requiredItem.ItemPrefabs .Where(requiredPrefab => availableIngredients.ContainsKey(requiredPrefab.Identifier)) .ForEach(requiredPrefab => { - var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; - - availablePrefabs - .Where(availablePrefab => availablePrefab.ParentInventory != inputContainer.Inventory) - .Where(availablePrefab => availablePrefab.ParentInventory.visualSlots != null) //slots are null if the inventory has never been displayed - .ForEach(availablePrefab => { //(linked item, but the UI is not set to be displayed at the same time) - int availableSlotIndex = availablePrefab.ParentInventory.FindIndex(availablePrefab); - - if (availablePrefab.ParentInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) + var availableItems = availableIngredients[requiredPrefab.Identifier]; + foreach (Item it in availableItems) + { + if (it.ParentInventory == inputContainer.Inventory) { continue; } + var rootContainer = it.GetRootContainer(); + if (rootContainer?.OwnInventory?.visualSlots == null) { continue; } + int availableSlotIndex = rootContainer.OwnInventory.FindIndex(it.Container == rootContainer ? it : it.Container); + if (availableSlotIndex < 0) { continue; } + if (rootContainer.OwnInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) + { + rootContainer.OwnInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + if (slotIndex < inputContainer.Capacity) { - availablePrefab.ParentInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); - if (slotIndex < inputContainer.Capacity) - { - inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); - } + inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); } - }); + } + } }); if (slotIndex >= inputContainer.Capacity) { break; } @@ -676,7 +674,17 @@ namespace Barotrauma.Items.Components activateButton.Enabled = false; inSufficientPowerWarning.Visible = currPowerConsumption > 0 && !hasPower; - var availableIngredients = GetAvailableIngredients(); + if (!IsActive) + { + //only check ingredients if the fabricator isn't active (if it is, this is done in Update) + if (refreshIngredientsTimer <= 0.0f) + { + RefreshAvailableIngredients(); + refreshIngredientsTimer = RefreshIngredientsInterval; + } + refreshIngredientsTimer -= deltaTime; + } + if (character != null) { foreach (GUIComponent child in itemList.Content.Children) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 5e0ff3193..85629e383 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -183,6 +183,7 @@ namespace Barotrauma.Items.Components private ImmutableDictionary electricalMapComponents; private ImmutableDictionary electricalChildren; private ImmutableDictionary doorChildren; + private ImmutableDictionary weaponChildren; private ImmutableHashSet? itemsFoundOnSub; @@ -366,8 +367,8 @@ namespace Barotrauma.Items.Components hullInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), style: "GUIToolTip") { - CanBeFocused = false - + CanBeFocused = false, + Visible = false }; var hullInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), hullInfoFrame.RectTransform, Anchor.Center)) @@ -431,7 +432,7 @@ namespace Barotrauma.Items.Components scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && it.GetComponent() != null).ToImmutableHashSet(); + ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent() != null || it.GetComponent() != null)).ToImmutableHashSet(); miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents); IEnumerable electrialPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); @@ -460,29 +461,63 @@ namespace Barotrauma.Items.Components electricalChildren = electricChildren.ToImmutableDictionary(); Dictionary doorChilds = new Dictionary(); + Dictionary weaponChilds = new Dictionary(); foreach (var (entity, component) in hullStatusComponents) { if (!hullPointsOfInterest.Contains(entity)) { continue; } - const int minSize = 8; + if (!(entity is Item it)) { continue; } const int borderMaxSize = 2; - Point size = component.BorderComponent.Rect.Size; - - size.X = Math.Max(size.X, minSize); - size.Y = Math.Max(size.Y, minSize); - float width = Math.Min(borderMaxSize, Math.Min(size.X, size.Y) / 8f); - - GUIFrame frame = new GUIFrame(new RectTransform(size, component.RectComponent.RectTransform, anchor: Anchor.Center), style: "ScanLines", color: DoorIndicatorColor) + if (it.GetComponent() is { }) { - OutlineColor = GUI.Style.Green, - OutlineThickness = width - }; - doorChilds.Add(component, frame); + const int minSize = 8; + + Point size = component.BorderComponent.Rect.Size; + + size.X = Math.Max(size.X, minSize); + size.Y = Math.Max(size.Y, minSize); + float width = Math.Min(borderMaxSize, Math.Min(size.X, size.Y) / 8f); + + GUIFrame frame = new GUIFrame(new RectTransform(size, component.RectComponent.RectTransform, anchor: Anchor.Center), style: "ScanLines", color: DoorIndicatorColor) + { + OutlineColor = DoorIndicatorColor, + OutlineThickness = width + }; + doorChilds.Add(component, frame); + } + else if (it.GetComponent() is { } turret) + { + int parentWidth = (int) (submarineContainer.Rect.Width / 16f); + GUICustomComponent frame = new GUICustomComponent(new RectTransform(new Point(parentWidth, parentWidth), component.RectComponent.RectTransform, anchor: Anchor.Center), (batch, customComponent) => + { + Vector2 center = customComponent.Center; + float rotation = turret.Rotation; + + if (!hasPower) + { + float minRotation = MathHelper.ToRadians(Math.Min(turret.RotationLimits.X, turret.RotationLimits.Y)), + maxRotation = MathHelper.ToRadians(Math.Max(turret.RotationLimits.X, turret.RotationLimits.Y)); + + rotation = (minRotation + maxRotation) / 2; + } + + if (turret.WeaponIndicatorSprite is { } weaponSprite) + { + Vector2 origin = weaponSprite.Origin; + float scale = parentWidth / Math.Max(weaponSprite.size.X, weaponSprite.size.Y); + Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? GUI.Style.Red : GUI.Style.Green; + weaponSprite.Draw(batch, center, color, origin, rotation, scale, it.SpriteEffects); + } + }); + + weaponChilds.Add(component, frame); + } } doorChildren = doorChilds.ToImmutableDictionary(); + weaponChildren = weaponChilds.ToImmutableDictionary(); Rectangle parentRect = miniMapFrame.Rect; @@ -655,7 +690,7 @@ namespace Barotrauma.Items.Components worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); foreach (Gap gap in Gap.GapList) { - if (gap.IsRoomToRoom || gap.Submarine != item.Submarine || gap.ConnectedDoor != null) { continue; } + if (gap.IsRoomToRoom || gap.Submarine != item.Submarine || gap.ConnectedDoor != null || gap.HiddenInGame) { continue; } RectangleF entityRect = ScaleRectToUI(gap, miniMapFrame.Rect, worldBorders); Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f; @@ -668,13 +703,33 @@ namespace Barotrauma.Items.Components } } - if (currentMode == MiniMapMode.HullStatus) + if (currentMode == MiniMapMode.HullStatus && hullStatusComponents != null) { foreach (var (entity, component) in hullStatusComponents) { if (!(entity is Hull hull)) { continue; } if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } DrawHullCards(spriteBatch, hull, hullData, component.RectComponent); + + if (item.CurrentHull is { } currentHull && currentHull == hull) + { + Sprite pingCircle = GUI.Style.YouAreHereCircle.Sprite; + if (pingCircle is null) { continue; } + + Vector2 charPos = item.WorldPosition; + Vector2 hullPos = hull.WorldRect.Location.ToVector2(), + hullSize = hull.WorldRect.Size.ToVector2(); + Vector2 relativePos = (charPos - hullPos) / hullSize * component.RectComponent.Rect.Size.ToVector2(); + relativePos.Y = -relativePos.Y; + + float parentWidth = submarineContainer.Rect.Width / 64f; + float spriteSize = pingCircle.size.X * (parentWidth / pingCircle.size.X); + + Vector2 drawPos = component.RectComponent.Rect.Location.ToVector2() + relativePos; + drawPos -= new Vector2(spriteSize, spriteSize) / 2f; + + pingCircle.Draw(spriteBatch, drawPos, GUI.Style.Red * 0.8f, Vector2.Zero, 0f, parentWidth / pingCircle.size.X); + } } } @@ -944,7 +999,7 @@ namespace Barotrauma.Items.Components if (ShowHullIntegrity) { float amount = 1f + hullData.LinkedHulls.Count; - gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom).Sum(g => g.Open) / amount; + gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom && !g.HiddenInGame).Sum(g => g.Open) / amount; borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min(gapOpenSum, 1.0f)); } @@ -1039,10 +1094,9 @@ namespace Barotrauma.Items.Components } else if (it.GetComponent() is { } powerTransfer) { - int current = (int) -powerTransfer.CurrPowerConsumption, - load = (int) powerTransfer.PowerLoad; + int current = (int)-powerTransfer.CurrPowerConsumption, load = (int)powerTransfer.PowerLoad; - line1 = TextManager.GetWithVariable("statusmonitor.junctioncurrent.tooltip", "[amount]", current.ToString()); + line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString(), fallBackTag: "statusmonitor.junctioncurrent.tooltip"); line2 = TextManager.GetWithVariable("statusmonitor.junctionload.tooltip", "[amount]", load.ToString()); } @@ -1086,38 +1140,41 @@ namespace Barotrauma.Items.Components } else { - bool hullsVisible = currentMode == MiniMapMode.HullStatus; + bool hullsVisible = currentMode == MiniMapMode.HullStatus && item.Submarine != null; - foreach (var (entity, component) in hullStatusComponents) + if (hullStatusComponents != null) { - if (!(entity is Hull hull)) { continue; } - if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } - - if (hullData.Distort) { continue; } - - GUIComponent hullFrame = component.RectComponent; - - if (hullsVisible && hullData.HullWaterAmount is { } waterAmount) + foreach (var (entity, component) in hullStatusComponents) { - if (hullFrame.Rect.Height * waterAmount > 3.0f) + if (!(entity is Hull hull)) { continue; } + if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } + + if (hullData.Distort) { continue; } + + GUIComponent hullFrame = component.RectComponent; + + if (hullsVisible && hullData.HullWaterAmount is { } waterAmount) { - RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount); - - const float width = 1f; - - GUI.DrawFilledRectangle(spriteBatch, waterRect, HullWaterColor); - - if (!MathUtils.NearlyEqual(waterAmount, 1.0f)) + if (hullFrame.Rect.Height * waterAmount > 3.0f) { - Vector2 offset = new Vector2(0, width); - GUI.DrawLine(spriteBatch, waterRect.Location + offset, new Vector2(waterRect.Right, waterRect.Y) + offset, HullWaterLineColor, width: width); + RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount); + + const float width = 1f; + + GUI.DrawFilledRectangle(spriteBatch, waterRect, HullWaterColor); + + if (!MathUtils.NearlyEqual(waterAmount, 1.0f)) + { + Vector2 offset = new Vector2(0, width); + GUI.DrawLine(spriteBatch, waterRect.Location + offset, new Vector2(waterRect.Right, waterRect.Y) + offset, HullWaterLineColor, width: width); + } } } - } - if (hullsVisible && hullData.HullOxygenAmount is { } oxygenAmount) - { - GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, oxygenAmount / 100.0f), true); + if (hullsVisible && hullData.HullOxygenAmount is { } oxygenAmount) + { + GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, oxygenAmount / 100.0f), true); + } } } } @@ -1221,7 +1278,7 @@ namespace Barotrauma.Items.Components Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); Vector2 origin = new Vector2(sprite.Origin.X * spriteScale.X, sprite.Origin.Y * spriteScale.Y); - if (item.GetComponent() is { } turret) + if (!item.Prefab.ShowInStatusMonitor && item.GetComponent() is { } turret) { Vector2 drawPos = turret.GetDrawPos(); drawPos.Y = -drawPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 275ffa13a..aa5e644f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), columnLeft.RectTransform), style: "HorizontalLine"); float relativeYMargin = 0.02f; - Vector2 relativeTextSize = new Vector2(0.9f, 0.2f); + Vector2 relativeTextSize = new Vector2(0.9f, 0.15f); Vector2 sliderSize = new Vector2(1.0f, 0.125f); Vector2 meterSize = new Vector2(1, 1 - relativeTextSize.Y - relativeYMargin - sliderSize.Y - 0.1f); @@ -198,7 +198,7 @@ namespace Barotrauma.Items.Components FissionRateScrollBar = new GUIScrollBar(new RectTransform(sliderSize, leftArea.RectTransform, Anchor.TopCenter) { - RelativeOffset = new Vector2(0, fissionMeter.RectTransform.RelativeOffset.Y + meterSize.Y) + RelativeOffset = new Vector2(0, fissionMeter.RectTransform.RelativeOffset.Y + meterSize.Y + relativeYMargin) }, style: "DeviceSlider", barSize: 0.15f) { @@ -208,7 +208,7 @@ namespace Barotrauma.Items.Components { LastUser = Character.Controlled; unsentChanges = true; - targetFissionRate = scrollAmount * 100.0f; + TargetFissionRate = scrollAmount * 100.0f; return false; } @@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components TurbineOutputScrollBar = new GUIScrollBar(new RectTransform(sliderSize, rightArea.RectTransform, Anchor.TopCenter) { - RelativeOffset = new Vector2(0, turbineMeter.RectTransform.RelativeOffset.Y + meterSize.Y) + RelativeOffset = new Vector2(0, turbineMeter.RectTransform.RelativeOffset.Y + meterSize.Y + relativeYMargin) }, style: "DeviceSlider", barSize: 0.15f, isHorizontal: true) { @@ -226,7 +226,7 @@ namespace Barotrauma.Items.Components { LastUser = Character.Controlled; unsentChanges = true; - targetTurbineOutput = scrollAmount * 100.0f; + TargetTurbineOutput = scrollAmount * 100.0f; return false; } @@ -370,7 +370,7 @@ namespace Barotrauma.Items.Components }; string loadStr = TextManager.Get("ReactorLoad"); string kW = TextManager.Get("kilowatt"); - loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)load).ToString())} {kW}"; + loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}"; var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed"); new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null); @@ -387,8 +387,8 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; - FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; + TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f; + FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f; var itemContainer = item.GetComponent(); if (itemContainer != null) { @@ -462,7 +462,7 @@ namespace Barotrauma.Items.Components if (graphTimer > updateGraphInterval) { UpdateGraph(outputGraph, -currPowerConsumption); - UpdateGraph(loadGraph, load); + UpdateGraph(loadGraph, Load); graphTimer = 0.0f; } @@ -487,7 +487,7 @@ namespace Barotrauma.Items.Components float jitter = 0.0f; if (FissionRate > allowedFissionRate.Y - 5.0f) { - float jitterAmount = Math.Min(targetFissionRate - allowedFissionRate.Y, 10.0f); + float jitterAmount = Math.Min(TargetFissionRate - allowedFissionRate.Y, 10.0f); float t = graphTimer / updateGraphInterval; jitter = (PerlinNoise.GetPerlin(t * 0.5f, t * 0.1f) - 0.5f) * jitterAmount; @@ -525,12 +525,12 @@ namespace Barotrauma.Items.Components criticalHeatWarning.Selected = temperature > allowedTemperature.Y && lightOn; lowTemperatureWarning.Selected = temperature < allowedTemperature.X && lightOn; - criticalOutputWarning.Selected = -currPowerConsumption > load * 1.5f && lightOn; + criticalOutputWarning.Selected = -currPowerConsumption > Load * 1.5f && lightOn; warningButtons["ReactorWarningOverheating"].Selected = temperature > optimalTemperature.Y && lightOn; - warningButtons["ReactorWarningHighOutput"].Selected = -currPowerConsumption > load * 1.1f && lightOn; + warningButtons["ReactorWarningHighOutput"].Selected = -currPowerConsumption > Load * 1.1f && lightOn; warningButtons["ReactorWarningLowTemp"].Selected = temperature < optimalTemperature.X && lightOn; - warningButtons["ReactorWarningLowOutput"].Selected = -currPowerConsumption < load * 0.9f && lightOn; + warningButtons["ReactorWarningLowOutput"].Selected = -currPowerConsumption < Load * 0.9f && lightOn; warningButtons["ReactorWarningFuelOut"].Selected = prevAvailableFuel < fissionRate * 0.01f && lightOn; warningButtons["ReactorWarningLowFuel"].Selected = prevAvailableFuel < fissionRate && lightOn; warningButtons["ReactorWarningMeltdown"].Selected = meltDownTimer > MeltdownDelay * 0.5f || item.Condition == 0.0f && lightOn; @@ -571,12 +571,12 @@ namespace Barotrauma.Items.Components unsentChanges = true; if (input.X != 0.0f && GUIScrollBar.DraggingBar != FissionRateScrollBar) { - targetFissionRate = MathHelper.Clamp(targetFissionRate + input.X, 0.0f, 100.0f); + TargetFissionRate = MathHelper.Clamp(TargetFissionRate + input.X, 0.0f, 100.0f); FissionRateScrollBar.BarScroll += input.X / 100.0f; } if (input.Y != 0.0f && GUIScrollBar.DraggingBar != TurbineOutputScrollBar) { - targetTurbineOutput = MathHelper.Clamp(targetTurbineOutput + input.Y, 0.0f, 100.0f); + TargetTurbineOutput = MathHelper.Clamp(TargetTurbineOutput + input.Y, 0.0f, 100.0f); TurbineOutputScrollBar.BarScroll += input.Y / 100.0f; } } @@ -596,7 +596,7 @@ namespace Barotrauma.Items.Components MathHelper.Clamp((allowedRange.X - range.X) / (range.Y - range.X), 0.0f, 0.95f), MathHelper.Clamp((allowedRange.Y - range.X) / (range.Y - range.X), 0.0f, 1.0f)); - Vector2 sectorRad = new Vector2(-1.57f, 1.57f); + Vector2 sectorRad = new Vector2(-1.35f, 1.35f); Vector2 optimalSectorRad = new Vector2( MathHelper.Lerp(sectorRad.X, sectorRad.Y, optimalRangeNormalized.X), @@ -606,23 +606,25 @@ namespace Barotrauma.Items.Components MathHelper.Lerp(sectorRad.X, sectorRad.Y, allowedRangeNormalized.X), MathHelper.Lerp(sectorRad.X, sectorRad.Y, allowedRangeNormalized.Y)); + Vector2 pointerPos = pos - new Vector2(0, 30) * scale; + if (optimalRangeNormalized.X == optimalRangeNormalized.Y) { - sectorSprite.Draw(spriteBatch, pos, GUI.Style.Red, MathHelper.PiOver2, scale); + sectorSprite.Draw(spriteBatch, pointerPos, GUI.Style.Red, MathHelper.PiOver2, scale); } else { spriteBatch.End(); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; - spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, 0, GameMain.GraphicsWidth, (int)(pos.Y + (meterSprite.size.Y - meterSprite.Origin.Y) * scale) - 3); + spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, 0, GameMain.GraphicsWidth, (int)(pointerPos.Y + (meterSprite.size.Y - meterSprite.Origin.Y) * scale) - 3); spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); float scaleMultiplier = 0.95f; - sectorSprite.Draw(spriteBatch, pos, optimalRangeColor, MathHelper.PiOver2 + (allowedSectorRad.X + allowedSectorRad.Y) / 2.0f, scale * scaleMultiplier); - sectorSprite.Draw(spriteBatch, pos, offRangeColor, optimalSectorRad.X, scale * scaleMultiplier); - sectorSprite.Draw(spriteBatch, pos, warningColor, allowedSectorRad.X, scale * scaleMultiplier); - sectorSprite.Draw(spriteBatch, pos, offRangeColor, MathHelper.Pi + optimalSectorRad.Y, scale * scaleMultiplier); - sectorSprite.Draw(spriteBatch, pos, warningColor, MathHelper.Pi + allowedSectorRad.Y, scale * scaleMultiplier); + sectorSprite.Draw(spriteBatch, pointerPos, optimalRangeColor, MathHelper.PiOver2 + (allowedSectorRad.X + allowedSectorRad.Y) / 2.0f, scale * scaleMultiplier); + sectorSprite.Draw(spriteBatch, pointerPos, offRangeColor, optimalSectorRad.X, scale * scaleMultiplier); + sectorSprite.Draw(spriteBatch, pointerPos, warningColor, allowedSectorRad.X, scale * scaleMultiplier); + sectorSprite.Draw(spriteBatch, pointerPos, offRangeColor, MathHelper.Pi + optimalSectorRad.Y, scale * scaleMultiplier); + sectorSprite.Draw(spriteBatch, pointerPos, warningColor, MathHelper.Pi + allowedSectorRad.Y, scale * scaleMultiplier); spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; @@ -634,7 +636,7 @@ namespace Barotrauma.Items.Components float normalizedValue = (value - range.X) / (range.Y - range.X); float valueRad = MathHelper.Lerp(sectorRad.X, sectorRad.Y, normalizedValue); Vector2 offset = new Vector2(0, 40) * scale; - meterPointer.Draw(spriteBatch, pos - offset, valueRad, scale); + meterPointer.Draw(spriteBatch, pointerPos, valueRad, scale); } static void UpdateGraph(IList graph, T newValue) @@ -713,8 +715,8 @@ namespace Barotrauma.Items.Components { msg.Write(autoTemp); msg.Write(PowerOn); - msg.WriteRangedSingle(targetFissionRate, 0.0f, 100.0f, 8); - msg.WriteRangedSingle(targetTurbineOutput, 0.0f, 100.0f, 8); + msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); + msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); correctionTimer = CorrectionDelay; } @@ -730,17 +732,17 @@ namespace Barotrauma.Items.Components AutoTemp = msg.ReadBoolean(); PowerOn = msg.ReadBoolean(); Temperature = msg.ReadRangedSingle(0.0f, 100.0f, 8); - targetFissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8); - targetTurbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); + TargetFissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8); + TargetTurbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); degreeOfSuccess = msg.ReadRangedSingle(0.0f, 1.0f, 8); - if (Math.Abs(FissionRateScrollBar.BarScroll - targetFissionRate / 100.0f) > 0.01f) + if (Math.Abs(FissionRateScrollBar.BarScroll - TargetFissionRate / 100.0f) > 0.01f) { - FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; + FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f; } - if (Math.Abs(TurbineOutputScrollBar.BarScroll - targetTurbineOutput / 100.0f) > 0.01f) + if (Math.Abs(TurbineOutputScrollBar.BarScroll - TargetTurbineOutput / 100.0f) > 0.01f) { - TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; + TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f; } IsActive = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 44d0ad13c..74b61c33a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -456,7 +456,7 @@ namespace Barotrauma.Items.Components } } - float distort = 1.0f - item.Condition / item.MaxCondition; + float distort = MathHelper.Clamp(1.0f - item.Condition / item.MaxCondition, 0.0f, 1.0f); for (int i = sonarBlips.Count - 1; i >= 0; i--) { sonarBlips[i].FadeTimer -= deltaTime * MathHelper.Lerp(0.5f, 2.0f, distort); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index fbf133ced..23d3a6839 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -56,7 +56,9 @@ namespace Barotrauma.Items.Components if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } float maxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(character); - if (item.Condition / maxRepairConditionMultiplier < RepairThreshold) { return true; } + float defaultMaxCondition = item.MaxCondition / maxRepairConditionMultiplier; + + if (MathUtils.Percentage(item.Condition, defaultMaxCondition) < RepairThreshold) { return true; } if (CurrentFixer == character) { @@ -280,14 +282,14 @@ namespace Barotrauma.Items.Components progressBarOverlayText.Visible = false; } - RepairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && !item.IsFullCondition && item.ConditionPercentage < RepairThreshold; - RepairButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Repair) ? - repairButtonText : + RepairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && !item.IsFullCondition && IsBelowRepairThreshold; + RepairButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Repair) ? + repairButtonText : repairingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); SabotageButton.Visible = character.IsTraitor; SabotageButton.IgnoreLayoutGroups = !SabotageButton.Visible; - SabotageButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Sabotage)) && character.IsTraitor && item.ConditionPercentage > MinSabotageCondition; + SabotageButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Sabotage)) && character.IsTraitor && IsBelowRepairThreshold; SabotageButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Sabotage || !character.IsTraitor) ? sabotageButtonText : sabotagingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index a816f402a..1f425fca0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); - inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: Color.LimeGreen) + inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) { MaxTextLength = MaxMessageLength, OverflowClip = true, @@ -63,15 +63,15 @@ namespace Barotrauma.Items.Components } OutputValue = input; - ShowOnDisplay(input, addToHistory: true); + ShowOnDisplay(input, addToHistory: true, TextColor); item.SendSignal(input, "signal_out"); } - partial void ShowOnDisplay(string input, bool addToHistory) + partial void ShowOnDisplay(string input, bool addToHistory, Color color) { if (addToHistory) { - messageHistory.Add(input); + messageHistory.Add(new TerminalMessage(input, color)); while (messageHistory.Count > MaxMessages) { messageHistory.RemoveAt(0); @@ -85,7 +85,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), "> " + input, - textColor: Color.LimeGreen, wrap: true, font: UseMonospaceFont ? GUI.MonospacedFont : GUI.GlobalFont) + textColor: color, wrap: true, font: UseMonospaceFont ? GUI.MonospacedFont : GUI.GlobalFont) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs new file mode 100644 index 000000000..503c61912 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class TriggerComponent : ItemComponent, IServerSerializable + { + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + CurrentForceFluctuation = msg.ReadRangedSingle(0.0f, 1.0f, 8); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index f4ba37137..11c827e3a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -14,6 +14,7 @@ namespace Barotrauma.Items.Components partial class Turret : Powered, IDrawableComponent, IServerSerializable { private Sprite crosshairSprite, crosshairPointerSprite; + public Sprite WeaponIndicatorSprite; private GUIProgressBar powerIndicator; @@ -134,6 +135,9 @@ namespace Barotrauma.Items.Components case "crosshair": crosshairSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); break; + case "weaponindicator": + WeaponIndicatorSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + break; case "crosshairpointer": crosshairPointerSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 2b50d7e4a..142b13118 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -101,7 +101,7 @@ namespace Barotrauma private float currentHighlightState, fadeInDuration, fadeOutDuration; private Color currentHighlightColor; - private IEnumerable UpdateBorderHighlight() + private IEnumerable UpdateBorderHighlight() { HighlightTimer = 1.0f; while (currentHighlightState < fadeInDuration + fadeOutDuration) @@ -1255,9 +1255,18 @@ namespace Barotrauma else { bool anySuccess = false; + bool allowCombine = true; + //if we're dragging a stack of partial items or trying to drag to a stack of partial items + //(which should not normally exist, but can happen when e.g. fire damages a stack of items) + //don't allow combining because it leads to weird behavior (stack of items of mixed quality) + if (DraggingItems.Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1 || + selectedInventory.GetItemsAt(slotIndex).Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1) + { + allowCombine = false; + } foreach (Item item in DraggingItems) { - bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, true, Character.Controlled); + bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); anySuccess |= success; if (!success) { break; } } @@ -1673,7 +1682,7 @@ namespace Barotrauma } sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale); - if ((!item.AllowStealing || (inventory != null && inventory.slots[slotIndex].Items.Any(it => !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + if (((item.SpawnedInCurrentOutpost && !item.AllowStealing) || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) { var stealIcon = CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand]; Vector2 iconSize = new Vector2(25 * GUI.Scale); @@ -1818,7 +1827,7 @@ namespace Barotrauma } } - private IEnumerable SyncItemsAfterDelay(UInt16 lastEventID) + private IEnumerable SyncItemsAfterDelay(UInt16 lastEventID) { while (syncItemsDelay > 0.0f || //don't apply inventory updates until diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index eab3b9777..98ae88380 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -175,12 +175,12 @@ namespace Barotrauma } } - float displayCondition = FakeBroken ? 0.0f : condition; + float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; for (int i = 0; i < Prefab.BrokenSprites.Count;i++) { if (Prefab.BrokenSprites[i].FadeIn) { continue; } - float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxCondition : 0.0f; - if (displayCondition <= minCondition || displayCondition <= Prefab.BrokenSprites[i].MaxCondition) + float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f; + if (displayCondition <= minCondition || displayCondition <= Prefab.BrokenSprites[i].MaxConditionPercentage) { activeSprite = Prefab.BrokenSprites[i].Sprite; break; @@ -284,8 +284,8 @@ namespace Barotrauma { if (Prefab.BrokenSprites[i].FadeIn) { - float min = i > 0 ? Prefab.BrokenSprites[i - i].MaxCondition : 0.0f; - float max = Prefab.BrokenSprites[i].MaxCondition; + float min = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f; + float max = Prefab.BrokenSprites[i].MaxConditionPercentage; fadeInBrokenSpriteAlpha = 1.0f - ((displayCondition - min) / (max - min)); if (fadeInBrokenSpriteAlpha > 0.0f && fadeInBrokenSpriteAlpha <= 1.0f) { @@ -293,7 +293,7 @@ namespace Barotrauma } continue; } - if (displayCondition <= Prefab.BrokenSprites[i].MaxCondition) + if (displayCondition <= Prefab.BrokenSprites[i].MaxConditionPercentage) { activeSprite = Prefab.BrokenSprites[i].Sprite; drawOffset = Prefab.BrokenSprites[i].Offset.ToVector2() * Scale; @@ -1632,7 +1632,7 @@ namespace Barotrauma { item = new Item(itemPrefab, pos, sub, id: itemId) { - SpawnedInOutpost = spawnedInOutpost, + SpawnedInCurrentOutpost = spawnedInOutpost, AllowStealing = allowStealing, Quality = quality }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index d81332c48..274eabdf9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -11,7 +11,7 @@ namespace Barotrauma class BrokenItemSprite { //sprite will be rendered if the condition of the item is below this - public readonly float MaxCondition; + public readonly float MaxConditionPercentage; public readonly Sprite Sprite; public readonly bool FadeIn; public readonly Point Offset; @@ -19,7 +19,7 @@ namespace Barotrauma public BrokenItemSprite(Sprite sprite, float maxCondition, bool fadeIn, Point offset) { Sprite = sprite; - MaxCondition = MathHelper.Clamp(maxCondition, 0.0f, 100.0f); + MaxConditionPercentage = MathHelper.Clamp(maxCondition, 0.0f, 100.0f); FadeIn = fadeIn; Offset = offset; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index 070748dfb..20eaf6139 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -95,7 +95,7 @@ namespace Barotrauma MathHelper.Clamp(particlePos.Y, hull.WorldRect.Y - hull.WorldRect.Height, hull.WorldRect.Y)); } - private IEnumerable DimLight(LightSource light) + private IEnumerable DimLight(LightSource light) { float currBrightness = 1.0f; while (light.Color.A > 0.0f && flashDuration > 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index b3b2a653f..f160435ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -438,7 +438,7 @@ namespace Barotrauma for (int i = 0; i < Bodies.Count; i++) { Vector2 pos = FarseerPhysics.ConvertUnits.ToDisplayUnits(Bodies[i].Position); - if (Submarine != null) pos += Submarine.Position; + if (Submarine != null) { pos += Submarine.DrawPosition; } pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, pos, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index 95c090122..9610cddd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -202,6 +202,7 @@ namespace Barotrauma } } GUITextBlock.AutoScaleAndNormalize(parent.Content.GetAllChildren().Where(c => c != submarineNameText && c != descBlock)); + parent.ForceLayoutRecalculation(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 4cd0d5e84..7fd0f8bb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -160,7 +160,7 @@ namespace Barotrauma.Networking public TransferInDelegate OnTransferFailed; private readonly List activeTransfers; - private readonly List> finishedTransfers; + private readonly List<(int transferId, double finishedTime)> finishedTransfers; private readonly Dictionary downloadFolders = new Dictionary() { @@ -176,7 +176,7 @@ namespace Barotrauma.Networking public FileReceiver() { activeTransfers = new List(); - finishedTransfers = new List>(); + finishedTransfers = new List<(int transferId, double finishedTime)>(); } public void ReadMessage(IReadMessage inc) @@ -193,8 +193,8 @@ namespace Barotrauma.Networking case (byte)FileTransferMessageType.Initiate: { byte transferId = inc.ReadByte(); - var existingTransfer = activeTransfers.Find(t => t.ID == transferId); - finishedTransfers.RemoveAll(t => t.First == transferId); + var existingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.EndPointString) && t.ID == transferId); + finishedTransfers.RemoveAll(t => t.transferId == transferId); byte fileType = inc.ReadByte(); //ushort chunkLen = inc.ReadUInt16(); int fileSize = inc.ReadInt32(); @@ -211,7 +211,7 @@ namespace Barotrauma.Networking } else //resend acknowledgement packet { - GameMain.Client.UpdateFileTransfer(transferId, 0); + GameMain.Client.UpdateFileTransfer(transferId, existingTransfer.Received); } return; } @@ -316,14 +316,14 @@ namespace Barotrauma.Networking { byte transferId = inc.ReadByte(); - var activeTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); + var activeTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.EndPointString) && t.ID == transferId); if (activeTransfer == null) { //it's possible for the server to send some extra data //before it acknowledges that the download is finished, //so let's suppress the error message in that case - finishedTransfers.RemoveAll(t => t.Second + 5.0 < Timing.TotalTime); - if (!finishedTransfers.Any(t => t.First == transferId)) + finishedTransfers.RemoveAll(t => t.finishedTime + 5.0 < Timing.TotalTime); + if (!finishedTransfers.Any(t => t.transferId == transferId)) { GameMain.Client.CancelFileTransfer(transferId); DebugConsole.ThrowError("File transfer error: received data without a transfer initiation message"); @@ -373,7 +373,7 @@ namespace Barotrauma.Networking if (ValidateReceivedData(activeTransfer, out string errorMessage)) { - finishedTransfers.Add(new Pair(transferId, Timing.TotalTime)); + finishedTransfers.Add((transferId, Timing.TotalTime)); StopTransfer(activeTransfer); Md5Hash.RemoveFromCache(activeTransfer.FilePath); OnFinished(activeTransfer); @@ -391,7 +391,7 @@ namespace Barotrauma.Networking case (byte)FileTransferMessageType.Cancel: { byte transferId = inc.ReadByte(); - var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); + var matchingTransfer = activeTransfers.Find(t => t.Connection.EndpointMatches(t.Connection.EndPointString) && t.ID == transferId); if (matchingTransfer != null) { new GUIMessageBox("File transfer cancelled", "The server has cancelled the transfer of the file \"" + matchingTransfer.FileName + "\"."); @@ -528,7 +528,7 @@ namespace Barotrauma.Networking transfer.Status = FileTransferStatus.Canceled; } - if (activeTransfers.Contains(transfer)) activeTransfers.Remove(transfer); + if (activeTransfers.Contains(transfer)) { activeTransfers.Remove(transfer); } transfer.Dispose(); if (deleteFile && File.Exists(transfer.FilePath)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 85267e5a7..0b7d8e563 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -460,7 +460,7 @@ namespace Barotrauma.Networking private bool wrongPassword; // Before main looping starts, we loop here and wait for approval message - private IEnumerable WaitForStartingInfo() + private IEnumerable WaitForStartingInfo() { GUI.SetCursorWaiting(); requiresPw = false; @@ -861,8 +861,8 @@ namespace Barotrauma.Networking if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) { //waiting for a save file - if (campaign != null && - campaign.PendingSaveID > campaign.LastSaveID && + if (campaign != null && + NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) && fileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave)) { return; @@ -872,6 +872,7 @@ namespace Barotrauma.Networking break; case ServerPacketHeader.ENDGAME: CampaignMode.TransitionType transitionType = (CampaignMode.TransitionType)inc.ReadByte(); + bool save = inc.ReadBoolean(); string endMessage = string.Empty; endMessage = inc.ReadString(); @@ -905,6 +906,7 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Interrupted; CoroutineManager.StartCoroutine(EndGame(endMessage, traitorResults, transitionType), "EndGame"); + GUI.SetSavingIndicatorState(save); break; case ServerPacketHeader.CAMPAIGN_SETUP_INFO: UInt16 saveCount = inc.ReadUInt16(); @@ -1236,7 +1238,7 @@ namespace Barotrauma.Networking } } - private IEnumerable WaitInServerQueue() + private IEnumerable WaitInServerQueue() { waitInServerQueueBox = new GUIMessageBox( TextManager.Get("ServerQueuePleaseWait"), @@ -1424,7 +1426,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RefreshEnabledElements(); } - private IEnumerable StartGame(IReadMessage inc) + private IEnumerable StartGame(IReadMessage inc) { Character?.Remove(); Character = null; @@ -1562,31 +1564,47 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.CrewManager != null) { GameMain.GameSession.CrewManager.Reset(); } byte campaignID = inc.ReadByte(); + UInt16 campaignSaveID = inc.ReadUInt16(); int nextLocationIndex = inc.ReadInt32(); int nextConnectionIndex = inc.ReadInt32(); int selectedLocationIndex = inc.ReadInt32(); bool mirrorLevel = inc.ReadBoolean(); - if (campaign.CampaignID != campaignID) { - string errorMsg = "Failed to start campaign round (campaign ID does not match)."; gameStarted = true; - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError("Failed to start campaign round (campaign ID does not match)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } else if (campaign.Map == null) { - string errorMsg = "Failed to start campaign round (campaign map not loaded yet)."; gameStarted = true; - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); GameMain.NetLobbyScreen.Select(); roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } + if (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) + { + campaign.PendingSaveID = campaignSaveID; + DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0,0,60); + while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID)) + { + if (DateTime.Now > saveFileTimeOut) + { + gameStarted = true; + DebugConsole.ThrowError("Failed to start campaign round (timed out while waiting for the up-to-date save file)."); + GameMain.NetLobbyScreen.Select(); + roundInitStatus = RoundInitStatus.Interrupted; + yield return CoroutineStatus.Failure; + } + yield return new WaitForSeconds(0.1f); + } + } + campaign.Map.SelectLocation(selectedLocationIndex); LevelData levelData = nextLocationIndex > -1 ? @@ -1665,10 +1683,6 @@ namespace Barotrauma.Networking } if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } - - clientPeer.Update((float)Timing.Step); - - if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } } catch (Exception e) { @@ -1744,7 +1758,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - public IEnumerable EndGame(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public IEnumerable EndGame(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { //round starting up, wait for it to finish DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 60); @@ -1854,8 +1868,7 @@ namespace Barotrauma.Networking GameMain.GameSession.OwnedSubmarines = new List(); for (int i = 0; i < ownedIndexes.Length; i++) { - int index; - if (int.TryParse(ownedIndexes[i], out index)) + if (int.TryParse(ownedIndexes[i], out int index)) { SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "owned")) @@ -2670,8 +2683,11 @@ namespace Barotrauma.Networking public override void CreateEntityEvent(INetSerializable entity, object[] extraData) { - if (!(entity is IClientSerializable)) throw new InvalidCastException("Entity is not IClientSerializable"); - entityEventManager.CreateEvent(entity as IClientSerializable, extraData); + if (!(entity is IClientSerializable clientSerializable)) + { + throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); + } + entityEventManager.CreateEvent(clientSerializable, extraData); } public bool HasPermission(ClientPermissions permission) @@ -3013,12 +3029,13 @@ namespace Barotrauma.Networking /// /// Tell the server to end the round (permission required) /// - public void RequestRoundEnd() + public void RequestRoundEnd(bool save) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.ManageRound); msg.Write(true); //indicates round end + msg.Write(save); clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3321,7 +3338,9 @@ namespace Barotrauma.Networking if (respawnManager.RespawnCountdownStarted) { float timeLeft = (float)(respawnManager.RespawnTime - DateTime.Now).TotalSeconds; - respawnText = TextManager.GetWithVariable(respawnManager.UsingShuttle ? "RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); + respawnText = TextManager.GetWithVariable( + respawnManager.UsingShuttle && !respawnManager.ForceSpawnInMainSub ? + "RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); } else if (respawnManager.PendingRespawnCount > 0) { @@ -3437,7 +3456,7 @@ namespace Barotrauma.Networking } // Need a delayed selection due to the inputbox being deselected when a left click occurs outside of it - IEnumerable selectCoroutine() + IEnumerable selectCoroutine() { yield return new WaitForSeconds(0.01f, true); chatBox.InputBox.Select(chatBox.InputBox.Text.Length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index 13a4dcebb..e53215e38 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -72,6 +72,9 @@ namespace Barotrauma { CreateLabeledTickBox(parent, nameof(DangerousItemStealBots)); } + CreateLabeledSlider(parent, 0.0f, 30.0f, 0.5f, nameof(DangerousItemContainKarmaDecrease)); + CreateLabeledTickBox(parent, nameof(IsDangerousItemContainKarmaDecreaseIncremental)); + CreateLabeledSlider(parent, 0.0f, 100.0f, 1.0f, nameof(MaxDangerousItemContainKarmaDecrease)); } private void CreateLabeledSlider(GUIComponent parent, float min, float max, float step, string propertyName) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index e0f4462ae..23bcbdeea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -43,24 +43,9 @@ namespace Barotrauma.Networking public void CreateEvent(IClientSerializable entity, object[] extraData = null) { - if (GameMain.Client == null || GameMain.Client.Character == null) return; + if (GameMain.Client?.Character == null) { return; } - if (!(entity is Entity)) - { - DebugConsole.ThrowError("Can't create an entity event for " + entity + "!"); - return; - } - - if (((Entity)entity).Removed) - { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } - if (((Entity)entity).IdFreed) - { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } + if (!ValidateEntity(entity)) { return; } var newEvent = new ClientEntityEvent(entity, (UInt16)(ID + 1)) { @@ -161,7 +146,7 @@ namespace Barotrauma.Networking UInt16 firstEventID = msg.ReadUInt16(); int eventCount = msg.ReadByte(); - + for (int i = 0; i < eventCount; i++) { //16 = entity ID, 8 = msg length @@ -179,7 +164,7 @@ namespace Barotrauma.Networking UInt16 thisEventID = (UInt16)(firstEventID + (UInt16)i); UInt16 entityID = msg.ReadUInt16(); - + if (entityID == Entity.NullEntityID) { if (GameSettings.VerboseLogging) @@ -240,8 +225,11 @@ namespace Barotrauma.Networking if (msg.BitPosition != msgPosition + msgLength * 8) { - string errorMsg = "Message byte position incorrect after reading an event for the entity \"" + entity.ToString() - + "\". Read " + (msg.BitPosition - msgPosition) + " bits, expected message length was " + (msgLength * 8) + " bits."; + var prevEntity = entities.Count >= 2 ? entities[entities.Count - 2] : null; + ushort prevId = prevEntity is Entity p ? p.ID : (ushort)0; + string errorMsg = $"Message byte position incorrect after reading an event for the entity \"{entity}\" (ID {(entity is Entity e ? e.ID : 0)}). " + +$"The previous entity was \"{prevEntity}\" (ID {prevId}) " + +$"Read {msg.BitPosition - msgPosition} bits, expected message length was {msgLength * 8} bits."; DebugConsole.ThrowError(errorMsg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs index eb34d3cdd..e77762455 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs @@ -60,11 +60,11 @@ namespace Barotrauma.Networking { GUI.DrawRectangle(spriteBatch, rect, Color.Black * 0.4f, true); - graphs[(int)NetStatType.ReceivedBytes].Draw(spriteBatch, rect, null, 0.0f, Color.Cyan); - graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Orange); + graphs[(int)NetStatType.ReceivedBytes].Draw(spriteBatch, rect, color: Color.Cyan); + graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, color: GUI.Style.Orange); if (graphs[(int)NetStatType.ResentMessages].Average() > 0) { - graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Red); + graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, color: GUI.Style.Red); GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", new Vector2(rect.Right + 10, rect.Y + 50), GUI.Style.Red); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index cc3910164..596f5c8d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace Barotrauma.Networking { @@ -17,6 +18,11 @@ namespace Barotrauma.Networking get; private set; } + public bool ForceSpawnInMainSub + { + get; private set; + } + partial void UpdateTransportingProjSpecific(float deltaTime) { if (GameMain.Client?.Character == null || GameMain.Client.Character.Submarine != RespawnShuttle) { return; } @@ -30,10 +36,46 @@ namespace Barotrauma.Networking GameMain.Client.AddChatMessage("ServerMessage.ShuttleLeaving", ChatMessageType.Server); } } + + private CoroutineHandle respawnPromptCoroutine; + + public void ShowRespawnPromptIfNeeded(float delay = 5.0f) + { + if (!UseRespawnPrompt) { return; } + if (CoroutineManager.IsCoroutineRunning(respawnPromptCoroutine) || GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "respawnquestionprompt")) + { + return; + } + + respawnPromptCoroutine = CoroutineManager.Invoke(() => + { + if (Character.Controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } + var respawnPrompt = new GUIMessageBox( + TextManager.Get("tutorial.tryagainheader"), TextManager.Get("respawnquestionprompt"), + new string[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) + { + UserData = "respawnquestionprompt" + }; + respawnPrompt.Buttons[0].OnClicked += (btn, userdata) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false); + respawnPrompt.Close(); + return true; + }; + respawnPrompt.Buttons[1].OnClicked += (btn, userdata) => + { + GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true); + respawnPrompt.Close(); + return true; + }; + }, delay: delay); + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { + bool respawnPromptPending = false; var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); - + ForceSpawnInMainSub = false; switch (newState) { case State.Transporting: @@ -46,13 +88,14 @@ namespace Barotrauma.Networking if (CurrentState != newState) { CoroutineManager.StopCoroutines("forcepos"); - //CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos"); } break; case State.Waiting: PendingRespawnCount = msg.ReadUInt16(); RequiredRespawnCount = msg.ReadUInt16(); + respawnPromptPending = msg.ReadBoolean(); RespawnCountdownStarted = msg.ReadBoolean(); + ForceSpawnInMainSub = msg.ReadBoolean(); ResetShuttle(); float newRespawnTime = msg.ReadSingle(); RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(newRespawnTime * 1000.0f)); @@ -63,6 +106,12 @@ namespace Barotrauma.Networking } CurrentState = newState; + if (respawnPromptPending) + { + GameMain.Client.HasSpawned = true; + ShowRespawnPromptIfNeeded(delay: 1.0f); + } + msg.ReadPadBits(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index ee2ffa7c1..2b25e89a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -106,10 +106,10 @@ namespace Barotrauma.Networking public void CreatePreviewWindow(GUIFrame frame) { - frame.ClearChildren(); - if (frame == null) { return; } + frame.ClearChildren(); + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUI.LargeFont) { ToolTip = ServerName diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 07f3a2f9a..4b9497ab8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -141,6 +141,10 @@ namespace Barotrauma.Networking ReadExtraCargo(incMsg); + ReadHiddenSubs(incMsg); + + GameMain.NetLobbyScreen.UpdateSubVisibility(); + Voting.ClientRead(incMsg); bool isAdmin = incMsg.ReadBoolean(); @@ -202,6 +206,11 @@ namespace Barotrauma.Networking Whitelist.ClientAdminWrite(outMsg); } + if (dataToSend.HasFlag(NetFlags.HiddenSubs)) + { + WriteHiddenSubs(outMsg); + } + if (dataToSend.HasFlag(NetFlags.Misc)) { outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); @@ -288,7 +297,7 @@ namespace Barotrauma.Networking }; //center frames - GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.8f), settingsFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 430) }); + GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.85f), settingsFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 430) }); GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), innerFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { Stretch = true, @@ -363,7 +372,7 @@ namespace Barotrauma.Networking selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); selectionFrame.RectTransform.IsFixedSize = true; - GetPropertyData("SubSelectionMode").AssignGUIComponent(selectionMode); + GetPropertyData(nameof(SubSelectionMode)).AssignGUIComponent(selectionMode); // Mode Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection"), font: GUI.SubHeadingFont); @@ -381,7 +390,7 @@ namespace Barotrauma.Networking } selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); selectionFrame.RectTransform.IsFixedSize = true; - GetPropertyData("ModeSelectionMode").AssignGUIComponent(selectionMode); + GetPropertyData(nameof(ModeSelectionMode)).AssignGUIComponent(selectionMode); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), style: "HorizontalLine"); @@ -389,7 +398,7 @@ namespace Barotrauma.Networking var voiceChatEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsVoiceChatEnabled")); - GetPropertyData("VoiceChatEnabled").AssignGUIComponent(voiceChatEnabled); + GetPropertyData(nameof(VoiceChatEnabled)).AssignGUIComponent(voiceChatEnabled); //*********************************************** @@ -407,14 +416,14 @@ namespace Barotrauma.Networking } }; startIntervalSlider.Range = new Vector2(10.0f, 300.0f); - GetPropertyData("AutoRestartInterval").AssignGUIComponent(startIntervalSlider); + GetPropertyData(nameof(AutoRestartInterval)).AssignGUIComponent(startIntervalSlider); startIntervalSlider.OnMoved(startIntervalSlider, startIntervalSlider.BarScroll); //*********************************************** var startWhenClientsReady = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsStartWhenClientsReady")); - GetPropertyData("StartWhenClientsReady").AssignGUIComponent(startWhenClientsReady); + GetPropertyData(nameof(StartWhenClientsReady)).AssignGUIComponent(startWhenClientsReady); CreateLabeledSlider(serverTab, "ServerSettingsStartWhenClientsReadyRatio", out GUIScrollBar slider, out GUITextBlock sliderLabel); string clientsReadyRequiredLabel = sliderLabel.Text; @@ -425,19 +434,19 @@ namespace Barotrauma.Networking ((GUITextBlock)scrollBar.UserData).Text = clientsReadyRequiredLabel.Replace("[percentage]", ((int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f)).ToString()); return true; }; - GetPropertyData("StartWhenClientsReadyRatio").AssignGUIComponent(slider); + GetPropertyData(nameof(StartWhenClientsReadyRatio)).AssignGUIComponent(slider); slider.OnMoved(slider, slider.BarScroll); //*********************************************** var allowSpecBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsAllowSpectating")); - GetPropertyData("AllowSpectating").AssignGUIComponent(allowSpecBox); + GetPropertyData(nameof(AllowSpectating)).AssignGUIComponent(allowSpecBox); var shareSubsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsShareSubFiles")); - GetPropertyData("AllowFileTransfers").AssignGUIComponent(shareSubsBox); + GetPropertyData(nameof(AllowFileTransfers)).AssignGUIComponent(shareSubsBox); var randomizeLevelBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsRandomizeSeed")); - GetPropertyData("RandomizeSeed").AssignGUIComponent(randomizeLevelBox); + GetPropertyData(nameof(RandomizeSeed)).AssignGUIComponent(randomizeLevelBox); var saveLogsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsSaveLogs")) { @@ -448,7 +457,7 @@ namespace Barotrauma.Networking return true; } }; - GetPropertyData("SaveServerLogs").AssignGUIComponent(saveLogsBox); + GetPropertyData(nameof(SaveServerLogs)).AssignGUIComponent(saveLogsBox); //-------------------------------------------------------------------------------- // game settings @@ -480,20 +489,20 @@ namespace Barotrauma.Networking selectionPlayStyle.AddRadioButton((int)playStyle, selectionTick); playStyleTickBoxes.Add(selectionTick); } - GetPropertyData("PlayStyle").AssignGUIComponent(selectionPlayStyle); + GetPropertyData(nameof(PlayStyle)).AssignGUIComponent(selectionPlayStyle); GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); playstyleList.RectTransform.MinSize = new Point(0, (int)(playstyleList.Content.Children.First().Rect.Height * 2.0f + playstyleList.Padding.Y + playstyleList.Padding.W)); var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsEndRoundVoting")); - GetPropertyData("AllowEndVoting").AssignGUIComponent(endVoteBox); + GetPropertyData(nameof(AllowEndVoting)).AssignGUIComponent(endVoteBox); CreateLabeledSlider(roundsTab, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); string endRoundLabel = sliderLabel.Text; slider.Step = 0.2f; slider.Range = new Vector2(0.5f, 1.0f); - GetPropertyData("EndVoteRequiredRatio").AssignGUIComponent(slider); + GetPropertyData(nameof(EndVoteRequiredRatio)).AssignGUIComponent(slider); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { ((GUITextBlock)scrollBar.UserData).Text = endRoundLabel + " " + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; @@ -503,13 +512,13 @@ namespace Barotrauma.Networking var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")); - GetPropertyData("AllowRespawn").AssignGUIComponent(respawnBox); + GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); CreateLabeledSlider(roundsTab, "ServerSettingsRespawnInterval", out slider, out sliderLabel); string intervalLabel = sliderLabel.Text; slider.Range = new Vector2(10.0f, 600.0f); slider.StepValue = 10.0f; - GetPropertyData("RespawnInterval").AssignGUIComponent(slider); + GetPropertyData(nameof(RespawnInterval)).AssignGUIComponent(slider); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { GUITextBlock text = scrollBar.UserData as GUITextBlock; @@ -518,18 +527,26 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - var minRespawnText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), "") + var respawnLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), roundsTab.RectTransform), + isHorizontal: true); + + var minRespawnLayout + = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnLayout.RectTransform)); + + var minRespawnText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), minRespawnLayout.RectTransform), "") { ToolTip = TextManager.Get("ServerSettingsMinRespawnToolTip") }; string minRespawnLabel = TextManager.Get("ServerSettingsMinRespawn") + " "; - CreateLabeledSlider(roundsTab, "", out slider, out sliderLabel); + CreateLabeledSlider(minRespawnLayout, "", out slider, out sliderLabel); + sliderLabel.RectTransform.RelativeSize = Vector2.Zero; + slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); slider.ToolTip = minRespawnText.RawToolTip; slider.UserData = minRespawnText; slider.Step = 0.1f; slider.Range = new Vector2(0.0f, 1.0f); - GetPropertyData("MinRespawnRatio").AssignGUIComponent(slider); + GetPropertyData(nameof(MinRespawnRatio)).AssignGUIComponent(slider); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { ((GUITextBlock)scrollBar.UserData).Text = minRespawnLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; @@ -537,13 +554,18 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, MinRespawnRatio); - var respawnDurationText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), "") + var respawnDurationLayout + = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnLayout.RectTransform)); + + var respawnDurationText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), respawnDurationLayout.RectTransform), "") { ToolTip = TextManager.Get("ServerSettingsRespawnDurationToolTip") }; string respawnDurationLabel = TextManager.Get("ServerSettingsRespawnDuration") + " "; - CreateLabeledSlider(roundsTab, "", out slider, out sliderLabel); + CreateLabeledSlider(respawnDurationLayout, "", out slider, out sliderLabel); + sliderLabel.RectTransform.RelativeSize = Vector2.Zero; + slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); slider.ToolTip = respawnDurationText.RawToolTip; slider.UserData = respawnDurationText; slider.Step = 0.1f; @@ -556,7 +578,7 @@ namespace Barotrauma.Networking { return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X); }; - GetPropertyData("MaxTransportTime").AssignGUIComponent(slider); + GetPropertyData(nameof(MaxTransportTime)).AssignGUIComponent(slider); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { if (barScroll == 1.0f) @@ -572,14 +594,34 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); + + var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), + TextManager.Get("LosEffect")); + + var losModeRadioButtonLayout + = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsTab.RectTransform), + isHorizontal: true) + { + Stretch = true + }; + + var losModeRadioButtonGroup = new GUIRadioButtonGroup(); + LosMode[] losModes = (LosMode[])Enum.GetValues(typeof(LosMode)); + for (int i = 0; i < losModes.Length; i++) + { + var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), losModeRadioButtonLayout.RectTransform), TextManager.Get($"LosMode{losModes[i]}"), font: GUI.SmallFont, style: "GUIRadioButton"); + losModeRadioButtonGroup.AddRadioButton(i, losTick); + } + GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); + var traitorsMinPlayerCount = CreateLabeledNumberInput(roundsTab, "ServerSettingsTraitorsMinPlayerCount", 1, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); - GetPropertyData("TraitorsMinPlayerCount").AssignGUIComponent(traitorsMinPlayerCount); + GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); - GetPropertyData("AllowRagdollButton").AssignGUIComponent(ragdollButtonBox); + GetPropertyData(nameof(AllowRagdollButton)).AssignGUIComponent(ragdollButtonBox); var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); - GetPropertyData("DisableBotConversations").AssignGUIComponent(disableBotConversationsBox); + GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); var buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), roundsTab.RectTransform), isHorizontal: true) { @@ -729,35 +771,35 @@ namespace Barotrauma.Networking var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowFriendlyFire")); - GetPropertyData("AllowFriendlyFire").AssignGUIComponent(allowFriendlyFire); + GetPropertyData(nameof(AllowFriendlyFire)).AssignGUIComponent(allowFriendlyFire); var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsKillableNPCs")); - GetPropertyData("KillableNPCs").AssignGUIComponent(killableNPCs); + GetPropertyData(nameof(KillableNPCs)).AssignGUIComponent(killableNPCs); var destructibleOutposts = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsDestructibleOutposts")); - GetPropertyData("DestructibleOutposts").AssignGUIComponent(destructibleOutposts); + GetPropertyData(nameof(DestructibleOutposts)).AssignGUIComponent(destructibleOutposts); var lockAllDefaultWires = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsLockAllDefaultWires")); - GetPropertyData("LockAllDefaultWires").AssignGUIComponent(lockAllDefaultWires); + GetPropertyData(nameof(LockAllDefaultWires)).AssignGUIComponent(lockAllDefaultWires); var allowRewiring = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowRewiring")); - GetPropertyData("AllowRewiring").AssignGUIComponent(allowRewiring); + GetPropertyData(nameof(AllowRewiring)).AssignGUIComponent(allowRewiring); var allowWifiChatter = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowWifiChat")); - GetPropertyData("AllowLinkingWifiToChat").AssignGUIComponent(allowWifiChatter); + GetPropertyData(nameof(AllowLinkingWifiToChat)).AssignGUIComponent(allowWifiChatter); var allowDisguises = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowDisguises")); - GetPropertyData("AllowDisguises").AssignGUIComponent(allowDisguises); + GetPropertyData(nameof(AllowDisguises)).AssignGUIComponent(allowDisguises); var voteKickBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowVoteKick")); - GetPropertyData("AllowVoteKick").AssignGUIComponent(voteKickBox); + GetPropertyData(nameof(AllowVoteKick)).AssignGUIComponent(voteKickBox); GUITextBlock.AutoScaleAndNormalize(tickBoxContainer.Content.Children.Select(c => ((GUITickBox)c).TextBlock)); @@ -772,7 +814,7 @@ namespace Barotrauma.Networking ((GUITextBlock)scrollBar.UserData).Text = votesRequiredLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; return true; }; - GetPropertyData("KickVoteRequiredRatio").AssignGUIComponent(slider); + GetPropertyData(nameof(KickVoteRequiredRatio)).AssignGUIComponent(slider); slider.OnMoved(slider, slider.BarScroll); CreateLabeledSlider(antigriefingTab, "ServerSettingsAutobanTime", out slider, out sliderLabel); @@ -784,13 +826,13 @@ namespace Barotrauma.Networking ((GUITextBlock)scrollBar.UserData).Text = autobanLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); return true; }; - GetPropertyData("AutoBanTime").AssignGUIComponent(slider); + GetPropertyData(nameof(AutoBanTime)).AssignGUIComponent(slider); slider.OnMoved(slider, slider.BarScroll); var wrongPasswordBanBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsBanAfterWrongPassword")); - GetPropertyData("BanAfterWrongPassword").AssignGUIComponent(wrongPasswordBanBox); + GetPropertyData(nameof(BanAfterWrongPassword)).AssignGUIComponent(wrongPasswordBanBox); var allowedPasswordRetries = CreateLabeledNumberInput(antigriefingTab, "ServerSettingsPasswordRetriesBeforeBan", 0, 10); - GetPropertyData("MaxPasswordRetriesBeforeBan").AssignGUIComponent(allowedPasswordRetries); + GetPropertyData(nameof(MaxPasswordRetriesBeforeBan)).AssignGUIComponent(allowedPasswordRetries); wrongPasswordBanBox.OnSelected += (tb) => { allowedPasswordRetries.Enabled = tb.Selected; @@ -800,7 +842,7 @@ namespace Barotrauma.Networking // karma -------------------------------------------------------------------------- var karmaBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsUseKarma")); - GetPropertyData("KarmaEnabled").AssignGUIComponent(karmaBox); + GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox); karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform)); foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 13604817e..d71b3868f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -935,7 +935,7 @@ namespace Barotrauma.Steam return workshopPublishStatus; } - private static IEnumerable PublishItem(WorkshopPublishStatus workshopPublishStatus) + private static IEnumerable PublishItem(WorkshopPublishStatus workshopPublishStatus) { if (!isInitialized) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 31dc44d22..d9aeb9d49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -66,10 +66,10 @@ namespace Barotrauma if (clients == null) { return; } - List> voteList = GetVoteList(voteType, clients); - foreach (Pair votable in voteList) + IReadOnlyDictionary voteList = GetVoteCounts(voteType, clients); + foreach (KeyValuePair votable in voteList) { - SetVoteText(listBox, votable.First, votable.Second); + SetVoteText(listBox, votable.Key, votable.Value); } break; case VoteType.StartRound: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 6b7cd2e8c..dbe56f22d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -106,8 +106,12 @@ namespace Barotrauma.Particles public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { this.prefab = prefab; +#if DEBUG debugName = $"Particle ({prefab.Name})"; - +#else + //don't instantiate new string objects in release builds + debugName = prefab.Name; +#endif spriteIndex = Rand.Int(prefab.Sprites.Count); animState = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 72d20579a..1ecd582e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -188,11 +188,14 @@ namespace Barotrauma.Particles var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab; if (particlePrefab == null) { return; } - angle += Rand.Range(Prefab.Properties.AngleMinRad, Prefab.Properties.AngleMaxRad); - - Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); - Vector2 velocity = dir * Rand.Range(Prefab.Properties.VelocityMin, Prefab.Properties.VelocityMax) * velocityMultiplier; - position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); + Vector2 velocity = Vector2.Zero; + if (!MathUtils.NearlyEqual(Prefab.Properties.VelocityMax * velocityMultiplier, 0.0f) || !MathUtils.NearlyEqual(Prefab.Properties.DistanceMax, 0.0f)) + { + angle += Rand.Range(Prefab.Properties.AngleMinRad, Prefab.Properties.AngleMaxRad); + Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); + velocity = dir * Rand.Range(Prefab.Properties.VelocityMin, Prefab.Properties.VelocityMax) * velocityMultiplier; + position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); + } var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, tracerPoints: tracerPoints); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 1b5c3fdc0..441f71846 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -167,8 +167,8 @@ namespace Barotrauma.Particles if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } - - if (particles[particleCount] == null) particles[particleCount] = new Particle(); + + if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, tracerPoints: tracerPoints); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 23a9d554d..e612e92f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -255,7 +255,7 @@ namespace Barotrauma File.WriteAllText(filePath, crashReport); - if (GameSettings.SaveDebugConsoleLogs) DebugConsole.SaveLogs(); + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } if (GameSettings.SendUserStatistics) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index e02424ee4..289937c2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -271,7 +271,7 @@ namespace Barotrauma } } - private IEnumerable WaitForCampaignSetup() + private IEnumerable WaitForCampaignSetup() { GUI.SetCursorWaiting(); string headerText = TextManager.Get("CampaignStartingPleaseWait"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 0a553e0a8..07490fbf9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1802,9 +1802,11 @@ namespace Barotrauma.CharacterEditor { case AnimationType.Walk: case AnimationType.Run: - case AnimationType.Crouch: if (!ragdollParams.CanWalk) { continue; } break; + case AnimationType.Crouch: + if (!ragdollParams.CanWalk || !isHumanoid) { continue; } + break; case AnimationType.SwimSlow: case AnimationType.SwimFast: break; @@ -2690,7 +2692,15 @@ namespace Barotrauma.CharacterEditor characterDropDown.SelectItem(currentCharacterConfig); characterDropDown.OnSelected = (component, data) => { - SpawnCharacter((string)data); + string configFile = (string)data; + try + { + SpawnCharacter(configFile); + } + catch (Exception e) + { + HandleSpawnException(configFile, e); + } return true; }; if (currentCharacterConfig == CharacterPrefab.HumanConfigFile) @@ -2719,19 +2729,48 @@ namespace Barotrauma.CharacterEditor prevCharacterButton.TextBlock.AutoScaleHorizontal = true; prevCharacterButton.OnClicked += (b, obj) => { - SpawnCharacter(GetPreviousConfigFile()); + string configFile = GetPreviousConfigFile(); + try + { + SpawnCharacter(configFile); + } + catch (Exception e) + { + HandleSpawnException(configFile, e); + } return true; }; var nextCharacterButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), charButtons.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("NextCharacter")); prevCharacterButton.TextBlock.AutoScaleHorizontal = true; nextCharacterButton.OnClicked += (b, obj) => { - SpawnCharacter(GetNextConfigFile()); + string configFile = GetNextConfigFile(); + try + { + SpawnCharacter(configFile); + } + catch (Exception e) + { + HandleSpawnException(configFile, e); + } return true; }; charButtons.RectTransform.MinSize = new Point(0, prevCharacterButton.RectTransform.MinSize.Y); characterPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), characterSelectionPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right); characterSelectionPanel.RectTransform.MinSize = new Point(0, (int)(content.RectTransform.Children.Sum(c => c.MinSize.Y) * 1.2f)); + + void HandleSpawnException(string configFile, Exception e) + { + if (configFile != CharacterPrefab.HumanConfigFile) + { + DebugConsole.ThrowError($"Failed to spawn the character \"{configFile}\".", e); + SpawnCharacter(CharacterPrefab.HumanConfigFile); + } + else + { + throw new Exception($"Failed to spawn the character \"{configFile}\".", innerException: e); + } + } } private void CreateFileEditPanel() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index fe25d54b9..061d6e262 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -143,9 +143,33 @@ namespace Barotrauma editorContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)); - var seedContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), seedContainer.RectTransform), TextManager.Get("leveleditor.levelseed")); - seedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedContainer.RectTransform), ToolBox.RandomSeed(8)); + var seedContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedRightPanel.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + Vector2 randomizeButtonRelativeSize = GetRandomizeButtonRelativeSize(); + Vector2 elementRelativeSize = GetSeedElementRelativeSize(); + var seedLabel = new GUITextBlock(new RectTransform(elementRelativeSize, seedContainer.RectTransform), TextManager.Get("leveleditor.levelseed")); + seedBox = new GUITextBox(new RectTransform(elementRelativeSize, seedContainer.RectTransform), GetLevelSeed()); + var seedButton = new GUIButton(new RectTransform(randomizeButtonRelativeSize, seedContainer.RectTransform), style: "RandomizeButton") + { + OnClicked = (button, userData) => + { + if(seedBox == null) { return false; } + seedBox.Text = GetLevelSeed(); + return true; + } + }; + seedContainer.RectTransform.SizeChanged += () => + { + Vector2 randomizeButtonRelativeSize = GetRandomizeButtonRelativeSize(); + Vector2 elementRelativeSize = GetSeedElementRelativeSize(); + seedLabel.RectTransform.RelativeSize = elementRelativeSize; + seedBox.RectTransform.RelativeSize = elementRelativeSize; + seedButton.RectTransform.RelativeSize = randomizeButtonRelativeSize; + }; + Vector2 GetRandomizeButtonRelativeSize() => 0.2f * seedContainer.Rect.Width > seedContainer.Rect.Height ? + new Vector2(Math.Min((float)seedContainer.Rect.Height / seedContainer.Rect.Width, 0.2f), 1.0f) : + new Vector2(0.15f, Math.Min((0.2f * seedContainer.Rect.Width) / seedContainer.Rect.Height, 1.0f)); + Vector2 GetSeedElementRelativeSize() => new Vector2(0.5f * (1.0f - randomizeButtonRelativeSize.X), 1.0f); + static string GetLevelSeed() => ToolBox.RandomSeed(8); mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 2a12028a9..2ca44aba9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -679,7 +679,7 @@ namespace Barotrauma return true; } - private IEnumerable SelectScreenWithWaitCursor(Screen screen) + private IEnumerable SelectScreenWithWaitCursor(Screen screen) { GUI.SetCursorWaiting(); //tiny delay to get the cursor to render @@ -719,7 +719,7 @@ namespace Barotrauma } #endregion - public void QuickStart(bool fixedSeed = false, string sub = null) + public void QuickStart(bool fixedSeed = false, string sub = null, float difficulty = 40, LevelGenerationParams levelGenerationParams = null) { if (fixedSeed) { @@ -751,7 +751,7 @@ namespace Barotrauma GameModePreset.DevSandbox, missionPrefabs: null); //(gamesession.GameMode as SinglePlayerCampaign).GenerateMap(ToolBox.RandomSeed(8)); - gamesession.StartRound(fixedSeed ? "abcd" : ToolBox.RandomSeed(8), difficulty: 40); + gamesession.StartRound(fixedSeed ? "abcd" : ToolBox.RandomSeed(8), difficulty, levelGenerationParams); GameMain.GameScreen.Select(); // TODO: modding support string[] jobIdentifiers = new string[] { "captain", "engineer", "mechanic", "securityofficer", "medicaldoctor" }; @@ -890,7 +890,7 @@ namespace Barotrauma } } - private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) + private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) { string originalText = messageBox.Text.Text; int doneCount = 0; @@ -1452,7 +1452,7 @@ namespace Barotrauma } } - private IEnumerable WairForRemoteContentReceived() + private IEnumerable WairForRemoteContentReceived() { while (true) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 40b99af75..1d1c19e48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -18,9 +18,7 @@ namespace Barotrauma private readonly GUILayoutGroup infoFrameContent; private readonly GUIFrame myCharacterFrame; - private readonly GUIListBox subList, modeList; - - private readonly GUIListBox chatBox, playerList; + private readonly GUIListBox chatBox; private readonly GUIButton serverLogReverseButton; private readonly GUIListBox serverLogBox, serverLogFilterTicks; @@ -73,7 +71,8 @@ namespace Barotrauma private readonly GUIComponent gameModeContainer; private readonly GUIButton spectateButton; private readonly GUILayoutGroup roundControlsHolder; - public GUIButton SettingsButton { get; private set; } + + public readonly GUIButton SettingsButton; public static GUIButton JobInfoFrame; private readonly GUITickBox spectateBox; @@ -85,12 +84,15 @@ namespace Barotrauma private bool createPendingChangesText = true; public GUIButton PlayerFrame; + public readonly GUIButton SubVisibilityButton; + + private readonly GUITextBox subSearchBox; + private readonly GUIComponent subPreviewContainer; private readonly GUITickBox autoRestartBox; private readonly GUITextBlock autoRestartText; - private readonly GUIDropDown shuttleList; private readonly GUITickBox shuttleTickBox; private readonly GUIComponent settingsBlocker; @@ -161,20 +163,11 @@ namespace Barotrauma private readonly GUITextBlock publicOrPrivate; - public GUIListBox SubList - { - get { return subList; } - } + public readonly GUIListBox SubList; - public GUIDropDown ShuttleList - { - get { return shuttleList; } - } + public readonly GUIDropDown ShuttleList; - public GUIListBox ModeList - { - get { return modeList; } - } + public readonly GUIListBox ModeList; private int selectedModeIndex; public int SelectedModeIndex @@ -184,7 +177,7 @@ namespace Barotrauma { if (HighlightedModeIndex == selectedModeIndex) { - modeList.Select(value); + ModeList.Select(value); } selectedModeIndex = value; } @@ -192,17 +185,14 @@ namespace Barotrauma public int HighlightedModeIndex { - get { return modeList.SelectedIndex; } + get { return ModeList.SelectedIndex; } set { - modeList.Select(value, true); + ModeList.Select(value, true); } } - public GUIListBox PlayerList - { - get { return playerList; } - } + public readonly GUIListBox PlayerList; public GUITextBox CharacterNameBox { @@ -228,16 +218,9 @@ namespace Barotrauma private set; } - public SubmarineInfo SelectedSub - { - get { return subList.SelectedData as SubmarineInfo; } - set { subList.Select(value); } - } + public SubmarineInfo SelectedSub => SubList.SelectedData as SubmarineInfo; - public SubmarineInfo SelectedShuttle - { - get { return shuttleList.SelectedData as SubmarineInfo; } - } + public SubmarineInfo SelectedShuttle => ShuttleList.SelectedData as SubmarineInfo; public MultiPlayerCampaignSetupUI CampaignSetupUI; public List CampaignSubmarines = new List(); @@ -253,7 +236,7 @@ namespace Barotrauma public GameModePreset SelectedMode { - get { return modeList.SelectedData as GameModePreset; } + get { return ModeList.SelectedData as GameModePreset; } } public MissionType MissionType @@ -529,7 +512,7 @@ namespace Barotrauma //player list ------------------------------------------------------------------ - playerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform)) + PlayerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform)) { OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; } }; @@ -737,25 +720,65 @@ namespace Barotrauma }; var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), serverInfoHolder.RectTransform)); - ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), style: "GUITextBoxNoBorder") + ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), + style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft); + var serverMessageHint = new GUITextBlock(new RectTransform(Vector2.One, ServerMessage.RectTransform), + textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUI.Style.Font, text: TextManager.Get("ClickToWriteServerMessage")); + + void updateServerMessageScrollBasedOnCaret() { - Wrap = true + float caretY = ServerMessage.CaretScreenPos.Y; + float bottomCaretExtent = ServerMessage.Font.LineHeight * 1.5f; + float topCaretExtent = -ServerMessage.Font.LineHeight * 0.5f; + if (caretY + bottomCaretExtent > serverMessageContainer.Rect.Bottom) + { + serverMessageContainer.ScrollBar.BarScroll + = (caretY - ServerMessage.Rect.Top - serverMessageContainer.Rect.Height + bottomCaretExtent) + / (ServerMessage.Rect.Height - serverMessageContainer.Rect.Height); + } + else if (caretY + topCaretExtent < serverMessageContainer.Rect.Top) + { + serverMessageContainer.ScrollBar.BarScroll + = (caretY - ServerMessage.Rect.Top + topCaretExtent) + / (ServerMessage.Rect.Height - serverMessageContainer.Rect.Height); + } + } + + ServerMessage.OnSelected += (textBox, key) => + { + serverMessageHint.Visible = false; + updateServerMessageScrollBasedOnCaret(); }; ServerMessage.OnTextChanged += (textBox, text) => { Vector2 textSize = textBox.Font.MeasureString(textBox.WrappedText); textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10)); serverMessageContainer.UpdateScrollBarSize(); - serverMessageContainer.BarScroll = 1.0f; + serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); + return true; + }; + ServerMessage.OnEnterPressed += (textBox, text) => + { + string str = textBox.Text; + int caretIndex = textBox.CaretIndex; + textBox.Text = $"{str[..caretIndex]}\n{str[caretIndex..]}"; + textBox.CaretIndex = caretIndex + 1; + return true; }; ServerMessage.OnDeselected += (textBox, key) => { if (!textBox.Readonly) { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Message); + GameMain.Client?.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Message); } + serverMessageHint.Visible = !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); }; + + ServerMessage.OnKeyHit += (sender, key) => updateServerMessageScrollBasedOnCaret(); + + + clientHiddenElements.Add(serverMessageHint); clientReadonlyElements.Add(ServerMessage); //submarine list ------------------------------------------------------------------ @@ -768,26 +791,36 @@ namespace Barotrauma var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUI.SubHeadingFont); + SubVisibilityButton + = new GUIButton( + new RectTransform(Vector2.One * 1.2f, subLabel.RectTransform, anchor: Anchor.CenterRight, + scaleBasis: ScaleBasis.BothHeight), + style: "EyeButton") + { + OnClicked = (button, o) => + { + CreateSubmarineVisibilityMenu(); + return false; + } + }; + clientHiddenElements.Add(SubVisibilityButton); + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) { Stretch = true }; var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => + subSearchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; + subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + subSearchBox.OnTextChanged += (textBox, text) => { - foreach (GUIComponent child in subList.Content.Children) - { - if (!(child.UserData is SubmarineInfo sub)) { continue; } - child.Visible = string.IsNullOrEmpty(text) || sub.DisplayName.ToLower().Contains(text.ToLower()); - } + UpdateSubVisibility(); return true; }; - subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) + SubList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) { OnSelected = VotableClicked }; @@ -832,7 +865,7 @@ namespace Barotrauma shuttleTickBox.TextBlock.TextScale = 1.0f; } }; - shuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) + ShuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) { OnSelected = (component, obj) => { @@ -840,8 +873,8 @@ namespace Barotrauma return true; } }; - shuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); - shuttleHolder.RectTransform.MinSize = new Point(0, shuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); + ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); + shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); subPreviewContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), rightColumn.RectTransform), style: null); subPreviewContainer.RectTransform.SizeChanged += () => @@ -871,7 +904,7 @@ namespace Barotrauma UserData = "modevotes", Visible = false }; - modeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) + ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) { OnSelected = VotableClicked }; @@ -880,7 +913,7 @@ namespace Barotrauma { if (mode.IsSinglePlayer) { continue; } - var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), modeList.Content.RectTransform), style: null) + var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), ModeList.Content.RectTransform), style: null) { UserData = mode }; @@ -938,7 +971,7 @@ namespace Barotrauma { OnClicked = (_, __) => { - GameMain.Client.RequestSelectMode(modeList.Content.GetChildIndex(modeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + GameMain.Client.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); return true; } }; @@ -1221,7 +1254,7 @@ namespace Barotrauma GUI.ClearCursorWait(); } - public IEnumerable WaitForStartRound(GUIButton startButton) + public IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); string headerText = TextManager.Get("RoundStartingPleaseWait"); @@ -1264,6 +1297,8 @@ namespace Barotrauma { if (GameMain.NetworkMember == null) { return; } + visibilityMenuOrder.Clear(); + CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; @@ -1362,7 +1397,7 @@ namespace Barotrauma ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - shuttleList.Enabled = shuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); + ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); @@ -1370,6 +1405,8 @@ namespace Barotrauma roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); + SubVisibilityButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; RefreshGameModeContent(); @@ -1557,7 +1594,7 @@ namespace Barotrauma }; } - UpdateJobPreferences(); + UpdateJobPreferences(characterInfo); appearanceFrame = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox") { @@ -1853,7 +1890,7 @@ namespace Barotrauma } else { - if (subList == shuttleList || subList == shuttleList.ListBox || subList == shuttleList.ListBox.Content) + if (subList == ShuttleList || subList == ShuttleList.ListBox || subList == ShuttleList.ListBox.Content) { subTextBlock.TextColor = new Color(subTextBlock.TextColor, sub.HasTag(SubmarineTag.Shuttle) ? 1.0f : 0.6f); } @@ -1865,9 +1902,22 @@ namespace Barotrauma frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.RawToolTip; } + CreateSubmarineClassText( + frame, + sub, + subTextBlock, + subList); + } + + private void CreateSubmarineClassText( + GUIComponent parent, + SubmarineInfo sub, + GUITextBlock subTextBlock, + GUIComponent subList) + { if (sub.HasTag(SubmarineTag.Shuttle)) { - var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, + var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, @@ -1875,10 +1925,10 @@ namespace Barotrauma CanBeFocused = false }; //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) - if (subList == this.subList.Content) + if (subList == this.SubList.Content) { subTextBlock.TextColor *= 0.8f; - foreach (GUIComponent child in frame.Children) + foreach (GUIComponent child in parent.Children) { child.Color *= 0.8f; } @@ -1886,17 +1936,17 @@ namespace Barotrauma } else { - var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) { UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, - ToolTip = subTextBlock.RawToolTip + ToolTip = subTextBlock.RawToolTip, + CanBeFocused = false }; } - } - + public bool VotableClicked(GUIComponent component, object userData) { if (GameMain.Client == null) { return false; } @@ -1995,7 +2045,7 @@ namespace Barotrauma public void AddPlayer(Client client) { - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), playerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), PlayerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, client.Name, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, style: null) { Padding = Vector4.One * 10.0f * GUI.Scale, @@ -2109,8 +2159,8 @@ namespace Barotrauma public void RemovePlayer(Client client) { - GUIComponent child = playerList.Content.GetChildByUserData(client); - if (child != null) { playerList.RemoveChild(child); } + GUIComponent child = PlayerList.Content.GetChildByUserData(client); + if (child != null) { PlayerList.RemoveChild(child); } } public void SelectPlayer(GUITextBlock component, GUITextBlock.ClickableArea area) @@ -2459,7 +2509,7 @@ namespace Barotrauma private bool ClosePlayerFrame(GUIButton button, object userData) { PlayerFrame = null; - playerList.Deselect(); + PlayerList.Deselect(); return true; } @@ -2520,7 +2570,7 @@ namespace Barotrauma GUI.Style.Apply(micIcon, targetMicStyle); } - foreach (GUIComponent child in playerList.Content.Children) + foreach (GUIComponent child in PlayerList.Content.Children) { if (child.UserData is Client client) { @@ -2752,13 +2802,13 @@ namespace Barotrauma appearanceFrame.ClearChildren(); - var info = GameMain.Client.CharacterInfo; + var info = GameMain.Client.CharacterInfo ?? Character.Controlled?.Info; CharacterAppearanceCustomizationMenu = new CharacterInfo.AppearanceCustomizationMenu(info, appearanceFrame) { OnHeadSwitch = menu => { StoreHead(true); - UpdateJobPreferences(); + UpdateJobPreferences(info); SelectAppearanceTab(button, _); }, OnSliderMoved = (bar, scroll) => @@ -2818,7 +2868,7 @@ namespace Barotrauma } } - UpdateJobPreferences(); + UpdateJobPreferences(GameMain.Client.CharacterInfo ?? Character.Controlled?.Info); if (moveToNext) { @@ -3014,16 +3064,16 @@ namespace Barotrauma public void SelectMode(int modeIndex) { - if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } + if (modeIndex < 0 || modeIndex >= ModeList.Content.CountChildren) { return; } - if ((GameModePreset)modeList.Content.GetChild(modeIndex).UserData != GameModePreset.MultiPlayerCampaign) + if ((GameModePreset)ModeList.Content.GetChild(modeIndex).UserData != GameModePreset.MultiPlayerCampaign) { ToggleCampaignMode(false); } - var prevMode = modeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset; + var prevMode = ModeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset; - if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && modeList.SelectedIndex != modeIndex) { modeList.Select(modeIndex, true); } + if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, true); } selectedModeIndex = modeIndex; if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) @@ -3043,7 +3093,7 @@ namespace Barotrauma public void HighlightMode(int modeIndex) { - if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } + if (modeIndex < 0 || modeIndex >= ModeList.Content.CountChildren) { return; } HighlightedModeIndex = modeIndex; RefreshGameModeContent(); @@ -3139,7 +3189,7 @@ namespace Barotrauma RefreshEnabledElements(); if (enabled) { - modeList.Select(GameModePreset.MultiPlayerCampaign, true); + ModeList.Select(GameModePreset.MultiPlayerCampaign, true); } } @@ -3147,17 +3197,17 @@ namespace Barotrauma { string name = submarine?.Name; bool displayed = false; - subList.OnSelected -= VotableClicked; - subList.Deselect(); + SubList.OnSelected -= VotableClicked; + SubList.Deselect(); subPreviewContainer.ClearChildren(); - foreach (GUIComponent child in subList.Content.Children) + foreach (GUIComponent child in SubList.Content.Children) { if (!(child.UserData is SubmarineInfo sub)) { continue; } //just check the name, even though the campaign sub may not be the exact same version //we're selecting the sub just for show, the selection is not actually used for anything if (sub.Name == name) { - subList.Select(sub); + SubList.Select(sub); if (SubmarineInfo.SavedSubmarines.Contains(sub)) { CreateSubPreview(sub); @@ -3166,7 +3216,7 @@ namespace Barotrauma break; } } - subList.OnSelected += VotableClicked; + SubList.OnSelected += VotableClicked; if (!displayed) { CreateSubPreview(submarine); @@ -3194,11 +3244,13 @@ namespace Barotrauma return true; } - private void UpdateJobPreferences() + private void UpdateJobPreferences(CharacterInfo characterInfo) { + if (characterInfo == null) { return; } + GUICustomComponent characterIcon = JobPreferenceContainer.GetChild(); JobPreferenceContainer.RemoveChild(characterIcon); - GameMain.Client.CharacterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.025f) }); + characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.025f) }); GUIListBox listBox = JobPreferenceContainer.GetChild(); /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } @@ -3231,7 +3283,7 @@ namespace Barotrauma variantButton.OnClicked = (btn, obj) => { btn.Parent.UserData = obj; - UpdateJobPreferences(); + UpdateJobPreferences(characterInfo); return false; }; } @@ -3340,7 +3392,7 @@ namespace Barotrauma //matching sub found and already selected, all good if (sub != null) { - if (subList == this.subList) + if (subList == this.SubList) { CreateSubPreview(sub); } @@ -3533,6 +3585,249 @@ namespace Barotrauma } } + private List visibilityMenuOrder = new List(); + private void CreateSubmarineVisibilityMenu() + { + var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "", + buttons: Array.Empty(), + relativeSize: new Vector2(0.75f, 0.75f)); + messageBox.Content.ChildAnchor = Anchor.TopCenter; + var columns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), messageBox.Content.RectTransform), isHorizontal: true); + + GUILayoutGroup createColumn(float width) + => new GUILayoutGroup(new RectTransform(new Vector2(width, 1.0f), columns.RectTransform)) + { Stretch = true }; + + GUIListBox createColumnListBox(string labelTag) + { + var column = createColumn(0.45f); + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), column.RectTransform), + TextManager.Get(labelTag), textAlignment: Alignment.Center); + return new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), column.RectTransform)) + { + CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, + CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, + HideDraggedElement = true + }; + } + + void handleDraggingAcrossLists(GUIListBox from, GUIListBox to) + { + //TODO: put this in a static class once modding-refactor gets merged + + if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) + { + //move the dragged elements to the index determined previously + var draggedElement = from.DraggedElement; + + var selected = from.AllSelected.ToList(); + selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); + + float oldCount = to.Content.CountChildren; + float newCount = oldCount + selected.Count; + + var offset = draggedElement.RectTransform.AbsoluteOffset; + offset += from.Content.Rect.Location; + offset -= to.Content.Rect.Location; + + for (int i = 0; i < selected.Count; i++) + { + var c = selected[i]; + c.Parent.RemoveChild(c); + c.RectTransform.Parent = to.Content.RectTransform; + c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); + } + + from.DraggedElement = null; + from.Deselect(); + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + + //recalculate the dragged element's offset so it doesn't jump around + draggedElement.RectTransform.AbsoluteOffset = offset; + + to.DraggedElement = draggedElement; + + to.BarScroll = to.BarScroll * (oldCount / newCount); + } + } + + var visibleSubsList = createColumnListBox("VisibleSubmarines"); + var centerColumn = createColumn(0.1f); + + void centerSpacing() + { + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), centerColumn.RectTransform), style: null); + } + + GUIButton centerButton(string style) + => new GUIButton( + new RectTransform(new Vector2(1.0f, 0.1f), centerColumn.RectTransform), + style: style); + + var hiddenSubsList = createColumnListBox("HiddenSubmarines"); + + void addSubToList(SubmarineInfo sub, GUIListBox list) + { + var modFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.08f), list.Content.RectTransform), + style: "ListBoxElement") + { + UserData = sub + }; + + var frameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIDragIndicator") + { + CanBeFocused = false + }; + + var subName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), frameContent.RectTransform), + text: sub.Name) + { + CanBeFocused = false + }; + + CreateSubmarineClassText( + frameContent, + sub, + subName, + list.Content); + } + + foreach (var sub in GameMain.Client.ServerSubmarines + .OrderBy(s => visibilityMenuOrder.Contains(s)) + .ThenBy(s => visibilityMenuOrder.IndexOf(s))) + { + addSubToList(sub, + GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name) ? hiddenSubsList : visibleSubsList); + } + + void onRearranged(GUIListBox listBox, object userData) + { + visibilityMenuOrder.Clear(); + visibilityMenuOrder.AddRange(visibleSubsList.Content.Children.Select(c => c.UserData as SubmarineInfo)); + visibilityMenuOrder.AddRange(hiddenSubsList.Content.Children.Select(c => c.UserData as SubmarineInfo)); + } + + visibleSubsList.OnRearranged = onRearranged; + hiddenSubsList.OnRearranged = onRearranged; + + void swapListItems(GUIListBox from, GUIListBox to) + { + to.Deselect(); + var selected = from.AllSelected.ToArray(); + int lastIndex = from.Content.GetChildIndex(selected.LastOrDefault()); + int nextIndex = lastIndex + 1; + GUIComponent nextComponent = null; + if (lastIndex >= 0 && nextIndex < from.Content.CountChildren) + { + nextComponent = from.Content.GetChild(nextIndex); + } + foreach (var frame in selected) + { + frame.Parent.RemoveChild(frame); + frame.RectTransform.Parent = to.Content.RectTransform; + } + from.RecalculateChildren(); + from.RectTransform.RecalculateScale(true); + to.RecalculateChildren(); + to.RectTransform.RecalculateScale(true); + to.Select(selected); + if (nextComponent != null) { from.Select(nextComponent.ToEnumerable()); } + } + + centerSpacing(); + var visibleToHidden = centerButton("GUIButtonToggleRight"); + visibleToHidden.OnClicked = (button, o) => + { + swapListItems(visibleSubsList, hiddenSubsList); + return false; + }; + var hiddenToVisible = centerButton("GUIButtonToggleLeft"); + hiddenToVisible.OnClicked = (button, o) => + { + swapListItems(hiddenSubsList, visibleSubsList); + return false; + }; + centerSpacing(); + + var buttonLayout + = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.1f), messageBox.Content.RectTransform), + isHorizontal: true) + { + RelativeSpacing = 0.01f + }; + var cancelButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), + TextManager.Get("Cancel")) + { + OnClicked = (button, o) => + { + messageBox.Close(); + return false; + } + }; + var okButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), + TextManager.Get("OK")) + { + OnClicked = (button, o) => + { + var hiddenSubs = GameMain.Client.ServerSettings.HiddenSubs; + hiddenSubs.Clear(); + hiddenSubs.UnionWith(hiddenSubsList.Content.Children.Select(c => (c.UserData as SubmarineInfo).Name)); + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.HiddenSubs); + messageBox.Close(); + return false; + } + }; + + new GUICustomComponent(new RectTransform(Vector2.Zero, messageBox.RectTransform), + onUpdate: (f, component) => + { + handleDraggingAcrossLists(visibleSubsList, hiddenSubsList); + handleDraggingAcrossLists(hiddenSubsList, visibleSubsList); + if (PlayerInput.PrimaryMouseButtonClicked() + && !GUI.IsMouseOn(visibleToHidden) + && !GUI.IsMouseOn(hiddenToVisible)) + { + if (!GUI.IsMouseOn(hiddenSubsList) + || !hiddenSubsList.Content.IsParentOf(GUI.MouseOn)) + { + hiddenSubsList.Deselect(); + } + + if (!GUI.IsMouseOn(visibleSubsList) + || !visibleSubsList.Content.IsParentOf(GUI.MouseOn)) + { + visibleSubsList.Deselect(); + } + } + }, + onDraw: (spriteBatch, component) => + { + visibleSubsList.DraggedElement?.DrawManually(spriteBatch, true, true); + hiddenSubsList.DraggedElement?.DrawManually(spriteBatch, true, true); + }); + } + + public void UpdateSubVisibility() + { + foreach (GUIComponent child in SubList.Content.Children) + { + if (!(child.UserData is SubmarineInfo sub)) { continue; } + child.Visible = !GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name) + && (string.IsNullOrEmpty(subSearchBox.Text) || sub.DisplayName.Contains(subSearchBox.Text, StringComparison.OrdinalIgnoreCase)); + } + } + public void OnRoundEnded() { CampaignCharacterDiscarded = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index a966ab977..6d2513216 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -43,7 +43,7 @@ namespace Barotrauma CoroutineManager.StartCoroutine(UpdateColorFade(from, to, duration)); } - private IEnumerable UpdateColorFade(Color from, Color to, float duration) + private IEnumerable UpdateColorFade(Color from, Color to, float duration) { while (Selected != this) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 4430efdb1..0d07b20c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -578,6 +578,7 @@ namespace Barotrauma RecalculateHolder(); } serverInfo.CreatePreviewWindow(serverPreview.Content); + serverPreview.ForceLayoutRecalculation(); btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); } return true; @@ -1715,7 +1716,7 @@ namespace Barotrauma CoroutineManager.StartCoroutine(WaitForRefresh()); } - private IEnumerable WaitForRefresh() + private IEnumerable WaitForRefresh() { waitingForRefresh = true; if (refreshDisableTimer > DateTime.Now) @@ -2058,7 +2059,7 @@ namespace Barotrauma FilterServers(); } - private IEnumerable EstimateLobbyPing(ServerInfo serverInfo, GUITextBlock serverPingText) + private IEnumerable EstimateLobbyPing(ServerInfo serverInfo, GUITextBlock serverPingText) { while (!steamPingInfoReady) { @@ -2096,7 +2097,7 @@ namespace Barotrauma waitingForRefresh = false; } - private IEnumerable SendMasterServerRequest() + private IEnumerable SendMasterServerRequest() { RestClient client = null; try @@ -2271,7 +2272,7 @@ namespace Barotrauma return true; } - private IEnumerable ConnectToServer(string endpoint, string serverName) + private IEnumerable ConnectToServer(string endpoint, string serverName) { string serverIP = null; UInt64 serverSteamID = SteamManager.SteamIDStringToUInt64(endpoint); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 350ff7ac5..9da115082 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -304,7 +304,7 @@ namespace Barotrauma float subscribePollAdditionalWait = 0.0f; - private IEnumerable PollSubscribedItems() + private IEnumerable PollSubscribedItems() { if (!SteamManager.IsInitialized) { yield return CoroutineStatus.Success; } @@ -364,7 +364,7 @@ namespace Barotrauma } } - public IEnumerable RefreshDownloadState() + public IEnumerable RefreshDownloadState() { bool isDownloading = true; while (true) @@ -831,7 +831,7 @@ namespace Barotrauma } } - private IEnumerable WaitForItemPreviewDownloaded(Steamworks.Ugc.Item? item, GUIListBox listBox, string previewImagePath) + private IEnumerable WaitForItemPreviewDownloaded(Steamworks.Ugc.Item? item, GUIListBox listBox, string previewImagePath) { while (true) { @@ -1835,7 +1835,7 @@ namespace Barotrauma } - private IEnumerable WaitForPublish(SteamManager.WorkshopPublishStatus workshopPublishStatus) + private IEnumerable WaitForPublish(SteamManager.WorkshopPublishStatus workshopPublishStatus) { var item = workshopPublishStatus.Item; var coroutine = workshopPublishStatus.Coroutine; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 76be7c17f..1550a1afa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -369,11 +369,20 @@ namespace Barotrauma { ToolTip = TextManager.Get("AddSubToolTip") }; + + List<(string Name, SubmarineInfo Sub)> subs = new List<(string Name, SubmarineInfo Sub)>(); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.Type != SubmarineType.Player) { continue; } - linkedSubBox.AddItem(sub.Name, sub); + subs.Add((sub.Name, sub)); } + + foreach (var (name, sub) in subs.OrderBy(tuple => tuple.Name)) + { + linkedSubBox.AddItem(name, sub); + } + linkedSubBox.OnSelected += SelectLinkedSub; linkedSubBox.OnDropped += (component, obj) => { @@ -1220,11 +1229,19 @@ namespace Barotrauma string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); linkedSubBox.ClearChildren(); + + List<(string Name, SubmarineInfo Sub)> subs = new List<(string Name, SubmarineInfo Sub)>(); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.Type != SubmarineType.Player) { continue; } if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } - linkedSubBox.AddItem(sub.Name, sub); + subs.Add((sub.Name, sub)); + } + + foreach (var (subName, sub) in subs.OrderBy(tuple => tuple.Name)) + { + linkedSubBox.AddItem(subName, sub); } cam.UpdateTransform(); @@ -1294,7 +1311,7 @@ namespace Barotrauma /// /// /// - private static IEnumerable AutoSaveCoroutine() + private static IEnumerable AutoSaveCoroutine() { DateTime target = DateTime.Now.AddMinutes(GameSettings.AutoSaveIntervalSeconds); DateTime tempTarget = DateTime.Now; @@ -1998,14 +2015,21 @@ namespace Barotrauma var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); - Submarine.MainSub.Info?.OutpostModuleInfo?.DetermineGapPositions(Submarine.MainSub); - foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + var outpostModuleInfo = Submarine.MainSub.Info?.OutpostModuleInfo; + if (outpostModuleInfo != null) { - if ((OutpostModuleInfo.GapPosition)gapPos == OutpostModuleInfo.GapPosition.None) { continue; } - gapPositionDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); - if (Submarine.MainSub.Info?.OutpostModuleInfo?.GapPositions.HasFlag((OutpostModuleInfo.GapPosition)gapPos) ?? false) + if (outpostModuleInfo.GapPositions == OutpostModuleInfo.GapPosition.None) { - gapPositionDropDown.SelectItem(gapPos); + outpostModuleInfo.DetermineGapPositions(Submarine.MainSub); + } + foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + { + if ((OutpostModuleInfo.GapPosition)gapPos == OutpostModuleInfo.GapPosition.None) { continue; } + gapPositionDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); + if (outpostModuleInfo.GapPositions.HasFlag((OutpostModuleInfo.GapPosition)gapPos)) + { + gapPositionDropDown.SelectItem(gapPos); + } } } @@ -4774,7 +4798,7 @@ namespace Barotrauma if (dummyCharacter != null) { - dummyCharacter.AnimController.FindHull(dummyCharacter.CursorWorldPosition, false); + dummyCharacter.AnimController.FindHull(dummyCharacter.CursorWorldPosition, setSubmarine: false); foreach (Item item in dummyCharacter.Inventory.AllItems) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 45a3e50bf..3bdc5a04d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -221,7 +221,7 @@ namespace Barotrauma.Sounds { if (!MathUtils.IsValid(value)) { return; } - gain = Math.Clamp(value, 0.0f, 1.0f); + gain = Math.Max(value, 0.0f); if (ALSourceIndex < 0) { return; } @@ -525,6 +525,8 @@ namespace Barotrauma.Sounds throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); } + SetProperties(); + Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); alError = Al.GetError(); if (alError != Al.NoError) @@ -570,16 +572,9 @@ namespace Barotrauma.Sounds } } Sound.Owner.InitStreamThread(); + SetProperties(); } } - - this.Position = position; - this.Gain = gain; - this.FrequencyMultiplier = freqMult; - this.Looping = false; - this.Near = near; - this.Far = far; - this.Category = category; #if !DEBUG } catch @@ -594,6 +589,17 @@ namespace Barotrauma.Sounds } #endif + void SetProperties() + { + this.Position = position; + this.Gain = gain; + this.FrequencyMultiplier = freqMult; + this.Looping = false; + this.Near = near; + this.Far = far; + this.Category = category; + } + Sound.Owner.Update(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 377de997b..edfccb5b1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -147,7 +147,7 @@ namespace Barotrauma MathUtils.NearlyEqual(rangeA, rangeB); } - public static IEnumerable Init() + public static IEnumerable Init() { OverrideMusicType = null; @@ -826,7 +826,7 @@ namespace Barotrauma //find appropriate music for the current situation string currentMusicType = GetCurrentMusicType(); float currentIntensity = GameMain.GameSession?.EventManager != null ? - GameMain.GameSession.EventManager.CurrentIntensity * 100.0f : 0.0f; + GameMain.GameSession.EventManager.MusicIntensity * 100.0f : 0.0f; IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); int mainTrackIndex = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index ed81e93a2..ac6b119c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -84,7 +84,7 @@ namespace Barotrauma Vector4 sourceVector = Vector4.Zero; bool temp2 = false; - int maxLoadRetries = 3; + int maxLoadRetries = File.Exists(FilePath) ? 3 : 0; for (int i = 0; i <= maxLoadRetries; i++) { try @@ -169,7 +169,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Sprite \"{file}\" not found! {Environment.StackTrace.CleanupStackTrace()}"); + DebugConsole.ThrowError($"Sprite \"{file}\" not found!"); + DebugConsole.Log(Environment.StackTrace.CleanupStackTrace()); } return null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 327dac75a..9c9646925 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -434,130 +434,8 @@ namespace Barotrauma return Color.Lerp(gradient[(int)scaledT], gradient[(int)Math.Min(scaledT + 1, gradient.Length - 1)], (scaledT - (int)scaledT)); } - public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f, bool playerInput = false) //TODO: could integrate this into the ScalableFont class directly - { - Vector2 textSize = font.MeasureString(text); - if (textSize.X <= lineLength) { return text; } - - if (!playerInput) - { - text = text.Replace("\n", " \n "); - } - - List words = new List(); - string currWord = ""; - - for (int i = 0; i < text.Length; i++) - { - if (TextManager.IsCJK(text[i].ToString())) - { - if (currWord.Length > 0) - { - words.Add(currWord); - currWord = ""; - } - words.Add(text[i].ToString()); - } - else if (text[i] == ' ') - { - if (currWord.Length > 0) - { - words.Add(currWord); - currWord = ""; - } - words.Add(string.Empty); - } - else - { - currWord += text[i]; - } - } - if (currWord.Length > 0) - { - words.Add(currWord); - currWord = ""; - } - - StringBuilder wrappedText = new StringBuilder(); - float linePos = 0f; - Vector2 spaceSize = font.MeasureString(" ") * textScale; - for (int i = 0; i < words.Count; ++i) - { - string currentWord = words[i]; - if (currentWord.Length == 0) - { - // space - currentWord = " "; - } - else if (string.IsNullOrWhiteSpace(currentWord) && currentWord != "\n") - { - continue; - } - - Vector2 size = words[i].Length == 0 ? spaceSize : font.MeasureString(currentWord) * textScale; - - if (size.X > lineLength) - { - float splitSize = 0.0f; - List splitWord = new List() { string.Empty }; - int k = 0; - - for (int j = 0; j < currentWord.Length; j++) - { - splitWord[k] += currentWord[j]; - splitSize += (font.MeasureString(currentWord[j].ToString()) * textScale).X; - - if (splitSize + linePos > lineLength) - { - linePos = splitSize = 0.0f; - splitWord[k] = splitWord[k].Remove(splitWord[k].Length - 1) + "\n"; - if (splitWord[k].Length <= 1) { break; } - j--; - splitWord.Add(string.Empty); - k++; - } - } - - for (int j = 0; j < splitWord.Count; j++) - { - wrappedText.Append(splitWord[j]); - } - - linePos = splitSize; - } - else - { - if (linePos + size.X < lineLength) - { - wrappedText.Append(currentWord); - if (currentWord == "\n") - { - linePos = 0.0f; - } - else - { - linePos += size.X; - } - } - else - { - wrappedText.Append("\n"); - wrappedText.Append(currentWord); - - linePos = size.X; - } - } - } - - if (!playerInput) - { - return wrappedText.ToString().Replace(" \n ", "\n"); - } - else - { - return wrappedText.ToString(); - } - } + public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f) + => font.WrapText(text, lineLength / textScale); public static void ParseConnectCommand(string[] args, out string name, out string endpoint, out UInt64 lobbyId) { diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 5b8084010..26d363261 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.13.0 + 0.15.15.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -122,7 +122,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 0eda78d04..ee44c8626 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.13.0 + 0.15.15.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -123,7 +123,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index a09d8c2d7..cb3312d5f 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.13.0 + 0.15.15.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -126,7 +126,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index e0eefab46..83c32f101 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.13.0 + 0.15.15.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 44c6758d0..1e7744b49 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.13.0 + 0.15.15.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index c0998cfbc..8a6511484 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -60,5 +60,10 @@ namespace Barotrauma { GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateMoney }); } + + partial void OnTalentGiven(string talentIdentifier) + { + GameServer.Log($"{GameServer.CharacterLogName(this)} has gained the talent '{talentIdentifier}'", ServerLog.MessageType.Talent); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 81abdaf26..91e38cf04 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -29,6 +29,7 @@ namespace Barotrauma if (Character == null || Character.Removed) { return; } if (prevAmount != newAmount) { + GameServer.Log($"{GameServer.CharacterLogName(Character)} has gained {newAmount - prevAmount} experience ({prevAmount} -> {newAmount})", ServerLog.MessageType.Talent); GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateExperience }); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index ae7c4e523..80b8a45b2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -98,7 +98,7 @@ namespace Barotrauma { ColoredText msg = queuedMessages.Dequeue(); Messages.Add(msg); - if (GameSettings.SaveDebugConsoleLogs) + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { unsavedMessages.Add(msg); if (unsavedMessages.Count >= messagesPerFile) @@ -281,7 +281,7 @@ namespace Barotrauma { var msg = queuedMessages.Dequeue(); Messages.Add(msg); - if (GameSettings.SaveDebugConsoleLogs) + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { unsavedMessages.Add(msg); if (unsavedMessages.Count >= messagesPerFile) @@ -1315,7 +1315,7 @@ namespace Barotrauma commands.Add(new Command("sub|submarine", "submarine [name]: Select the submarine for the next round.", (string[] args) => { - SubmarineInfo sub = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); + SubmarineInfo sub = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.Equals(string.Join(" ", args), StringComparison.OrdinalIgnoreCase)); if (sub != null) { @@ -1377,7 +1377,7 @@ namespace Barotrauma commands.Add(new Command("endgame|endround|end", "end/endgame/endround: End the current round.", (string[] args) => { - if (Screen.Selected == GameMain.NetLobbyScreen) return; + if (Screen.Selected == GameMain.NetLobbyScreen) { return; } GameMain.Server.EndGame(); })); @@ -1399,11 +1399,18 @@ namespace Barotrauma commands.Add(new Command("eventdata", "", (string[] args) => { - if (args.Length == 0) return; - ServerEntityEvent ev = GameMain.Server.EntityEventManager.Events[Convert.ToUInt16(args[0])]; + if (args.Length == 0) { return; } + if (!UInt16.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out ushort eventId)) { return; } + ServerEntityEvent ev = GameMain.Server.EntityEventManager.Events.Find(ev => ev.ID == eventId); if (ev != null) { - NewMessage(ev.StackTrace.CleanupStackTrace(), Color.Lime); + string entityData = ""; + if (ev.Entity is { ID: var entityId, Removed: var removed, IdFreed: var idFreed }) + { + entityData = $"Entity ID: {entityId}; Entity removed: {removed}; Entity ID freed: {idFreed}"; + } + NewMessage($"EventData {eventId}\n{entityData}", Color.Lime); + //NewMessage(ev.StackTrace.CleanupStackTrace(), Color.Lime); } })); @@ -1578,16 +1585,13 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Character tpCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false); - if (tpCharacter == null) return; - - //var cam = GameMain.GameScreen.Cam; - tpCharacter.AnimController.CurrentHull = null; - tpCharacter.Submarine = null; - tpCharacter.AnimController.SetPosition(ConvertUnits.ToSimUnits(cursorWorldPos)); - tpCharacter.AnimController.FindHull(cursorWorldPos, true); - if (tpCharacter.AIController?.SteeringManager is IndoorsSteeringManager pathSteering) + if (tpCharacter != null) { - pathSteering.ResetPath(); + tpCharacter.TeleportTo(cursorWorldPos); + if (tpCharacter.AIController?.SteeringManager is IndoorsSteeringManager pathSteering) + { + pathSteering.ResetPath(); + } } } ); @@ -1779,7 +1783,7 @@ namespace Barotrauma List talentTrees = new List(); if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase)) { - talentTrees.AddRange(TalentTree.JobTalentTrees.Values); + talentTrees.AddRange(TalentTree.JobTalentTrees); } else { @@ -2370,6 +2374,16 @@ namespace Barotrauma GameMain.Server.CreateEntityEvent(wall); } })); + commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that stalls each file transfer packet by the specified duration.", (string[] args) => + { + float seconds = 0.0f; + if (args.Length > 0) + { + float.TryParse(args[0], out seconds); + } + GameMain.Server.FileSender.StallPacketsTime = seconds; + NewMessage("Set file transfer stall time to " + seconds); + })); #endif } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 76e83ca8b..5ac067bf3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -1,5 +1,4 @@ -using Barotrauma.Networking; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -8,6 +7,8 @@ namespace Barotrauma { private readonly bool[] teamDead = new bool[2]; + private List[] crews; + private bool initialized = false; public override string Description diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index ddaefc903..e0f044392 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -417,7 +417,7 @@ namespace Barotrauma SaveUtil.CleanUnnecessarySaveFiles(); - if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); } + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } MainThread = null; @@ -430,7 +430,7 @@ namespace Barotrauma stopwatch?.Start(); } - public CoroutineHandle ShowLoading(IEnumerable loader, bool waitKeyHit = true) + public CoroutineHandle ShowLoading(IEnumerable loader, bool waitKeyHit = true) { return CoroutineManager.StartCoroutine(loader); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index d3c2fbed0..30853fb52 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -264,7 +264,7 @@ namespace Barotrauma if (c.Inventory == null) { continue; } if (Level.Loaded.Type == LevelData.LevelType.Outpost && c.Submarine != Level.Loaded.StartOutpost) { - Map.CurrentLocation.RegisterTakenItems(c.Inventory.AllItems.Where(it => it.SpawnedInOutpost && it.OriginalModuleIndex > 0)); + Map.CurrentLocation.RegisterTakenItems(c.Inventory.AllItems.Where(it => it.SpawnedInCurrentOutpost && it.OriginalModuleIndex > 0)); } if (c.Info != null && c.IsBot) @@ -281,7 +281,7 @@ namespace Barotrauma } } - protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) { lastUpdateID++; @@ -365,7 +365,7 @@ namespace Barotrauma else { PendingSubmarineSwitch = null; - GameMain.Server.EndGame(TransitionType.None); + GameMain.Server.EndGame(TransitionType.None, wasSaved: false); LoadCampaign(GameMain.GameSession.SavePath); LastSaveID++; LastUpdateID++; @@ -376,7 +376,7 @@ namespace Barotrauma //-------------------------------------- - GameMain.Server.EndGame(transitionType); + GameMain.Server.EndGame(transitionType, wasSaved: true); ForceMapUI = false; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index 02bcb9320..be9d09f71 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components } } - private IEnumerable SendStateAfterDelay() + private IEnumerable SendStateAfterDelay() { while (sendStateTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs index d7331f33b..53d331b9c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components } } - private IEnumerable SendStateAfterDelay() + private IEnumerable SendStateAfterDelay() { while (sendStateTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs index 49db8e5ba..420b4c685 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs @@ -1,6 +1,4 @@ using Barotrauma.Networking; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -20,6 +18,7 @@ namespace Barotrauma.Items.Components public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { + msg.Write(user?.ID ?? 0); msg.Write(IsActive); msg.Write(progressTimer); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs index dc3a1d4ae..61dbbd7e5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs @@ -5,6 +5,8 @@ namespace Barotrauma.Items.Components { partial class Reactor { + const float NetworkUpdateIntervalLow = 10.0f; + private Client blameOnBroken; private float? nextServerLogWriteTime; @@ -17,19 +19,19 @@ namespace Barotrauma.Items.Components float fissionRate = msg.ReadRangedSingle(0.0f, 100.0f, 8); float turbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); - if (!item.CanClientAccess(c)) return; + if (!item.CanClientAccess(c)) { return; } IsActive = true; if (!autoTemp && AutoTemp) blameOnBroken = c; - if (turbineOutput < targetTurbineOutput) blameOnBroken = c; - if (fissionRate > targetFissionRate) blameOnBroken = c; + if (turbineOutput < TargetTurbineOutput) blameOnBroken = c; + if (fissionRate > TargetFissionRate) blameOnBroken = c; if (!_powerOn && powerOn) blameOnBroken = c; AutoTemp = autoTemp; _powerOn = powerOn; - targetFissionRate = fissionRate; - targetTurbineOutput = turbineOutput; + TargetFissionRate = fissionRate; + TargetTurbineOutput = turbineOutput; LastUser = c.Character; if (nextServerLogWriteTime == null) @@ -46,8 +48,8 @@ namespace Barotrauma.Items.Components msg.Write(autoTemp); msg.Write(_powerOn); msg.WriteRangedSingle(temperature, 0.0f, 100.0f, 8); - msg.WriteRangedSingle(targetFissionRate, 0.0f, 100.0f, 8); - msg.WriteRangedSingle(targetTurbineOutput, 0.0f, 100.0f, 8); + msg.WriteRangedSingle(TargetFissionRate, 0.0f, 100.0f, 8); + msg.WriteRangedSingle(TargetTurbineOutput, 0.0f, 100.0f, 8); msg.WriteRangedSingle(degreeOfSuccess, 0.0f, 1.0f, 8); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs index 39297e684..8a093a7d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components } } - private IEnumerable SendStateAfterDelay() + private IEnumerable SendStateAfterDelay() { while (sendStateTimer > 0.0f) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 517f51201..cbd3638ca 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -1,6 +1,7 @@ using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { @@ -19,17 +20,17 @@ namespace Barotrauma.Items.Components GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; - ShowOnDisplay(newOutputValue, addToHistory: true); + ShowOnDisplay(newOutputValue, addToHistory: true, TextColor); item.SendSignal(newOutputValue, "signal_out"); item.CreateServerEvent(this); } } - partial void ShowOnDisplay(string input, bool addToHistory) + partial void ShowOnDisplay(string input, bool addToHistory, Color color) { if (addToHistory) { - messageHistory.Add(input); + messageHistory.Add(new TerminalMessage(input, color)); while (messageHistory.Count > MaxMessages) { messageHistory.RemoveAt(0); @@ -41,7 +42,7 @@ namespace Barotrauma.Items.Components { //split too long messages to multiple parts int msgIndex = 0; - foreach (string str in messageHistory) + foreach (var (str, _) in messageHistory) { string msgToSend = str; if (string.IsNullOrEmpty(msgToSend)) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs new file mode 100644 index 000000000..2ebea696e --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class TriggerComponent : ItemComponent, IServerSerializable + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.WriteRangedSingle(CurrentForceFluctuation, 0.0f, 1.0f, 8); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index c7b6eb206..f936f258b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -280,7 +280,7 @@ namespace Barotrauma } msg.Write(body == null ? (byte)0 : (byte)body.BodyType); - msg.Write(SpawnedInOutpost); + msg.Write(SpawnedInCurrentOutpost); msg.Write(AllowStealing); msg.WriteRangedInteger(Quality, 0, Items.Components.Quality.MaxQuality); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index e7744bf45..dd99114fd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -103,7 +103,7 @@ namespace Barotrauma return; } - message.Write(false); + message.Write(false); //not a ballast flora update message.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); message.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index cfb20d850..c0ee6722f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -133,12 +133,14 @@ namespace Barotrauma.Networking public static bool IsValidName(string name, ServerSettings serverSettings) { + if (string.IsNullOrWhiteSpace(name)) { return false; } + char[] disallowedChars = new char[] { ';', ',', '<', '>', '/', '\\', '[', ']', '"', '?' }; - if (name.Any(c => disallowedChars.Contains(c))) return false; + if (name.Any(c => disallowedChars.Contains(c))) { return false; } foreach (char character in name) { - if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) return false; + if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) { return false; } } return true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 020725453..ea78a0062 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -108,6 +108,10 @@ namespace Barotrauma.Networking private readonly ServerPeer peer; +#if DEBUG + public float StallPacketsTime { get; set; } +#endif + public List ActiveTransfers { get { return activeTransfers; } @@ -264,6 +268,9 @@ namespace Barotrauma.Networking } peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); +#if DEBUG + transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime); +#endif } catch (Exception e) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 4425cbc2d..d374ae874 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -75,6 +75,11 @@ namespace Barotrauma.Networking private readonly ServerEntityEventManager entityEventManager; private FileSender fileSender; + + public FileSender FileSender + { + get { return fileSender; } + } #if DEBUG public void PrintSenderTransters() { @@ -139,7 +144,7 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(StartServer(isPublic)); } - private IEnumerable StartServer(bool isPublic) + private IEnumerable StartServer(bool isPublic) { bool error = false; try @@ -391,7 +396,7 @@ namespace Barotrauma.Networking character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); - Client owner = connectedClients.Find(c => c.EndpointMatches(character.OwnerClientEndPoint)); + Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && c.EndpointMatches(character.OwnerClientEndPoint)); if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > serverSettings.KillDisconnectedTime) { @@ -486,7 +491,7 @@ namespace Barotrauma.Networking else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode)) { #if !DEBUG - endRoundDelay = 1.0f; + endRoundDelay = 2.0f; endRoundTimer += deltaTime; #endif } @@ -517,7 +522,7 @@ namespace Barotrauma.Networking { Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage); } - EndGame(); + EndGame(wasSaved: false); return; } } @@ -896,7 +901,7 @@ namespace Barotrauma.Networking if (c.Connection == OwnerConnection) { SendDirectChatMessage(errorStr, c, ChatMessageType.MessageBox); - EndGame(); + EndGame(wasSaved: false); } else { @@ -989,8 +994,11 @@ namespace Barotrauma.Networking public override void CreateEntityEvent(INetSerializable entity, object[] extraData = null) { - if (!(entity is IServerSerializable)) throw new InvalidCastException("entity is not IServerSerializable"); - entityEventManager.CreateEvent(entity as IServerSerializable, extraData); + if (!(entity is IServerSerializable serverSerializable)) + { + throw new InvalidCastException($"Entity is not {nameof(IServerSerializable)}"); + } + entityEventManager.CreateEvent(serverSerializable, extraData); } private byte GetNewClientID() @@ -1129,6 +1137,8 @@ namespace Barotrauma.Networking lastRecvEntityEventID = (UInt16)(c.FirstNewEventID - 1); c.LastRecvEntityEventID = lastRecvEntityEventID; DebugConsole.Log("Finished midround syncing " + c.Name + " - switching from ID " + prevID + " to " + c.LastRecvEntityEventID); + //notify the client of the state of the respawn manager (so they show the respawn prompt if needed) + if (respawnManager != null) { CreateEntityEvent(respawnManager); } } else { @@ -1315,18 +1325,23 @@ namespace Barotrauma.Networking break; case ClientPermissions.ManageRound: bool end = inc.ReadBoolean(); + bool save = inc.ReadBoolean(); if (end) { if (gameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedOutpost) + if (mpCampaign != null && Level.IsLoadedOutpost && save) { mpCampaign.SavePlayers(); - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } - EndGame(); + else + { + save = false; + } + EndGame(wasSaved: save); } } else @@ -2005,9 +2020,9 @@ namespace Barotrauma.Networking if (initiatedStartGame || gameStarted) { return false; } Log("Starting a new round...", ServerLog.MessageType.ServerMessage); - SubmarineInfo selectedSub = null; SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; + SubmarineInfo selectedSub; if (serverSettings.Voting.AllowSubVoting) { selectedSub = serverSettings.Voting.HighestVoted(VoteType.Sub, connectedClients); @@ -2037,7 +2052,7 @@ namespace Barotrauma.Networking return true; } - private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) + private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) { initiatedStartGame = true; @@ -2079,7 +2094,6 @@ namespace Barotrauma.Networking while (fileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) { waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime; - yield return CoroutineStatus.Running; } } @@ -2090,7 +2104,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode, CampaignSettings settings) + private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode, CampaignSettings settings) { entityEventManager.Clear(); @@ -2474,7 +2488,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.UseRespawnShuttle); - msg.Write((byte)GameMain.Config.LosMode); + msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); serverSettings.WriteMonsterEnabled(msg); @@ -2498,6 +2512,7 @@ namespace Barotrauma.Networking int nextLocationIndex = campaign.Map.Locations.FindIndex(l => l.LevelData == campaign.NextLevel); int nextConnectionIndex = campaign.Map.Connections.FindIndex(c => c.LevelData == campaign.NextLevel); msg.Write(campaign.CampaignID); + msg.Write(campaign.LastSaveID); msg.Write(nextLocationIndex); msg.Write(nextConnectionIndex); msg.Write(campaign.Map.SelectedLocationIndex); @@ -2549,7 +2564,7 @@ namespace Barotrauma.Networking GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg); } - public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false) { if (!gameStarted) { @@ -2610,6 +2625,7 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ENDGAME); msg.Write((byte)transitionType); + msg.Write(wasSaved); msg.Write(endMessage); msg.Write((byte)missions.Count); foreach (Mission mission in missions) @@ -3247,7 +3263,7 @@ namespace Barotrauma.Networking ((float)EndVoteCount / (float)EndVoteMax) >= serverSettings.EndVoteRequiredRatio) { Log("Ending round by votes (" + EndVoteCount + "/" + (EndVoteMax - EndVoteCount) + ")", ServerLog.MessageType.ServerMessage); - EndGame(); + EndGame(wasSaved: false); } } @@ -3329,7 +3345,7 @@ namespace Barotrauma.Networking serverSettings.SaveClientPermissions(); } - private IEnumerable SendClientPermissionsAfterClientListSynced(Client recipient, Client client) + private IEnumerable SendClientPermissionsAfterClientListSynced(Client recipient, Client client) { DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); while (recipient.LastRecvClientListUpdate < LastClientListUpdateID) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index b0f520261..a0bab780e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -43,6 +43,8 @@ namespace Barotrauma get; private set; } = new Dictionary(); + + public int DangerousItemsContained { get; set; } } public bool TestMode = false; @@ -575,6 +577,25 @@ namespace Barotrauma } } + public void OnItemContained(Item containedItem, Item container, Character character) + { + if (containedItem == null || container == null || character == null || character.IsTraitor) { return; } + if (container.Prefab.Identifier == "weldingtool" && containedItem.HasTag("oxygensource")) + { + var client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + if (client == null) { return; } + float amount = -DangerousItemContainKarmaDecrease; + var memory = GetClientMemory(client); + if (IsDangerousItemContainKarmaDecreaseIncremental) + { + amount *= memory.DangerousItemsContained; + } + amount = Math.Max(amount, -MaxDangerousItemContainKarmaDecrease); + AdjustKarma(character, amount, "Put an oxygen tank inside a welding tool"); + clientMemories[client].DangerousItemsContained = memory.DangerousItemsContained + 1; + } + } + private void AdjustKarma(Character target, float amount, string debugKarmaChangeReason = "") { if (target == null) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 95aa0e83f..1feb6ffc1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -113,22 +113,7 @@ namespace Barotrauma.Networking public void CreateEvent(IServerSerializable entity, object[] extraData = null) { - if (entity == null || !(entity is Entity)) - { - DebugConsole.ThrowError("Can't create an entity event for " + entity + "!"); - return; - } - - if (((Entity)entity).Removed && !(entity is Level)) - { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n"+Environment.StackTrace.CleanupStackTrace()); - return; - } - if (((Entity)entity).IdFreed) - { - DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n"+Environment.StackTrace.CleanupStackTrace()); - return; - } + if (!ValidateEntity(entity)) { return; } var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1)); if (extraData != null) newEvent.SetData(extraData); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 1d2ab8e2e..840b4e972 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -49,6 +49,29 @@ namespace Barotrauma.Networking } } + private bool IsRespawnPromptPendingForClient(Client c) + { + if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; } + + if (!c.InGame) { return false; } + if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { return false; } + if (c.Character != null && !c.Character.IsDead) { return false; } + + var matchingData = campaign.GetClientCharacterData(c); + if (matchingData != null && matchingData.HasSpawned) + { + if (Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead)) + { + return false; + } + else if (!c.WaitForNextRoundRespawn.HasValue) + { + return true; + } + } + return false; + } + private List GetBotsToRespawn() { if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal) @@ -304,7 +327,7 @@ namespace Barotrauma.Networking c.WaitForNextRoundRespawn = null; var matchingData = campaign?.GetClientCharacterData(c); - if (matchingData != null && !matchingData.HasSpawned) + if (matchingData != null) { c.CharacterInfo = matchingData.CharacterInfo; } @@ -493,9 +516,9 @@ namespace Barotrauma.Networking if (characterInfo?.Job == null) { return; } foreach (Skill skill in characterInfo.Job.Skills) { - var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Prefab == s); + var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier.Equals(s.Identifier, StringComparison.OrdinalIgnoreCase)); if (skillPrefab == null) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.X, SkillReductionOnCampaignMidroundRespawn); + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.Start, SkillReductionOnCampaignMidroundRespawn); } } @@ -511,9 +534,14 @@ namespace Barotrauma.Networking msg.Write((float)(ReturnTime - DateTime.Now).TotalSeconds); break; case State.Waiting: + MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; + var matchingData = campaign?.GetClientCharacterData(c); + bool forceSpawnInMainSub = matchingData != null && !matchingData.HasSpawned; msg.Write((ushort)pendingRespawnCount); msg.Write((ushort)requiredRespawnCount); + msg.Write(IsRespawnPromptPendingForClient(c)); msg.Write(RespawnCountdownStarted); + msg.Write(forceSpawnInMainSub); msg.Write((float)(RespawnTime - DateTime.Now).TotalSeconds); break; case State.Returning: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 5da3ddd5f..7ac6e30e5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -55,6 +56,8 @@ namespace Barotrauma.Networking WriteExtraCargo(outMsg); + WriteHiddenSubs(outMsg); + Voting.ServerWrite(outMsg); if (c.HasPermission(Networking.ClientPermissions.ManageSettings)) @@ -127,6 +130,12 @@ namespace Barotrauma.Networking changed |= Whitelist.ServerAdminRead(incMsg, c); } + if (flags.HasFlag(NetFlags.HiddenSubs)) + { + ReadHiddenSubs(incMsg); + changed |= true; + } + if (flags.HasFlag(NetFlags.Misc)) { int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; @@ -205,6 +214,8 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); + doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); + doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => c.First + "-" + c.Second))); @@ -243,6 +254,11 @@ namespace Barotrauma.Networking SerializableProperties = SerializableProperty.DeserializeProperties(this, doc.Root); + if (string.IsNullOrEmpty(doc.Root.GetAttributeString("losmode", ""))) + { + LosMode = GameMain.Config.LosMode; + } + AutoRestart = doc.Root.GetAttributeBool("autorestart", false); Voting.AllowSubVoting = SubSelectionMode == SelectionMode.Vote; @@ -253,6 +269,8 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); + HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); + string[] defaultAllowedClientNameChars = new string[] { "32-33", @@ -337,6 +355,18 @@ namespace Barotrauma.Networking } } + public void SelectNonHiddenSubmarine() + { + if (HiddenSubs.Contains(GameMain.NetLobbyScreen.SelectedSub.Name)) + { + var candidates = GameMain.NetLobbyScreen.GetSubList().Where(s => !HiddenSubs.Contains(s.Name)).ToArray(); + if (candidates.Any()) + { + GameMain.NetLobbyScreen.SelectedSub = candidates.GetRandom(Rand.RandSync.Unsynced); + } + } + } + public void LoadClientPermissions() { ClientPermissions.Clear(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 889f82de4..ec62dc818 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -155,23 +155,23 @@ namespace Barotrauma msg.Write(allowSubVoting); if (allowSubVoting) { - List> voteList = GetVoteList(VoteType.Sub, GameMain.Server.ConnectedClients); + IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Sub, GameMain.Server.ConnectedClients); msg.Write((byte)voteList.Count); - foreach (Pair vote in voteList) + foreach (KeyValuePair vote in voteList) { - msg.Write((byte)vote.Second); - msg.Write(((SubmarineInfo)vote.First).Name); + msg.Write((byte)vote.Value); + msg.Write(vote.Key.Name); } } msg.Write(AllowModeVoting); if (allowModeVoting) { - List> voteList = GetVoteList(VoteType.Mode, GameMain.Server.ConnectedClients); + IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Mode, GameMain.Server.ConnectedClients); msg.Write((byte)voteList.Count); - foreach (Pair vote in voteList) + foreach (KeyValuePair vote in voteList) { - msg.Write((byte)vote.Second); - msg.Write(((GameModePreset)vote.First).Identifier); + msg.Write((byte)vote.Value); + msg.Write(vote.Key.Identifier); } } msg.Write(AllowEndVoting); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 03bbb4e15..43583e932 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -42,11 +42,11 @@ namespace Barotrauma #endif Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); - if(Console.IsOutputRedirected) + if (Console.IsOutputRedirected) { Console.WriteLine("Output redirection detected; colored text and command input will be disabled."); } - if(Console.IsInputRedirected) + if (Console.IsInputRedirected) { Console.WriteLine("Redirected input is detected but is not supported by this application. Input will be ignored."); } @@ -154,9 +154,9 @@ namespace Barotrauma sb.AppendLine("Last debug messages:"); DebugConsole.Clear(); - for (int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i-- ) + for (int i = DebugConsole.Messages.Count - 1; i > 0 && i > DebugConsole.Messages.Count - 15; i--) { - sb.AppendLine(" "+DebugConsole.Messages[i].Time+" - "+DebugConsole.Messages[i].Text); + sb.AppendLine(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text); } string crashReport = sb.ToString(); @@ -167,7 +167,9 @@ namespace Barotrauma } Console.Write(crashReport); - File.WriteAllText(filePath,sb.ToString()); + File.WriteAllText(filePath, sb.ToString()); + + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } if (GameSettings.SendUserStatistics) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index c24ba4ba1..77f81cc49 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -271,6 +271,8 @@ namespace Barotrauma var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; } + + GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); } } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 9454f5d56..7b8278d09 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.13.0 + 0.15.15.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index b2bb40191..2f110d253 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -78,6 +78,7 @@ + @@ -204,7 +205,8 @@ - + + diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index 4ab2df218..7467a8269 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -25,7 +25,10 @@ resetkarmabetweenrounds="true" dangerousitemstealkarmadecrease="15" dangerousitemstealbots="false" - ballastflorakarmaincrease="0.05" /> + ballastflorakarmaincrease="0.05" + dangerousitemcontainkarmadecrease="5.0" + isdangerousitemcontainkarmadecreaseincremental="true" + maxdangerousitemcontainkarmadecrease="30" /> + ballastflorakarmaincrease="0.03" + dangerousitemcontainkarmadecrease="5.0" + isdangerousitemcontainkarmadecreaseincremental="true" + maxdangerousitemcontainkarmadecrease="30" /> + ballastflorakarmaincrease="0.05" + dangerousitemcontainkarmadecrease="5.0" + isdangerousitemcontainkarmadecreaseincremental="true" + maxdangerousitemcontainkarmadecrease="30" /> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs index 060108b4b..8189c8fac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs @@ -64,7 +64,7 @@ namespace Barotrauma #endif } - private IEnumerable Update(ISpatialEntity targetEntity, Camera cam) + private IEnumerable Update(ISpatialEntity targetEntity, Camera cam) { if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index b105f540e..3522c45dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -229,7 +229,7 @@ namespace Barotrauma { if (sectorRad >= MathHelper.TwoPi) { return true; } Vector2 diff = worldPosition - WorldPosition; - return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f; + return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f; } public void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 9c32b8721..6e654e1cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -524,11 +524,12 @@ namespace Barotrauma if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } if (Character.CurrentHull == null) { return; } - bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; + bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen; bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) { + if (!Character.NeedsAir) { return false; } bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; Hull targetHull = gotoObjective.GetTargetHull(); return gotoObjective.Target != null && targetHull == null || @@ -567,6 +568,7 @@ namespace Barotrauma Character.AnimController.HeadInWater || Character.Submarine == null || (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || + ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; @@ -625,7 +627,7 @@ namespace Barotrauma if (removeDivingSuit) { var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); - if (divingSuit != null) + if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS)) { if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { @@ -950,7 +952,7 @@ namespace Barotrauma if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) { - if (item.Repairables.All(r => item.ConditionPercentage > r.RepairIconThreshold)) { continue; } + if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; } if (AddTargets(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { var orderPrefab = Order.GetPrefab("reportbrokendevices"); @@ -1117,6 +1119,7 @@ namespace Barotrauma // Don't react to attackers that are outside of the sub (e.g. AoE attacks) return; } + bool isAttackerFightingEnemy = false; if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) @@ -1136,7 +1139,6 @@ namespace Barotrauma } else { - (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); // Inform other NPCs if (cumulativeDamage > 1 || totalDamage >= 10) { @@ -1184,6 +1186,10 @@ namespace Barotrauma } } } + if (!isAttackerFightingEnemy) + { + (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); + } } } else @@ -1211,7 +1217,15 @@ namespace Barotrauma if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; } if (!otherHumanAI.IsFriendly(Character)) { continue; } bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); - if (!isWitnessing && !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; } + if (!isWitnessing) + { + //if the other character did not witness the attack, and the character is not within report range (or capable of reporting) + //don't react to the attack + if (Character.IsDead || Character.IsUnconscious || !CheckReportRange(Character, otherCharacter, ReportRange)) + { + continue; + } + } var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10); float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); otherHumanAI.AddCombatObjective(combatMode, attacker, delay); @@ -1244,18 +1258,20 @@ namespace Barotrauma return AIObjectiveCombat.CombatMode.None; } // If there are any enemies around, just ignore the friendly fire - if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) + if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) { + isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; } else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) { return Character.CombatAction.WitnessReaction; } - else if (Character.IsInstigator && attacker.IsPlayer) + else if (attacker.IsPlayer && FindInstigator() is Character instigator) { - // The guards don't react when the player attacks instigators. - return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); + // The guards don't react when the player there's an instigator around + isAttackerFightingEnemy = true; + return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) { @@ -1295,6 +1311,22 @@ namespace Barotrauma return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; } } + + Character FindInstigator() + { + if (Character.IsInstigator) + { + return Character; + } + else if (c.AIController is HumanAIController humanAi) + { + return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull)); + } + else + { + return null; + } + } } } } @@ -1416,15 +1448,20 @@ namespace Barotrauma return true; } - public static bool NeedsDivingGear(Hull hull, out bool needsSuit) + public bool NeedsDivingGear(Hull hull, out bool needsSuit) { + if (!Character.NeedsAir) + { + needsSuit = false; + return false; + } needsSuit = false; if (hull == null || hull.WaterPercentage > 90 || hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f)) { - needsSuit = true; + needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); return true; } if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) @@ -1570,9 +1607,9 @@ namespace Barotrauma Character thief = character; bool someoneSpoke = false; - bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false; + bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false; - if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) + if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1624,7 +1661,7 @@ namespace Barotrauma } } } - else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost && !item.AllowStealing, true) is { } foundItem) + else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem) { ItemTaken(foundItem, character); } @@ -1698,7 +1735,7 @@ namespace Barotrauma if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) { - if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; } + if (item.Repairables.All(r => r.IsBelowRepairThreshold)) { continue; } AddTargets(character, item); } } @@ -1786,7 +1823,7 @@ namespace Barotrauma { if (isCurrentHull) { - CurrentHullSafety = 0; + CurrentHullSafety = character.NeedsAir ? 0 : 100; } return CurrentHullSafety; } @@ -1809,8 +1846,8 @@ namespace Barotrauma private static float CalculateHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { - if (hull == null) { return 0; } - if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; } + if (hull == null) { return character.NeedsAir ? 0 : 100; } + if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; } // Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower. // With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD. float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index d2b1edb72..418b72362 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -204,11 +204,7 @@ namespace Barotrauma pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; - if (newPath.Unreachable || newPath.Nodes.None()) - { - useNewPath = false; - } - else if (!useNewPath && currentPath != null && currentPath.CurrentNode != null) + if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). if (IsIdenticalPath()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 69270a61e..88d3df439 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -12,20 +12,19 @@ namespace Barotrauma class LatchOntoAI { const float RaycastInterval = 5.0f; - private float raycastTimer; - - private Structure targetWall; private Body targetBody; private Vector2 attachSurfaceNormal; - private Submarine targetSubmarine; - private Character targetCharacter; private readonly Character character; public bool AttachToSub { get; private set; } public bool AttachToWalls { get; private set; } public bool AttachToCharacters { get; private set; } + public Submarine TargetSubmarine { get; private set; } + public Structure TargetWall { get; private set; } + public Character TargetCharacter { get; private set; } + private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown; private readonly float damageOnDetach, detachStun; private readonly bool weld; @@ -51,7 +50,7 @@ namespace Barotrauma public bool IsAttached => AttachJoints.Count > 0; - public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null; + public bool IsAttachedToSub => IsAttached && TargetSubmarine != null && TargetCharacter == null; public LatchOntoAI(XElement element, EnemyAIController enemyAI) { @@ -93,9 +92,9 @@ namespace Barotrauma var sub = wall.Submarine; if (sub == null) { return; } Reset(); - targetWall = wall; - targetSubmarine = sub; - targetBody = targetSubmarine.PhysicsBody.FarseerBody; + TargetWall = wall; + TargetSubmarine = sub; + targetBody = TargetSubmarine.PhysicsBody.FarseerBody; this.attachSurfaceNormal = attachSurfaceNormal; _attachPos = attachPos; } @@ -103,23 +102,20 @@ namespace Barotrauma public void SetAttachTarget(Character target) { if (!AttachToCharacters) { return; } + if (target.Submarine != character.Submarine) { return; } Reset(); - targetCharacter = target; - targetSubmarine = target.Submarine; + TargetCharacter = target; targetBody = target.AnimController.Collider.FarseerBody; attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition); } public void Update(EnemyAIController enemyAI, float deltaTime) { - if (character.Submarine != null) + if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine || + character.Submarine != null && TargetSubmarine != null && TargetCharacter == null) { - if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine || - character.Submarine != null && targetSubmarine != null && targetCharacter == null) - { - DeattachFromBody(reset: true); - return; - } + DeattachFromBody(reset: true); + return; } if (IsAttached) { @@ -150,7 +146,7 @@ namespace Barotrauma return; } } - if (targetCharacter != null) + if (TargetCharacter != null) { if (enemyAI.AttackingLimb?.attack == null) { @@ -159,10 +155,14 @@ namespace Barotrauma else { float range = enemyAI.AttackingLimb.attack.DamageRange * 2f; - if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) + if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) { DeattachFromBody(reset: true, cooldown: 1); } + else + { + TargetCharacter.Latchers.Add(this); + } } } } @@ -176,15 +176,15 @@ namespace Barotrauma deattachCheckTimer -= deltaTime; } - if (targetCharacter != null) + if (TargetCharacter != null) { // Own sim pos -> target where we are _attachPos = character.SimPosition; } Vector2 transformedAttachPos = _attachPos; - if (character.Submarine == null && targetSubmarine != null) + if (character.Submarine == null && TargetSubmarine != null) { - transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position); + transformedAttachPos += ConvertUnits.ToSimUnits(TargetSubmarine.Position); } if (transformedAttachPos != Vector2.Zero) { @@ -267,7 +267,7 @@ namespace Barotrauma if (enemyAI.AttackingLimb == null) { break; } if (targetBody == null) { break; } if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } - Vector2 referencePos = targetCharacter != null ? targetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); + Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { AttachToBody(transformedAttachPos); @@ -286,11 +286,11 @@ namespace Barotrauma deattach = true; attachCooldown = coolDown; } - if (!deattach && targetWall != null && targetSubmarine != null) + if (!deattach && TargetWall != null && TargetSubmarine != null) { // Deattach if the wall is broken enough where we are attached to - int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true); - if (enemyAI.CanPassThroughHole(targetWall, targetSection)) + int targetSection = TargetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true); + if (enemyAI.CanPassThroughHole(TargetWall, targetSection)) { deattach = true; attachCooldown = coolDown; @@ -298,7 +298,7 @@ namespace Barotrauma if (!deattach) { // Deattach if the velocity is high - float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length(); + float velocity = TargetSubmarine.Velocity == Vector2.Zero ? 0.0f : TargetSubmarine.Velocity.Length(); deattach = velocity > maxDeattachSpeed; if (!deattach) { @@ -385,11 +385,8 @@ namespace Barotrauma } as Joint; GameMain.World.Add(colliderJoint); - AttachJoints.Add(colliderJoint); - if (targetCharacter != null) - { - targetCharacter.Latchers.Add(this); - } + AttachJoints.Add(colliderJoint); + TargetCharacter?.Latchers.Add(this); if (maxAttachDuration > 0) { deattachCheckTimer = maxAttachDuration; @@ -407,25 +404,19 @@ namespace Barotrauma { attachCooldown = cooldown; } + TargetCharacter?.Latchers.Remove(this); if (reset) { Reset(); } - if (targetCharacter != null) - { - targetCharacter.Latchers.Remove(this); - } } private void Reset() { - if (targetCharacter != null) - { - targetCharacter.Latchers.Remove(this); - } - targetCharacter = null; - targetWall = null; - targetSubmarine = null; + TargetCharacter?.Latchers.Remove(this); + TargetCharacter = null; + TargetWall = null; + TargetSubmarine = null; targetBody = null; AttachPos = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 23bfc48ad..6986c8940 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -93,13 +93,6 @@ namespace Barotrauma _abandon = value; if (_abandon) { -#if DEBUG - if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder() && !objectiveManager.IsCurrentOrder()) - { - // TODO: dismiss - throw new Exception("Order abandoned!"); - } -#endif OnAbandon(); } } @@ -247,7 +240,7 @@ namespace Barotrauma } } - protected bool IsAllowed + public bool IsAllowed { get { @@ -271,7 +264,7 @@ namespace Barotrauma if (!IsAllowed) { Priority = 0; - Abandon = !isOrder; + Abandon = true; return Priority; } if (isOrder) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index f0cf653a8..dab9b3d1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -94,7 +94,7 @@ namespace Barotrauma if (item == null) { return false; } if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } - if (item.SpawnedInOutpost) { return false; } + if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if (item.ParentInventory != null) { if (item.Container == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 6bdddd863..857c56da9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -294,10 +294,6 @@ namespace Barotrauma } } - private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) => - weapon.HasRequiredContainedItems(character, addMessage: false) && - (!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); - private bool TryArm() { if (character.LockHands || Enemy == null) @@ -325,7 +321,7 @@ namespace Barotrauma Weapon = null; continue; } - if (IsLoaded(WeaponComponent, checkContainedItems: true)) + if (WeaponComponent.IsLoaded(character)) { // All good, the weapon is loaded break; @@ -380,6 +376,7 @@ namespace Barotrauma constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false) { AllowStealing = HumanAIController.IsMentallyUnstable, + EvaluateCombatPriority = false, // Use a custom formula instead GetItemPriority = i => { if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; } @@ -433,7 +430,7 @@ namespace Barotrauma // Not in the inventory anymore or cannot find the weapon component return false; } - if (!IsLoaded(WeaponComponent)) + if (!WeaponComponent.IsLoaded(character)) { // Try reloading (and seek ammo) if (!Reload(seekAmmo)) @@ -475,7 +472,7 @@ namespace Barotrauma foreach (var weapon in weaponList) { float priority = weapon.CombatPriority; - if (!IsLoaded(weapon)) + if (!weapon.IsLoaded(character)) { if (weapon is RangedWeapon && enemyIsClose) { @@ -564,31 +561,6 @@ namespace Barotrauma } return weaponComponent.Item; - static Attack GetAttackDefinition(ItemComponent weapon) - { - Attack attack = null; - if (weapon is MeleeWeapon meleeWeapon) - { - attack = meleeWeapon.Attack; - } - else if (weapon is RangedWeapon rangedWeapon) - { - attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack; - } - return attack; - } - - static float GetLethalDamage(ItemComponent weapon) - { - float lethalDmg = 0; - Attack attack = GetAttackDefinition(weapon); - if (attack != null) - { - lethalDmg = attack.GetTotalDamage(); - } - return lethalDmg; - } - float ApproximateStunDamage(ItemComponent weapon, Attack attack) { // Try to reduce the priority using the actual damage values and status effects. @@ -628,6 +600,31 @@ namespace Barotrauma } } + public static float GetLethalDamage(ItemComponent weapon) + { + float lethalDmg = 0; + Attack attack = GetAttackDefinition(weapon); + if (attack != null) + { + lethalDmg = attack.GetTotalDamage(); + } + return lethalDmg; + } + + private static Attack GetAttackDefinition(ItemComponent weapon) + { + Attack attack = null; + if (weapon is MeleeWeapon meleeWeapon) + { + attack = meleeWeapon.Attack; + } + else if (weapon is RangedWeapon rangedWeapon) + { + attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack; + } + return attack; + } + private HashSet FindWeaponsFromInventory() { weapons.Clear(); @@ -788,7 +785,6 @@ namespace Barotrauma { UsePathingOutside = false, IgnoreIfTargetDead = true, - DialogueIdentifier = "dialogcannotreachtarget", TargetName = Enemy.DisplayName, AlwaysUseEuclideanDistance = false }, @@ -812,7 +808,7 @@ namespace Barotrauma ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs"); if (prefab != null) { - Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true); + Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true); } } RemoveFollowTarget(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 5fa7abede..249a7818d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -144,7 +144,6 @@ namespace Barotrauma { TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear) { - DialogueIdentifier = "dialogcannotreachtarget", TargetName = container.Item.Name, AbortCondition = obj => container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index c42d0c6f0..5afa3665f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -19,10 +19,15 @@ namespace Barotrauma private AIObjectiveContainItem getOxygen; private Item targetItem; - public static float MIN_OXYGEN = 10; - public static string HEAVY_DIVING_GEAR = "deepdiving"; - public static string LIGHT_DIVING_GEAR = "lightdiving"; - public static string OXYGEN_SOURCE = "oxygensource"; + public const float MIN_OXYGEN = 10; + + public const string HEAVY_DIVING_GEAR = "deepdiving"; + public const string LIGHT_DIVING_GEAR = "lightdiving"; + /// + /// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear) + /// + public const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors"; + public const string OXYGEN_SOURCE = "oxygensource"; protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 2c8f6a40b..7dc9de1d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -46,10 +46,17 @@ namespace Barotrauma } if (character.CurrentHull == null) { - Priority = (objectiveManager.IsCurrentOrder() || - objectiveManager.IsCurrentOrder() || - objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat)) - && HumanAIController.HasDivingSuit(character) ? 0 : 100; + if (!character.NeedsAir) + { + Priority = 0; + } + else + { + Priority = (objectiveManager.IsCurrentOrder() || + objectiveManager.IsCurrentOrder() || + objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat)) + && HumanAIController.HasDivingSuit(character) ? 0 : 100; + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index afc834674..f42c4a725 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -1,6 +1,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; +using System.Collections.Immutable; using System.Collections.Generic; using System.Linq; @@ -11,6 +12,7 @@ namespace Barotrauma public override string Identifier { get; set; } = "get item"; public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool AllowMultipleInstances => true; public HashSet ignoredItems = new HashSet(); @@ -19,7 +21,7 @@ namespace Barotrauma public float TargetCondition { get; set; } = 1; public bool AllowDangerousPressure { get; set; } - private readonly string[] identifiersOrTags; + private readonly ImmutableArray identifiersOrTags; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs) private bool spawnItemIfNotFound = false; @@ -31,6 +33,7 @@ namespace Barotrauma public Item TargetItem => targetItem; private int currSearchIndex; public string[] ignoredContainerIdentifiers; + public string[] ignoredIdentifiersOrTags; private AIObjectiveGoTo goToObjective; private float currItemPriority; private readonly bool checkInventory; @@ -51,6 +54,10 @@ namespace Barotrauma public bool AllowVariants { get; set; } public bool Equip { get; set; } public bool Wear { get; set; } + public bool RequireLoaded { get; set; } + public bool EvaluateCombatPriority { get; set; } + public bool CheckPathForEachItem { get; set; } + public bool SpeakIfFails { get; set; } public InvSlotType? EquipSlotType { get; set; } @@ -67,18 +74,41 @@ namespace Barotrauma public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) : this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { } - public AIObjectiveGetItem(Character character, string[] identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) + public AIObjectiveGetItem(Character character, IEnumerable identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; Equip = equip; - this.identifiersOrTags = identifiersOrTags; this.spawnItemIfNotFound = spawnItemIfNotFound; - for (int i = 0; i < identifiersOrTags.Length; i++) - { - identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant(); - } this.checkInventory = checkInventory; + this.identifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray(); + ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray(); + } + + public static IEnumerable ParseGearTags(IEnumerable identifiersOrTags) + { + var tags = new List(); + foreach (string tag in identifiersOrTags) + { + if (!tag.Contains('!')) + { + tags.Add(tag.ToLowerInvariant()); + } + } + return tags; + } + + public static IEnumerable ParseIgnoredTags(IEnumerable identifiersOrTags) + { + var ignoredTags = new List(); + foreach (string tag in identifiersOrTags) + { + if (tag.Contains('!')) + { + ignoredTags.Add(tag.Remove("!").ToLowerInvariant()); + } + } + return ignoredTags; } private bool CheckInventory() @@ -219,6 +249,13 @@ namespace Barotrauma } else { + if (!Equip) + { + // Try equipping and wearing the item + Wear = true; + Equip = true; + return; + } #if DEBUG DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red); #endif @@ -243,6 +280,10 @@ namespace Barotrauma { // Try again ignoredItems.Add(targetItem); + if (targetItem != moveToTarget && moveToTarget is Item item) + { + ignoredItems.Add(item); + } ResetInternal(); } else @@ -269,7 +310,15 @@ namespace Barotrauma } float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100); - bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective); + if (!CheckPathForEachItem) + { + // While following the player, let's ensure that there's a valid path to the target before accepting it. + // Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can. + // This is relatively expensive, so let's do this only when it significantly improves the behavior. + // Only allow one path find call per frame. + CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective); + } + bool checkPath = CheckPathForEachItem; bool hasCalledPathFinder = false; int itemsPerFrame = (int)priority; for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++) @@ -280,14 +329,20 @@ namespace Barotrauma if (itemSub == null) { continue; } Submarine mySub = character.Submarine; if (mySub == null) { continue; } + if (!checkInventory) + { + // Ignore items in the inventory when defined not to check it. + if (item.IsOwnedBy(character)) { continue; } + } if (!AllowStealing) { - if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; } + if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; } } if (!CheckItem(item)) { continue; } if (item.Container != null) { if (item.Container.HasTag("donttakeitems")) { continue; } + if (ignoredItems.Contains(item.Container)) { continue; } if (ignoredContainerIdentifiers != null) { if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } @@ -315,17 +370,51 @@ namespace Barotrauma float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); yDist = yDist > 100 ? yDist * 5 : 0; float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist)); + float minDistFactor = EvaluateCombatPriority ? 0.1f : 0; + float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist)); itemPriority *= distanceFactor; - itemPriority *= item.Condition / item.MaxCondition; + if (EvaluateCombatPriority) + { + var mw = item.GetComponent(); + var rw = item.GetComponent(); + float combatFactor = 0; + if (mw != null) + { + if (mw.CombatPriority > 0) + { + combatFactor = mw.CombatPriority / 100; + } + else + { + // The combat factor of items with zero combat priority is not allowed to be greater than 0.1f + combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(mw) / 1000, 0.1f); + } + } + else if (rw != null) + { + if (rw.CombatPriority > 0) + { + combatFactor = rw.CombatPriority / 100; + } + else + { + combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(rw) / 1000, 0.1f); + } + } + else + { + combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f); + } + itemPriority *= combatFactor; + } + else + { + itemPriority *= item.Condition / item.MaxCondition; + } // Ignore if the item has a lower priority than the currently selected one if (itemPriority < currItemPriority) { continue; } if (!hasCalledPathFinder && PathSteering != null && checkPath) { - // While following the player, let's ensure that there's a valid path to the target before accepting it. - // Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can. - // This is relatively expensive, so let's do this only when it significantly improves the behavior. - // Only allow one path find call per frame. hasCalledPathFinder = true; var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); if (path.Unreachable) { continue; } @@ -355,7 +444,7 @@ namespace Barotrauma targetItem = spawnedItem; if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) { - spawnedItem.SpawnedInOutpost = true; + spawnedItem.SpawnedInCurrentOutpost = true; } }); } @@ -365,7 +454,6 @@ namespace Barotrauma #if DEBUG DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow); #endif - SpeakCannotFind(); Abandon = true; } } @@ -375,34 +463,19 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } - if (targetItem != null) + if (targetItem == null) { - if (Equip && EquipSlotType.HasValue) - { - return character.HasEquippedItem(targetItem, EquipSlotType.Value); - } - else - { - return character.HasItem(targetItem, Equip); - } - } - else if (identifiersOrTags != null) - { - var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true); - if (matchingItem != null) - { - if (Equip && EquipSlotType.HasValue) - { - return character.HasEquippedItem(matchingItem, EquipSlotType.Value); - } - else - { - return !Equip || character.HasEquippedItem(matchingItem); - } - } + // Not yet ready return false; } - return false; + if (Equip && EquipSlotType.HasValue) + { + return character.HasEquippedItem(targetItem, EquipSlotType.Value); + } + else + { + return character.HasItem(targetItem, Equip); + } } private bool CheckItem(Item item) @@ -410,8 +483,10 @@ namespace Barotrauma if (!item.IsInteractable(character)) { return false; } if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; + if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } + if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; } return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id)); } @@ -437,15 +512,20 @@ namespace Barotrauma protected override void OnAbandon() { base.OnAbandon(); - if (moveToTarget == null) { return; } + if (moveToTarget != null) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow); #endif + } + if (SpeakIfFails) + { + SpeakCannotFind(); + } } private void SpeakCannotFind() { - // TODO: Use the item name as the variable here. if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) { string msg = TextManager.Get("dialogcannotfinditem", true); @@ -455,19 +535,5 @@ namespace Barotrauma } } } - - // TODO: remove? - private void SpeakCannotReach() - { - if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) - { - string TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString(); - string msg = TargetName == null ? TextManager.Get("dialogcannotreachtarget", true) : TextManager.GetWithVariable("dialogcannotreachtarget", "[name]", TargetName, formatCapitals: !(moveToTarget is Character)); - if (msg != null) - { - character.Speak(msg, identifier: "dialogcannotreachtarget", minDurationBetweenSimilar: 20.0f); - } - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs new file mode 100644 index 000000000..ee84492d9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -0,0 +1,91 @@ +#nullable enable +using Barotrauma.Extensions; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + class AIObjectiveGetItems : AIObjective + { + public override string Identifier { get; set; } = "get items"; + public override string DebugTag => $"{Identifier}"; + public override bool KeepDivingGearOn => true; + + public bool AllowStealing { get; set; } + public bool TakeWholeStack { get; set; } + public bool AllowVariants { get; set; } + public bool Equip { get; set; } + public bool Wear { get; set; } + public bool CheckInventory { get; set; } + public bool EvaluateCombatPriority { get; set; } + public bool CheckPathForEachItem { get; set; } + public bool RequireLoaded { get; set; } + + private readonly ImmutableArray gearTags; + private readonly string[] ignoredTags; + private bool subObjectivesCreated; + + public readonly HashSet achievedItems = new HashSet(); + + public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) + { + gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray(); + ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray(); + } + + protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None(); + + protected override void Act(float deltaTime) + { + if (character.LockHands) + { + Abandon = true; + return; + } + if (!subObjectivesCreated) + { + foreach (string tag in gearTags) + { + AIObjectiveGetItem? getItem = null; + TryAddSubObjective(ref getItem, () => + new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory) + { + AllowVariants = AllowVariants, + Wear = Wear, + TakeWholeStack = TakeWholeStack, + AllowStealing = AllowStealing, + ignoredIdentifiersOrTags = ignoredTags, + CheckPathForEachItem = CheckPathForEachItem, + RequireLoaded = RequireLoaded + }, + onCompleted: () => + { + var item = getItem?.TargetItem; + if (item?.IsOwnedBy(character) != null) + { + achievedItems.Add(item); + } + }, + onAbandon: () => + { + var item = getItem?.TargetItem; + if (item != null) + { + achievedItems.Remove(item); + } + RemoveSubObjective(ref getItem); + }); + } + subObjectivesCreated = true; + } + } + + public override void Reset() + { + base.Reset(); + subObjectivesCreated = false; + achievedItems.Clear(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 154e88ec9..17930516a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -27,6 +27,7 @@ namespace Barotrauma public bool isFollowOrderObjective; public bool mimic; public bool SpeakIfFails { get; set; } = true; + public bool DebugLogWhenFails { get; set; } = true; public bool UsePathingOutside { get; set; } = true; public float extraDistanceWhileSwimming; @@ -61,6 +62,9 @@ namespace Barotrauma } } + // TODO: Currently we never check the visibility (to the end node), which is actually unintentional. + // I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better. + // However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true. public bool CheckVisibility { get; set; } public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } @@ -77,7 +81,7 @@ namespace Barotrauma public override bool AllowOutsideSubmarine => AllowGoingOutside; public override bool AllowInAnySub => true; - public string DialogueIdentifier { get; set; } + public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget"; public string TargetName { get; set; } public ISpatialEntity Target { get; private set; } @@ -149,7 +153,10 @@ namespace Barotrauma private void SpeakCannotReach() { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); + if (DebugLogWhenFails) + { + DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); + } #endif if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails) { @@ -170,17 +177,12 @@ namespace Barotrauma Abandon = true; return; } - if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy)) + if (cannotFollow || Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy)) { // Wait character.AIController.SteeringManager.Reset(); return; } - if (cannotFollow) - { - // Wait - character.AIController.SteeringManager.Reset(); - } if (!character.IsClimbing) { character.SelectedConstruction = null; @@ -211,26 +213,33 @@ namespace Barotrauma } bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; bool isInside = character.CurrentHull != null; - bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes); - if (isInside && targetIsOutside && !AllowGoingOutside) + bool hasOutdoorNodes = insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes; + if (isInside && hasOutdoorNodes && !AllowGoingOutside) { Abandon = true; } - else if (HumanAIController.IsCurrentPathNullOrUnreachable) + else if (HumanAIController.SteeringManager == PathSteering) { waitUntilPathUnreachable -= deltaTime; - SteeringManager.Reset(); - if (waitUntilPathUnreachable < 0) + if (HumanAIController.IsCurrentPathNullOrUnreachable) + { + SteeringManager.Reset(); + if (waitUntilPathUnreachable < 0) + { + waitUntilPathUnreachable = pathWaitingTime; + if (repeat) + { + SpeakCannotReach(); + } + else + { + Abandon = true; + } + } + } + else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false)) { waitUntilPathUnreachable = pathWaitingTime; - if (repeat) - { - SpeakCannotReach(); - } - else - { - Abandon = true; - } } } if (!Abandon) @@ -238,16 +247,16 @@ namespace Barotrauma if (getDivingGearIfNeeded && !character.LockHands) { Character followTarget = Target as Character; - bool needsDivingSuit = !isInside || targetIsOutside; - bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); + bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); + bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir; if (mimic) { - if (HumanAIController.HasDivingSuit(followTarget)) + if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir) { needsDivingGear = true; needsDivingSuit = true; } - else if (HumanAIController.HasDivingMask(followTarget)) + else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir) { needsDivingGear = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 383264b6f..443d8b340 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -323,7 +323,6 @@ namespace Barotrauma SortObjectives(); } - private CoroutineHandle speakRoutine; public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak) { if (character.IsDead) @@ -379,6 +378,7 @@ namespace Barotrauma var newCurrentOrder = CreateObjective(order, option, orderGiver); if (newCurrentOrder != null) { + newCurrentOrder.Abandoned += () => DismissSelf(order, option); CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder)); } if (!HasOrders()) @@ -386,53 +386,12 @@ namespace Barotrauma // Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding) CreateAutonomousObjectives(); } - else + else if (newCurrentOrder != null) { - // This should be redundant, because all the objectives are reset when they are selected as active. - newCurrentOrder?.Reset(); - if (speak && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f); - //if (speakRoutine != null) - //{ - // CoroutineManager.StopCoroutines(speakRoutine); - //} - //speakRoutine = CoroutineManager.InvokeAfter(() => - //{ - // if (GameMain.GameSession == null || Level.Loaded == null) { return; } - // if (newCurrentOrder != null && character.SpeechImpediment < 100.0f) - // { - // if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None()) - // { - // character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets"); - // } - // else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None()) - // { - // character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries"); - // } - // else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None()) - // { - // character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire"); - // } - // else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None()) - // { - // character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks"); - // } - // else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None()) - // { - // character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies"); - // } - // else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) - // { - // character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); - // } - // else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) - // { - // character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps"); - // } - // } - //}, 3); + string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative"); + character.Speak(msg, delay: 1.0f); } } } @@ -465,7 +424,6 @@ namespace Barotrauma break; case "return": newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier); - newObjective.Abandoned += () => DismissSelf(order, option); newObjective.Completed += () => DismissSelf(order, option); break; case "fixleaks": @@ -491,12 +449,10 @@ namespace Barotrauma if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier) { - IsLoop = true, + IsLoop = false, Override = orderGiver != null && orderGiver.IsCommanding }; - // ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs, - // if we want that the bot just switches the pump on/off and continues doing something else. - // If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done. + newObjective.Completed += () => DismissSelf(order, option); } else { @@ -566,6 +522,26 @@ namespace Barotrauma case "escapehandcuffs": newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier); break; + case "prepareforexpedition": + newObjective = new AIObjectivePrepare(character, this, order.TargetItems) + { + KeepActiveWhenReady = true, + CheckInventory = true, + Equip = false, + FindAllItems = true + }; + break; + case "findweapon": + newObjective = new AIObjectivePrepare(character, this, order.TargetItems) + { + KeepActiveWhenReady = false, + CheckInventory = false, + Equip = true, + EvaluateCombatPriority = true, + FindAllItems = false + }; + newObjective.Completed += () => DismissSelf(order, option); + break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 5db86b049..82d572d2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -213,7 +213,6 @@ namespace Barotrauma { TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50) { - DialogueIdentifier = "dialogcannotreachtarget", TargetName = target.Item.Name, endNodeFilter = node => node.Waypoint.Ladders == null }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs new file mode 100644 index 000000000..a7edab712 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -0,0 +1,150 @@ +#nullable enable +using Barotrauma.Extensions; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + class AIObjectivePrepare : AIObjective + { + public override string Identifier { get; set; } = "prepare"; + public override string DebugTag => $"{Identifier}"; + public override bool KeepDivingGearOn => true; + + private AIObjectiveGetItem? getSingleItemObjective; + private AIObjectiveGetItems? getMultipleItemsObjective; + private bool subObjectivesCreated; + private readonly ImmutableArray gearTags; + private readonly HashSet items = new HashSet(); + public bool KeepActiveWhenReady { get; set; } + public bool CheckInventory { get; set; } + public bool FindAllItems { get; set; } + public bool Equip { get; set; } + public bool EvaluateCombatPriority { get; set; } + + private AIObjective? GetSubObjective() => getSingleItemObjective ?? getMultipleItemsObjective as AIObjective; + + public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable items, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + gearTags = items.ToImmutableArray(); + } + + protected override bool CheckObjectiveSpecific() => IsCompleted; + + protected override float GetPriority() + { + if (!IsAllowed) + { + Priority = 0; + Abandon = true; + return Priority; + } + Priority = objectiveManager.GetOrderPriority(this); + var subObjective = GetSubObjective(); + if (subObjective != null && subObjective.IsCompleted) + { + Priority = 0; + items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character)); + if (items.None()) + { + Abandon = true; + + } + else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character)))) + { + Reset(); + } + } + return Priority; + } + + protected override void Act(float deltaTime) + { + if (character.LockHands) + { + Abandon = true; + return; + } + if (!subObjectivesCreated) + { + if (FindAllItems) + { + if (!TryAddSubObjective(ref getMultipleItemsObjective, () => new AIObjectiveGetItems(character, objectiveManager, gearTags) + { + CheckInventory = CheckInventory, + Equip = Equip, + EvaluateCombatPriority = EvaluateCombatPriority, + RequireLoaded = true + }, + onCompleted: () => + { + if (KeepActiveWhenReady) + { + if (getMultipleItemsObjective != null) + { + foreach (var item in getMultipleItemsObjective.achievedItems) + { + if (item?.IsOwnedBy(character) != null) + { + items.Add(item); + } + } + } + } + else + { + IsCompleted = true; + } + }, + onAbandon: () => Abandon = true)) + { + Abandon = true; + } + } + else + { + if (!TryAddSubObjective(ref getSingleItemObjective, () => new AIObjectiveGetItem(character, gearTags, objectiveManager, equip: Equip, checkInventory: CheckInventory) + { + EvaluateCombatPriority = EvaluateCombatPriority, + SpeakIfFails = true, + RequireLoaded = true + }, + onCompleted: () => + { + if (KeepActiveWhenReady) + { + if (getSingleItemObjective != null) + { + var item = getSingleItemObjective?.TargetItem; + if (item?.IsOwnedBy(character) != null) + { + items.Add(item); + } + } + } + else + { + IsCompleted = true; + } + }, + onAbandon: () => Abandon = true)) + { + Abandon = true; + } + } + subObjectivesCreated = true; + } + } + + public override void Reset() + { + base.Reset(); + items.Clear(); + subObjectivesCreated = false; + RemoveSubObjective(ref getMultipleItemsObjective); + RemoveSubObjective(ref getSingleItemObjective); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index a6132f8cb..cd6b24016 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -107,7 +107,7 @@ namespace Barotrauma { foreach (RelatedItem requiredItem in kvp.Value) { - var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true) + var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, equip: true) { AllowVariants = requiredItem.AllowVariants }; @@ -219,8 +219,7 @@ namespace Barotrauma { // Don't stop in ladders, because we can't interact with other items while holding the ladders. endNodeFilter = node => node.Waypoint.Ladders == null, - // Allow repairing hatches and airlock doors. - AllowGoingOutside = HumanAIController.ObjectiveManager.IsCurrentOrder() && Item.GetComponent() != null + TargetName = Item.Name }; if (repairTool != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 58bc98213..92f1e67e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -91,8 +91,7 @@ namespace Barotrauma public static bool NearlyFullCondition(Item item) { - float condition = item.ConditionPercentage; - return item.Repairables.All(r => condition >= r.RepairThreshold); + return item.Repairables.All(r => !r.IsBelowRepairThreshold); } protected override float TargetEvaluation() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 3d7b364ca..0da0eecd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -319,9 +319,32 @@ namespace Barotrauma { itemListStr = itemNameList[0]; } + else if (itemNameList.Count == 2) + { + //[treatment1] or [treatment2] + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsLast", + new string[] { "[treatment1]", "[treatment2]" }, + new string[] { itemNameList[0], itemNameList[1] }); + } else { - itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last()); + //[treatment1], [treatment2], [treatment3] ... or [treatmentx] + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsFirst", + new string[] { "[treatment1]", "[treatment2]" }, + new string[] { itemNameList[0], itemNameList[1] }); + for (int i = 2; i < itemNameList.Count - 1; i++) + { + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsFirst", + new string[] { "[treatment1]", "[treatment2]" }, + new string[] { itemListStr, itemNameList[i] }); + } + itemListStr = TextManager.GetWithVariables( + "DialogRequiredTreatmentOptionsLast", + new string[] { "[treatment1]", "[treatment2]" }, + new string[] { itemListStr, itemNameList.Last() }); } if (targetCharacter != character && character.IsOnPlayerTeam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index eb7504c08..d27ef7b37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -79,6 +79,7 @@ namespace Barotrauma { if (target == null || target.IsDead || target.Removed) { return false; } if (target.IsInstigator) { return false; } + if (target.IsPet) { return false; } if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; } if (character.AIController is HumanAIController humanAI) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 57b024e16..694b78aa2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -170,6 +170,7 @@ namespace Barotrauma public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1; public bool IsPrefab { get; private set; } public readonly bool MustManuallyAssign; + public readonly bool AutoDismiss; public readonly OrderTarget TargetPosition; @@ -363,6 +364,7 @@ namespace Barotrauma MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); + AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 37a5a7029..7012a205f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1048,7 +1048,8 @@ namespace Barotrauma void UpdateClimbing() { - if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent() == null || character.IsIncapacitated) + var ladder = character.SelectedConstruction?.GetComponent(); + if (ladder == null || character.IsIncapacitated) { Anim = Animation.None; return; @@ -1075,22 +1076,29 @@ namespace Barotrauma if (leftHand == null || rightHand == null || head == null || torso == null) { return; } Vector2 ladderSimPos = ConvertUnits.ToSimUnits( - character.SelectedConstruction.Rect.X + character.SelectedConstruction.Rect.Width / 2.0f, - character.SelectedConstruction.Rect.Y); + ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f, + ladder.Item.Rect.Y); - Vector2 ladderSimSize = ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Size.ToVector2()); + Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2()); + + float lowestLadderSimPos = ladderSimPos.Y - ladderSimPos.Y; + var lowestNearbyLadder = GetLowestNearbyLadder(ladder); + if (lowestNearbyLadder != null && lowestNearbyLadder != ladder) + { + ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y)); + } float stepHeight = ConvertUnits.ToSimUnits(30.0f); - if (currentHull == null && character.SelectedConstruction.Submarine != null) + if (currentHull == null && ladder.Item.Submarine != null) { - ladderSimPos += character.SelectedConstruction.Submarine.SimPosition; + ladderSimPos += ladder.Item.Submarine.SimPosition; } - else if (currentHull?.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine && character.SelectedConstruction.Submarine != null) + else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null) { - ladderSimPos += character.SelectedConstruction.Submarine.SimPosition - currentHull.Submarine.SimPosition; + ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition; } - else if (currentHull?.Submarine != null && character.SelectedConstruction.Submarine == null) + else if (currentHull?.Submarine != null && ladder.Item.Submarine == null) { ladderSimPos -= currentHull.Submarine.SimPosition; } @@ -1174,25 +1182,35 @@ namespace Barotrauma float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI; movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor)); - Vector2 subSpeed = currentHull != null || character.SelectedConstruction.Submarine == null - ? Vector2.Zero : character.SelectedConstruction.Submarine.Velocity; + Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null + ? Vector2.Zero : ladder.Item.Submarine.Velocity; Vector2 climbForce = new Vector2(0.0f, movement.Y + 0.3f) * movementFactor; + //reached the top of the ladders -> can't go further up if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); } + //reached the bottom -> can't go further down + float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.height; + if (floorFixture != null && + !floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) && + !floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) && + character.SimPosition.Y < standOnFloorY + minHeightFromFloor) + { + climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f); + } //apply forces to the collider to move the Character up/down Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); - if (!character.SelectedConstruction.Prefab.Triggers.Any()) + if (!ladder.Item.Prefab.Triggers.Any()) { character.SelectedConstruction = null; return; } - Rectangle trigger = character.SelectedConstruction.Prefab.Triggers.FirstOrDefault(); - trigger = character.SelectedConstruction.TransformTrigger(trigger); + Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault(); + trigger = ladder.Item.TransformTrigger(trigger); bool isRemote = false; bool isClimbing = true; @@ -1221,6 +1239,19 @@ namespace Barotrauma character.SelectedConstruction = null; IgnorePlatforms = false; } + + Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f) + { + foreach (Ladder ladder in Ladder.List) + { + if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; } + if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; } + if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; } + if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; } + return ladder; + } + return null; + } } void UpdateDying(float deltaTime) @@ -1576,6 +1607,7 @@ namespace Barotrauma Vector2 shoulderPos = rightShoulder.WorldAnchorA; Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY; + if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; } targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength); targetForce = 200.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 5913768c7..9dafa66bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -121,6 +121,7 @@ namespace Barotrauma protected Vector2 overrideTargetMovement; protected float floorY, standOnFloorY; + protected Fixture floorFixture; protected Vector2 floorNormal = Vector2.UnitY; protected float surfaceY; @@ -488,6 +489,7 @@ namespace Barotrauma limbDictionary = new Dictionary(); limbs = new Limb[RagdollParams.Limbs.Count]; RagdollParams.Limbs.ForEach(l => AddLimb(l)); + if (limbs.Contains(null)) { return; } SetupDrawOrder(); } @@ -548,19 +550,23 @@ namespace Barotrauma byte ID = Convert.ToByte(limbParams.ID); Limb limb = new Limb(this, character, limbParams); limb.body.FarseerBody.OnCollision += OnLimbCollision; + if (ID >= Limbs.Length) + { + throw new Exception($"Failed to add a limb to the character \"{Character?.ConfigPath ?? "null"}\" (limb index {ID} out of bounds). The ragdoll file may be configured incorrectly."); + } Limbs[ID] = limb; Mass += limb.Mass; - if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb); + if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); } } public void AddLimb(Limb limb) { - if (Limbs.Contains(limb)) return; + if (Limbs.Contains(limb)) { return; } limb.body.FarseerBody.OnCollision += OnLimbCollision; Array.Resize(ref limbs, Limbs.Length + 1); Limbs[Limbs.Length - 1] = limb; Mass += limb.Mass; - if (!limbDictionary.ContainsKey(limb.type)) limbDictionary.Add(limb.type, limb); + if (!limbDictionary.ContainsKey(limb.type)) { limbDictionary.Add(limb.type, limb); } SetupDrawOrder(); } @@ -923,8 +929,8 @@ namespace Barotrauma } Hull newHull = Hull.FindHull(findPos, currentHull); - - if (newHull == currentHull) return; + + if (newHull == currentHull) { return; } if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine)) { @@ -957,16 +963,16 @@ namespace Barotrauma if (newHull?.Submarine == null && currentHull?.Submarine != null) { //don't teleport out yet if the character is going through a gap - if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) { return; } if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; } + if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; } character.MemLocalState?.Clear(); - Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity); + Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity, detachProjectiles: false); } //out -> in else if (currentHull == null && newHull.Submarine != null) { character.MemLocalState?.Clear(); - Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity); + Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity, detachProjectiles: false); } //from one sub to another else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine) @@ -974,13 +980,13 @@ namespace Barotrauma character.MemLocalState?.Clear(); Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position; Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position; - - Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero); + Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero, detachProjectiles: false); } } CurrentHull = newHull; character.Submarine = currentHull?.Submarine; + character.AttachedProjectiles.ForEach(p => p?.Item?.UpdateTransform()); } private void PreventOutsideCollision() @@ -1013,7 +1019,7 @@ namespace Barotrauma } } - public void Teleport(Vector2 moveAmount, Vector2 velocityChange) + public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true) { foreach (Limb limb in Limbs) { @@ -1036,7 +1042,7 @@ namespace Barotrauma character.DisableImpactDamageTimer = 0.25f; - SetPosition(Collider.SimPosition + moveAmount); + SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles); character.CursorPosition += moveAmount; Collider?.UpdateDrawPosition(); @@ -1500,6 +1506,7 @@ namespace Barotrauma { onGround = false; Stairs = null; + floorFixture = null; Vector2 rayStart = simPosition; float height = ColliderHeightFromFloor; if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); } @@ -1572,6 +1579,7 @@ namespace Barotrauma if (standOnFloorFixture != null && !IsHanging) { + floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f) { @@ -1612,7 +1620,7 @@ namespace Barotrauma } } - public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false) + public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) { if (!MathUtils.IsValid(simPosition)) { @@ -1625,13 +1633,28 @@ namespace Barotrauma } if (MainLimb == null) { return; } + // A Work-around for an issue with teleporting the characters: + // Detach every latcher when either one of the latchers or the target is teleported, + // because otherwise all the characters are teleported to invalid positions. if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) { + var target = enemyAI.LatchOntoAI.TargetCharacter; + if (target != null) + { + target.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); + target.Latchers.Clear(); + } enemyAI.LatchOntoAI.DeattachFromBody(reset: true); } - Character.Latchers.ForEachMod(l => l.DeattachFromBody(reset: true)); + Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); Character.Latchers.Clear(); + if (detachProjectiles) + { + character.AttachedProjectiles.ForEachMod(p => p?.Unstick()); + character.AttachedProjectiles.Clear(); + } + Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { @@ -1712,7 +1735,7 @@ namespace Barotrauma if (distSqrd > resetDist * resetDist) { //ragdoll way too far, reset position - SetPosition(Collider.SimPosition, true, forceMainLimbToCollider: true); + SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true); } if (distSqrd > allowedDist * allowedDist) { @@ -1732,7 +1755,7 @@ namespace Barotrauma else if (collisionsDisabled) { //set the position of the ragdoll to make sure limbs don't get stuck inside walls when re-enabling collisions - SetPosition(Collider.SimPosition, true); + SetPosition(Collider.SimPosition, lerp: true); collisionsDisabled = false; //force collision categories to be updated prevCollisionCategory = Category.None; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 639a02146..d6cbaf017 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -130,6 +130,7 @@ namespace Barotrauma } public readonly HashSet Latchers = new HashSet(); + public readonly HashSet AttachedProjectiles = new HashSet(); protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; @@ -265,7 +266,7 @@ namespace Barotrauma private CharacterPrefab prefab; public readonly CharacterParams Params; - public string SpeciesName => Params.SpeciesName; + public string SpeciesName => Params?.SpeciesName ?? "null"; public string Group => Params.Group; public bool IsHumanoid => Params.Humanoid; public bool IsHusk => Params.Husk; @@ -2640,7 +2641,7 @@ namespace Barotrauma ApplyStatusEffects(ActionType.Always, deltaTime); PreviousHull = CurrentHull; - CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, true); + CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true); speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime); @@ -2710,7 +2711,7 @@ namespace Barotrauma if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { Implode(); - if (IsDead) { return; } + if (IsDead) { return; } } } } @@ -2719,7 +2720,9 @@ namespace Barotrauma PressureTimer = 0.0f; } } - else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && WorldPosition.Y < CharacterHealth.CrushDepth) + else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && + PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) && + WorldPosition.Y < CharacterHealth.CrushDepth) { //implode if below crush depth, and either outside or in a high-pressure hull if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) @@ -3124,47 +3127,53 @@ namespace Barotrauma //set the character order only if the character is close enough to hear the message if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; } - if (order != null && order.OrderGiver != orderGiver) + if (order != null) { - order.OrderGiver = orderGiver; - } - - switch (order?.Category) - { - case OrderCategory.Operate when order?.TargetEntity != null: - // If there's another character operating the same device, make them dismiss themself - foreach (var character in CharacterList) + if (order.OrderGiver != orderGiver) + { + order.OrderGiver = orderGiver; + } + if (order.AutoDismiss) + { + switch (order.Category) { - if (character == this) { continue; } - if (character.TeamID != TeamID) { continue; } - if (!(character.AIController is HumanAIController)) { continue; } - if (!HumanAIController.IsActive(character)) { continue; } - foreach (var currentOrder in character.CurrentOrders) - { - if (currentOrder.Order == null) { continue; } - if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } - if (currentOrder.Order.Identifier != order.Identifier) { continue; } - if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } - character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force); + case OrderCategory.Operate when order.TargetEntity != null: + // If there's another character operating the same device, make them dismiss themself + foreach (var character in CharacterList) + { + if (character == this) { continue; } + if (character.TeamID != TeamID) { continue; } + if (!(character.AIController is HumanAIController)) { continue; } + if (!HumanAIController.IsActive(character)) { continue; } + foreach (var currentOrder in character.CurrentOrders) + { + if (currentOrder.Order == null) { continue; } + if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } + if (currentOrder.Order.Identifier != order.Identifier) { continue; } + if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } + if (!currentOrder.Order.AutoDismiss) { continue; } + character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force); + break; + } + } + break; + case OrderCategory.Movement: + // If there character has another movement order, dismiss that order + OrderInfo? orderToReplace = null; + foreach (var currentOrder in CurrentOrders) + { + if (currentOrder.Order == null) { continue; } + if (currentOrder.Order.Category != OrderCategory.Movement) { continue; } + orderToReplace = currentOrder; + break; + } + if (orderToReplace.HasValue && orderToReplace.Value.Order.AutoDismiss) + { + SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force); + } break; - } } - break; - case OrderCategory.Movement: - // If there character has another movement order, dismiss that order - OrderInfo? orderToReplace = null; - foreach (var currentOrder in CurrentOrders) - { - if (currentOrder.Order == null) { continue; } - if (currentOrder.Order.Category != OrderCategory.Movement) { continue; } - orderToReplace = currentOrder; - break; - } - if (orderToReplace.HasValue) - { - SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force); - } - break; + } } // Prevent adding duplicate orders @@ -3624,7 +3633,7 @@ namespace Barotrauma { if (attacker.TeamID == TeamID) { - afflictions = afflictions.Where(a => !a.Prefab.IsBuff); + afflictions = afflictions.Where(a => a.Prefab.IsBuff); if (!afflictions.Any()) { return new AttackResult(); } } } @@ -3936,6 +3945,7 @@ namespace Barotrauma } SelectedConstruction = null; + SelectedCharacter = null; AnimController.ResetPullJoints(); @@ -4063,10 +4073,11 @@ namespace Barotrauma public void TeleportTo(Vector2 worldPos) { + CurrentHull = null; AnimController.CurrentHull = null; Submarine = null; - AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), false); - AnimController.FindHull(worldPos, true); + AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false); + AnimController.FindHull(worldPos, setSubmarine: true); } public static void SaveInventory(Inventory inventory, XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index c193a3918..00212a48b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1176,7 +1176,7 @@ namespace Barotrauma int salary = 0; foreach (Skill skill in Job.Skills) { - salary += (int)(skill.Level * skill.Prefab.PriceMultiplier); + salary += (int)(skill.Level * skill.PriceMultiplier); } return (int)(salary * Job.Prefab.PriceMultiplier); @@ -1274,19 +1274,18 @@ namespace Barotrauma return Math.Max(GetTotalTalentPoints() - GetUnlockedTalentsInTree().Count(), 0); } - public float GetProgressTowardsNextLevel() + public int GetProgressTowardsNextLevel() { - float progress = (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel()); - return progress; + return (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel()); } - public float GetExperienceRequiredForCurrentLevel() + public int GetExperienceRequiredForCurrentLevel() { GetCurrentLevel(out int experienceRequired); return experienceRequired; } - public float GetExperienceRequiredToLevelUp() + public int GetExperienceRequiredToLevelUp() { int level = GetCurrentLevel(out int experienceRequired); return experienceRequired + ExperienceRequiredPerLevel(level); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 5203f1728..165ed8bdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -174,7 +174,8 @@ namespace Barotrauma private void CharacterDead(Character character, CauseOfDeath causeOfDeath) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (Strength < TransformThresholdOnDeath || character.Removed) + if (Strength < TransformThresholdOnDeath || character.Removed || + character.CharacterHealth.GetAllAfflictions().Any(a => a.GetActiveEffect()?.BlockTransformation.Contains(Prefab.Identifier) ?? false)) { UnsubscribeFromDeathEvent(); return; @@ -193,7 +194,7 @@ namespace Barotrauma CoroutineManager.StartCoroutine(CreateAIHusk()); } - private IEnumerable CreateAIHusk() + private IEnumerable CreateAIHusk() { //character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base) // -> don't spawn the AI husk diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index f527262ad..e20ac25cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -234,6 +234,11 @@ namespace Barotrauma [Serialize("0,0,0,0", false)] public Color MaxBodyTint { get; private set; } + /// + /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character + /// + public string[] BlockTransformation { get; private set; } + public readonly Dictionary AfflictionStatValues = new Dictionary(); public readonly HashSet AfflictionAbilityFlags = new HashSet(); @@ -245,6 +250,7 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, element); resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true); + BlockTransformation = element.GetAttributeStringArray("blocktransformation", new string[0], convertToLowerInvariant: true); foreach (XElement subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index e1dfcebc8..0c7fb8a78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -153,7 +153,10 @@ namespace Barotrauma (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType); if (positionToStayIn != null && humanAI != null) { - humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)); + humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200) + { + DebugLogWhenFails = false + }); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 2841d305e..04c7a3acd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -9,7 +9,7 @@ namespace Barotrauma { private readonly JobPrefab prefab; - private Dictionary skills; + private readonly Dictionary skills; public string Name { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 5bb3e5286..442f29ab9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -282,7 +282,7 @@ namespace Barotrauma Variants = variant; - Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X)); + Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start)); // Disabled on purpose, TODO: remove all references? //ClothingElement = element.GetChildElement("PortraitClothing"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index f3a502403..e4fab177c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -34,14 +34,14 @@ namespace Barotrauma } } - internal SkillPrefab Prefab { get; private set; } + public readonly float PriceMultiplier = 1.0f; public Skill(SkillPrefab prefab) { - this.Prefab = prefab; Identifier = prefab.Identifier; - level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server); + level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server); icon = GetIcon(); + PriceMultiplier = prefab.PriceMultiplier; } public Skill(string identifier, float level) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs index 8aa7bf6b4..d4bb305f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs @@ -7,7 +7,7 @@ namespace Barotrauma { public readonly string Identifier; - public Vector2 LevelRange { get; private set; } + public Range LevelRange { get; private set; } /// /// How much this skill affects characters' hiring cost @@ -23,12 +23,13 @@ namespace Barotrauma var levelString = element.GetAttributeString("level", ""); if (levelString.Contains(",")) { - LevelRange = XMLExtensions.ParseVector2(levelString, false); + var rangeVector2 = XMLExtensions.ParseVector2(levelString, false); + LevelRange = new Range(rangeVector2.X, rangeVector2.Y); } else { float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture); - LevelRange = new Vector2(skillLevel, skillLevel); + LevelRange = new Range(skillLevel, skillLevel); } IsPrimarySkill = element.GetAttributeBool("primary", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 9731b08d5..8f5eb2299 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -430,8 +430,15 @@ namespace Barotrauma public float Dir { - get { return ((dir == Direction.Left) ? -1.0f : 1.0f); } - set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; } + get { return (dir == Direction.Left) ? -1.0f : 1.0f; } + set + { + dir = (value == -1.0f) ? Direction.Left : Direction.Right; + if (body != null) + { + body.Dir = Dir; + } + } } public int RefJointIndex => Params.RefJoint; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index ee5fedac0..1112c063a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -16,10 +16,12 @@ namespace Barotrauma.Abilities private readonly string itemIdentifier; private readonly string[] tags; private readonly WeaponType weapontype; + private bool ignoreNonHarmfulAttacks; public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { itemIdentifier = conditionElement.GetAttributeString("itemidentifier", ""); tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false); switch (conditionElement.GetAttributeString("weapontype", "")) { case "melee": @@ -35,8 +37,15 @@ namespace Barotrauma.Abilities { if (abilityObject is AbilityAttackData attackData) { - Item item = attackData?.SourceAttack?.SourceItem; + if (ignoreNonHarmfulAttacks && attackData.SourceAttack != null) + { + if (attackData.SourceAttack.Stun <= 0.0f && (attackData.SourceAttack.Afflictions?.All(a => a.Key.Prefab.IsBuff) ?? true)) + { + return false; + } + } + Item item = attackData?.SourceAttack?.SourceItem; if (item == null) { DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!"); @@ -61,10 +70,13 @@ namespace Barotrauma.Abilities switch (weapontype) { + // it is possible that an item that has both a melee and a projectile component will return true + // even when not used as a melee/ranged weapon respectively + // attackdata should contain data regarding whether the attack is melee or not case WeaponType.Melee: return item.GetComponent() != null; case WeaponType.Ranged: - return item.GetComponent() != null; + return item.GetComponent() != null; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs index fe3608df4..0d25f107e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -18,13 +18,13 @@ namespace Barotrauma.Abilities protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData) { - DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject}"); + DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}"); } protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject); public override bool MatchesCondition() { - DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed."); + DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed."); return false; } public override bool MatchesCondition(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 650bf1863..0abc6dbc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -79,17 +79,17 @@ namespace Barotrauma.Abilities protected virtual void ApplyEffect() { - DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect"); + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); } protected virtual void ApplyEffect(AbilityObject abilityObject) { - DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect"); + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); } protected void LogabilityObjectMismatch() { - DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type."); + DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}"); } // XML diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index a59955d4d..c30ac8152 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -10,8 +10,10 @@ namespace Barotrauma.Abilities protected readonly List statusEffects; + private readonly bool applyToSelf; private readonly bool nearbyCharactersAppliesToSelf; private readonly bool nearbyCharactersAppliesToAllies; + private readonly bool nearbyCharactersAppliesToEnemies; private readonly bool applyToSelected; readonly List targets = new List(); @@ -19,9 +21,11 @@ namespace Barotrauma.Abilities public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); + applyToSelf = abilityElement.GetAttributeBool("applytoself", false); applyToSelected = abilityElement.GetAttributeBool("applytoselected", false); nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true); nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true); + nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true); } protected void ApplyEffectSpecific(Character targetCharacter) @@ -46,6 +50,10 @@ namespace Barotrauma.Abilities { targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); } + if (!nearbyCharactersAppliesToEnemies) + { + targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); + } statusEffect.SetUser(Character); statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); } @@ -75,7 +83,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter) + if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf) { ApplyEffectSpecific(targetCharacter); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 9cbaa17eb..a2fc33e5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -31,5 +31,10 @@ namespace Barotrauma.Abilities } } + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index 30b6eb7c4..8b4245dc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class CharacterAbilityModifyAttackData : CharacterAbility { - private readonly List afflictions; + private readonly List afflictions = new List(); private readonly float addedDamageMultiplier; private readonly float addedPenetration; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs index b9f26160c..8a88acea6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Abilities @@ -15,14 +16,19 @@ namespace Barotrauma.Abilities if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab))); + if (subTree != null) { + subTree.ForceUnlock = true; foreach (var talentOption in subTree.TalentOptionStages) { foreach (var talent in talentOption.Talents) { if (talent == CharacterTalent.Prefab) { continue; } - Character.GiveTalent(talent); + if (Character.GiveTalent(talent)) + { + Character.Info.AdditionalTalentPoints++; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 277af65b0..193bf9c8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -101,37 +101,40 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(file.Path); if (doc == null) { return; } - var rootElement = doc.Root; - switch (rootElement.Name.ToString().ToLowerInvariant()) + void loadSinglePrefab(XElement element, bool isOverride) { - case "talent": - TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), false); - break; - case "talents": - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var itemElement = element.GetChildElement("talent"); - if (itemElement != null) - { - TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), true); - } - else - { - DebugConsole.ThrowError($"Cannot find a talent element from the children of the override element defined in {file.Path}"); - } - } - else - { - TalentPrefabs.Add(new TalentPrefab(element, file.Path), false); - } - } - break; - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); - break; + TalentPrefabs.Add(new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride); } + + void loadMultiplePrefabs(XElement element, bool isOverride) + { + foreach (var subElement in element.Elements()) + { + interpretElement(subElement, isOverride); + } + } + + void interpretElement(XElement subElement, bool isOverride) + { + if (subElement.IsOverride()) + { + loadMultiplePrefabs(subElement, true); + } + else if (subElement.Name.LocalName.Equals("talents", StringComparison.OrdinalIgnoreCase)) + { + loadMultiplePrefabs(subElement, isOverride); + } + else if (subElement.Name.LocalName.Equals("talent", StringComparison.OrdinalIgnoreCase)) + { + loadSinglePrefab(subElement, isOverride); + } + else + { + DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentPrefab)} prefab type: '{subElement.Name}' in {file.Path}"); + } + } + + interpretElement(doc.Root, false); } public static void LoadAll(IEnumerable files) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 02377cba0..20f7865ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -5,7 +5,7 @@ using System.Xml.Linq; namespace Barotrauma { - class TalentTree + class TalentTree : IPrefab, IDisposable { public enum TalentTreeStageState { @@ -16,7 +16,7 @@ namespace Barotrauma Highlighted } - public static readonly Dictionary JobTalentTrees = new Dictionary(); + public static readonly PrefabCollection JobTalentTrees = new PrefabCollection(); public readonly List TalentSubTrees = new List(); @@ -26,13 +26,21 @@ namespace Barotrauma private set; } + public string OriginalName => Identifier; + + public string Identifier { get; } + + public string FilePath { get; } + + public ContentPackage ContentPackage { get; set; } + public TalentTree(XElement element, string filePath) { ConfigElement = element; + FilePath = filePath; + Identifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant(); - string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant(); - - if (string.IsNullOrEmpty(jobIdentifier)) + if (string.IsNullOrEmpty(Identifier)) { DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!"); return; @@ -50,20 +58,15 @@ namespace Barotrauma TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { - DebugConsole.AddWarning($"Talent tree for job {jobIdentifier} contains non-existent talent {talent}! Talent tree not added."); + DebugConsole.AddWarning($"Talent tree for job {Identifier} contains non-existent talent {talent}! Talent tree not added."); return; } if (!duplicateSet.Add(talent)) { - DebugConsole.ThrowError($"Talent tree for job {jobIdentifier} contains duplicate talent {talent}! Talent tree not added."); + DebugConsole.ThrowError($"Talent tree for job {Identifier} contains duplicate talent {talent}! Talent tree not added."); return; } } - - if (!JobTalentTrees.TryAdd(jobIdentifier, this)) - { - DebugConsole.ThrowError($"Could not add talent tree for job {jobIdentifier}! A talent tree for this job is already likely defined"); - } } public bool TalentIsInTree(string talentIdentifier) @@ -78,37 +81,40 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(file.Path); if (doc == null) { return; } - var rootElement = doc.Root; - switch (rootElement.Name.ToString().ToLowerInvariant()) + void loadSinglePrefab(XElement element, bool isOverride) { - case "talenttree": - new TalentTree(rootElement, file.Path); - break; - case "talenttrees": - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var treeElement = element.GetChildElement("talenttree"); - if (treeElement != null) - { - new TalentTree(rootElement, file.Path); - } - else - { - DebugConsole.ThrowError($"Cannot find a talent tree element from the children of the override element defined in {file.Path}"); - } - } - else - { - new TalentTree(element, file.Path); - } - } - break; - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}"); - break; + JobTalentTrees.Add(new TalentTree(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride); } + + void loadMultiplePrefabs(XElement element, bool isOverride) + { + foreach (var subElement in element.Elements()) + { + interpretElement(subElement, isOverride); + } + } + + void interpretElement(XElement subElement, bool isOverride) + { + if (subElement.IsOverride()) + { + loadMultiplePrefabs(subElement, true); + } + else if (subElement.Name.LocalName.Equals("talenttrees", StringComparison.OrdinalIgnoreCase)) + { + loadMultiplePrefabs(subElement, isOverride); + } + else if (subElement.Name.LocalName.Equals("talenttree", StringComparison.OrdinalIgnoreCase)) + { + loadSinglePrefab(subElement, isOverride); + } + else + { + DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentTree)} prefab type: '{subElement.Name}' in {file.Path}"); + } + } + + interpretElement(doc.Root, false); } public static void LoadAll(IEnumerable files) @@ -190,6 +196,8 @@ namespace Barotrauma foreach (var subTree in talentTree.TalentSubTrees) { + if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.Talents.Any(t => t.Identifier == talentIdentifier))) { return true; } + foreach (var talentOptionStage in subTree.TalentOptionStages) { bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier)); @@ -220,7 +228,7 @@ namespace Barotrauma canStillUnlock = false; foreach (string talent in selectedTalents) { - if (IsViableTalentForCharacter(controlledCharacter, talent, viableTalents)) + if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents)) { viableTalents.Add(talent); canStillUnlock = true; @@ -229,6 +237,14 @@ namespace Barotrauma } return viableTalents; } + + private bool disposed = false; + public void Dispose() + { + if (disposed) { return; } + disposed = true; + JobTalentTrees.Remove(this); + } } class TalentSubTree @@ -237,6 +253,8 @@ namespace Barotrauma public string DisplayName { get; } + public bool ForceUnlock; + public readonly List TalentOptionStages = new List(); public TalentSubTree(XElement subTreeElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index 2ee6801ac..bdde120e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -5,14 +5,64 @@ using System.Threading; namespace Barotrauma { - enum CoroutineStatus + abstract class CoroutineStatus { - Running, Success, Failure + public static CoroutineStatus Running => EnumCoroutineStatus.Running; + public static CoroutineStatus Success => EnumCoroutineStatus.Success; + public static CoroutineStatus Failure => EnumCoroutineStatus.Failure; + + public abstract bool CheckFinished(float deltaTime); + public abstract bool EndsCoroutine(CoroutineHandle handle); + } + + class EnumCoroutineStatus : CoroutineStatus + { + private enum StatusValue + { + Running, Success, Failure + } + + private readonly StatusValue Value; + + private EnumCoroutineStatus(StatusValue value) { Value = value; } + + public new readonly static EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running); + public new readonly static EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success); + public new readonly static EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure); + + public override bool CheckFinished(float deltaTime) + { + return true; + } + + public override bool EndsCoroutine(CoroutineHandle handle) + { + if (Value == StatusValue.Failure) + { + DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); + } + return Value != StatusValue.Running; + } + + public override bool Equals(object obj) + { + return obj is EnumCoroutineStatus other && Value == other.Value; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public override string ToString() + { + return Value.ToString(); + } } class CoroutineHandle { - public readonly IEnumerator Coroutine; + public readonly IEnumerator Coroutine; public readonly string Name; public Exception Exception; @@ -20,7 +70,7 @@ namespace Barotrauma public Thread Thread; - public CoroutineHandle(IEnumerator coroutine, string name = "", bool useSeparateThread = false) + public CoroutineHandle(IEnumerator coroutine, string name = "", bool useSeparateThread = false) { Coroutine = coroutine; Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name; @@ -36,7 +86,7 @@ namespace Barotrauma public static float UnscaledDeltaTime, DeltaTime; - public static CoroutineHandle StartCoroutine(IEnumerable func, string name = "", bool useSeparateThread = false) + public static CoroutineHandle StartCoroutine(IEnumerable func, string name = "", bool useSeparateThread = false) { var handle = new CoroutineHandle(func.GetEnumerator(), name); lock (Coroutines) @@ -63,7 +113,7 @@ namespace Barotrauma return StartCoroutine(DoInvokeAfter(action, delay)); } - private static IEnumerable DoInvokeAfter(Action action, float delay) + private static IEnumerable DoInvokeAfter(Action action, float delay) { if (action == null) { @@ -127,9 +177,7 @@ namespace Barotrauma bool joined = false; while (!joined) { -#if CLIENT CrossThread.ProcessTasks(); -#endif joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500)); } } @@ -137,35 +185,26 @@ namespace Barotrauma } } + private static bool PerformCoroutineStep(CoroutineHandle handle) + { + var current = handle.Coroutine.Current; + if (current != null) + { + if (current.EndsCoroutine(handle) || handle.AbortRequested) { return true; } + if (!current.CheckFinished(UnscaledDeltaTime)) { return false; } + } + if (!handle.Coroutine.MoveNext()) { return true; } + return false; + } + public static void ExecuteCoroutineThread(CoroutineHandle handle) { try { while (!handle.AbortRequested) { - if (handle.Coroutine.Current != null) - { - WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds; - if (wfs != null) - { - Thread.Sleep((int)(wfs.TotalTime * 1000)); - } - else - { - switch ((CoroutineStatus)handle.Coroutine.Current) - { - case CoroutineStatus.Success: - return; - - case CoroutineStatus.Failure: - DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); - return; - } - } - } - - Thread.Yield(); - if (!handle.Coroutine.MoveNext()) return; + if (PerformCoroutineStep(handle)) { return; } + Thread.Sleep((int)(UnscaledDeltaTime * 1000)); } } catch (ThreadAbortException) @@ -187,36 +226,13 @@ namespace Barotrauma #endif if (handle.Thread == null) { - if (handle.AbortRequested) { return true; } - if (handle.Coroutine.Current != null) - { - WaitForSeconds wfs = handle.Coroutine.Current as WaitForSeconds; - if (wfs != null) - { - if (!wfs.CheckFinished(UnscaledDeltaTime)) return false; - } - else - { - switch ((CoroutineStatus)handle.Coroutine.Current) - { - case CoroutineStatus.Success: - return true; - - case CoroutineStatus.Failure: - DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); - return true; - } - } - } - - handle.Coroutine.MoveNext(); - return false; + return PerformCoroutineStep(handle); } else { if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped)) { - if (handle.Exception!=null || (CoroutineStatus)handle.Coroutine.Current == CoroutineStatus.Failure) + if (handle.Exception!=null || handle.Coroutine.Current == CoroutineStatus.Failure) { DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed"); } @@ -262,7 +278,7 @@ namespace Barotrauma } } - class WaitForSeconds + class WaitForSeconds : CoroutineStatus { public readonly float TotalTime; @@ -276,7 +292,7 @@ namespace Barotrauma this.ignorePause = ignorePause; } - public bool CheckFinished(float deltaTime) + public override bool CheckFinished(float deltaTime) { #if !SERVER if (ignorePause || !GUI.PauseMenuOpen) @@ -288,5 +304,10 @@ namespace Barotrauma #endif return timer <= 0.0f; } + + public override bool EndsCoroutine(CoroutineHandle handle) + { + return false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 859d6f2fd..56ab5b7f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -109,7 +109,7 @@ namespace Barotrauma public static bool CheatsEnabled; private static readonly List unsavedMessages = new List(); - private static readonly int messagesPerFile = 5000; + private static readonly int messagesPerFile = 800; public const string SavePath = "ConsoleLogs"; private static void AssignOnExecute(string names, Action onExecute) @@ -878,7 +878,7 @@ namespace Barotrauma List talentTrees = new List(); if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase)) { - talentTrees.AddRange(TalentTree.JobTalentTrees.Values); + talentTrees.AddRange(TalentTree.JobTalentTrees); } else { @@ -1062,7 +1062,7 @@ namespace Barotrauma }, null)); - IEnumerable TestLevels() + IEnumerable TestLevels() { SubmarineInfo selectedSub = null; string subName = GameMain.Config.QuickStartSubmarineName; @@ -1410,6 +1410,29 @@ namespace Barotrauma } }, null, isCheat: true)); + commands.Add(new Command("despawnnow", "despawnnow [character]: Immediately despawns the specified dead character. If the character argument is omitted, all dead characters are despawned.", (string[] args) => + { + if (args.Length == 0) + { + foreach (Character c in Character.CharacterList.Where(c => c.IsDead).ToList()) + { + c.DespawnNow(); + } + } + else + { + Character character = FindMatchingCharacter(args); + character?.DespawnNow(); + } + }, + () => + { + return new string[][] + { + Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().ToArray() + }; + }, isCheat: true)); + commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null, () => { @@ -1832,7 +1855,7 @@ namespace Barotrauma #if CLIENT activeQuestionText = null; #endif - NewMessage(command, Color.White, true); + NewCommand(command); //reset the variable before invoking the delegate because the method may need to activate another question var temp = activeQuestionCallback; activeQuestionCallback = null; @@ -1857,7 +1880,7 @@ namespace Barotrauma if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase)) { - NewMessage(command, Color.White, true); + NewCommand(command); } #if CLIENT @@ -2173,15 +2196,37 @@ namespace Barotrauma } } - public static void NewMessage(string msg, bool isCommand = false) + public static void ShowError(string msg, Color? color = null) { + color ??= Color.Red; + NewMessage(msg, color.Value, isCommand: false, isError: true); + } + + public static void NewCommand(string command, Color? color = null) + { + color ??= Color.White; + NewMessage(command, color.Value, isCommand: true, isError: false); + } + + public static void NewMessage(string msg, Color? color = null, bool debugOnly = false) + { + color ??= Color.White; + if (debugOnly) + { +#if DEBUG + NewMessage(msg, color.Value, isCommand: false, isError: false); +#endif + } + else + { + NewMessage(msg, color.Value, isCommand: false, isError: false); + } #if DEBUG Console.WriteLine(msg); #endif - NewMessage(msg, Color.White, isCommand); } - public static void NewMessage(string msg, Color color, bool isCommand = false, bool isError = false) + private static void NewMessage(string msg, Color color, bool isCommand, bool isError) { if (string.IsNullOrEmpty(msg)) { return; } @@ -2271,7 +2316,10 @@ namespace Barotrauma public static void Log(string message) { - if (GameSettings.VerboseLogging) NewMessage(message, Color.Gray); + if (GameSettings.VerboseLogging) + { + NewMessage(message, Color.Gray); + } } public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) @@ -2309,7 +2357,7 @@ namespace Barotrauma } #endif - NewMessage(error, Color.Red, isError: true); + ShowError(error); } public static void AddWarning(string warning) @@ -2319,7 +2367,7 @@ namespace Barotrauma } #if CLIENT - private static IEnumerable CreateMessageBox(string errorMsg) + private static IEnumerable CreateMessageBox(string errorMsg) { while (GUI.Style == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index d42fb1cf6..8293cc1e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -121,7 +121,7 @@ namespace Barotrauma { foreach (Item item in newCharacter.Inventory.AllItems) { - item.SpawnedInOutpost = true; + item.SpawnedInCurrentOutpost = true; item.AllowStealing = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 0e0e9a77c..434b1c217 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -33,6 +33,8 @@ namespace Barotrauma private float currentIntensity; //The exact intensity of the current situation, current intensity is lerped towards this value private float targetIntensity; + //follows targetIntensity a bit faster than currentIntensity to prevent e.g. combat musing staying on very long after the monsters are dead + private float musicIntensity; //How low the intensity has to be for an event to be triggered. //Gradually increases with time, so additional problems can still appear eventually even if @@ -50,7 +52,11 @@ namespace Barotrauma private float calculateDistanceTraveledTimer; private float distanceTraveled; - private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterTotalStrength; + private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterStrength; + public float CumulativeMonsterStrengthMain; + public float CumulativeMonsterStrengthRuins; + public float CumulativeMonsterStrengthWrecks; + public float CumulativeMonsterStrengthCaves; private float roundDuration; @@ -78,6 +84,10 @@ namespace Barotrauma { get { return currentIntensity; } } + public float MusicIntensity + { + get { return musicIntensity; } + } public List ActiveEvents { @@ -85,7 +95,22 @@ namespace Barotrauma } public readonly Queue QueuedEvents = new Queue(); - + + private struct TimeStamp + { + public readonly double Time; + public readonly Event Event; + + public TimeStamp(Event e) + { + Event = e; + Time = Timing.TotalTime; + } + } + + private readonly List timeStamps = new List(); + public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e)); + public EventManager() { isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -99,6 +124,7 @@ namespace Barotrauma if (isClient) { return; } + timeStamps.Clear(); pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); @@ -202,8 +228,12 @@ namespace Barotrauma crewAwayResetTimer = 0.0f; intensityUpdateTimer = 0.0f; CalculateCurrentIntensity(0.0f); - currentIntensity = targetIntensity; + currentIntensity = musicIntensity = targetIntensity; eventCoolDown = 0.0f; + CumulativeMonsterStrengthMain = 0; + CumulativeMonsterStrengthRuins = 0; + CumulativeMonsterStrengthWrecks = 0; + CumulativeMonsterStrengthCaves = 0; } private void SelectSettings() @@ -401,11 +431,7 @@ namespace Barotrauma { if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } -#if DEBUG - DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue); -#else - DebugConsole.Log($"Loading event set {eventSet.DebugIdentifier}"); -#endif + DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue, debugOnly: true); int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) @@ -413,7 +439,7 @@ namespace Barotrauma applyCount = level.Ruins.Count(); foreach (var ruin in level.Ruins) { - spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; }); + spawnPosFilter.Add(pos => pos.Ruin == ruin); } } else if (eventSet.PerCave) @@ -421,7 +447,7 @@ namespace Barotrauma applyCount = level.Caves.Count(); foreach (var cave in level.Caves) { - spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; }); + spawnPosFilter.Add(pos => pos.Cave == cave); } } else if (eventSet.PerWreck) @@ -430,7 +456,7 @@ namespace Barotrauma applyCount = wrecks.Count(); foreach (var wreck in wrecks) { - spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; }); + spawnPosFilter.Add(pos => pos.Submarine == wreck); } } @@ -463,11 +489,7 @@ namespace Barotrauma if (newEvent == null) { continue; } newEvent.Init(true); if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } -#if DEBUG - DebugConsole.NewMessage($"Initialized event {newEvent}"); -#else - DebugConsole.Log($"Initialized event {newEvent}"); -#endif + DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); @@ -498,11 +520,7 @@ namespace Barotrauma var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); -#if DEBUG - DebugConsole.NewMessage($"Initialized event {newEvent}"); -#else - DebugConsole.Log($"Initialized event {newEvent}"); -#endif + DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); @@ -525,6 +543,7 @@ namespace Barotrauma var allowedEventSets = eventSets.Where(es => + es.IsCampaignSet == GameMain.GameSession?.GameMode is CampaignMode && level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType && (string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase))); @@ -647,6 +666,7 @@ namespace Barotrauma isCrewAway = false; crewAwayDuration = 0.0f; eventThreshold += settings.EventThresholdIncrease * deltaTime; + eventThreshold = Math.Min(eventThreshold, 1.0f); eventCoolDown -= deltaTime; } @@ -739,7 +759,7 @@ namespace Barotrauma // enemy amount -------------------------------------------------------- enemyDanger = 0.0f; - monsterTotalStrength = 0; + monsterStrength = 0; foreach (Character character in Character.CharacterList) { if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } @@ -749,28 +769,9 @@ namespace Barotrauma if (!enemyAI.AIParams.StayInAbyss) { // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. - monsterTotalStrength += enemyAI.CombatStrength; + monsterStrength += enemyAI.CombatStrength; } - // Example combat strengths: - // Hammerheadspawn 1 - // Moloch Pupa 1 - // Terminal cell 20 - // Leucocyte 40 - // Husk 90 - // Crawler 100 - // Unarmored Mudraptor 140 - // Spineling 150 - // Tigerthresher 200 - // Armored Mudraptor 210 - // Watcher 400 - // Golden Hammerhead 400 - // Hammerhead 500 - // Hammerhead Matriarch 550 - // Bonethresher 600 - // Moloch 1250 - // Black Moloch 1500 - // Endworm 10000 if (character.CurrentHull?.Submarine != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) { @@ -792,7 +793,7 @@ namespace Barotrauma // 5 Mudraptors -> +0.21 (0.42 in total, before they get inside). // 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub). // 2 Molochs -> +0.5 (1.0 in total, if both target the sub). - enemyDanger += monsterTotalStrength / 5000f; + enemyDanger += monsterStrength / 5000f; enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f); // The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle. @@ -868,11 +869,15 @@ namespace Barotrauma { //25 seconds for intensity to go from 0.0 to 1.0 currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity); + //20 seconds for intensity to go from 0.0 to 1.0 + musicIntensity = Math.Min(musicIntensity + 0.05f * IntensityUpdateInterval, targetIntensity); } else { //400 seconds for intensity to go from 1.0 to 0.0 currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity); + //20 seconds for intensity to go from 1.0 to 0.0 + musicIntensity = Math.Max(musicIntensity - 0.05f * IntensityUpdateInterval, targetIntensity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 3938f6db0..856f99d79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -13,6 +13,7 @@ namespace Barotrauma public float Commonness; public string Identifier; public string BiomeIdentifier; + public float SpawnDistance; public bool UnlockPathEvent; public string UnlockPathTooltip; @@ -46,25 +47,30 @@ namespace Barotrauma UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); UnlockPathFaction = element.GetAttributeString("unlockpathfaction", ""); + + SpawnDistance = element.GetAttributeFloat("spawndistance", 0); + } + + public bool TryCreateInstance(out T instance) where T : Event + { + instance = CreateInstance() as T; + return instance is T; } public Event CreateInstance() { ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(EventPrefab) }); - object instance = null; + Event instance = null; try { - instance = constructor.Invoke(new object[] { this }); + instance = constructor.Invoke(new object[] { this }) as Event; } catch (Exception ex) { DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString()); } - - Event ev = (Event)instance; - if (!ev.LevelMeetsRequirements()) { return null; } - - return (Event)instance; + if (instance != null && !instance.LevelMeetsRequirements()) { return null; } + return instance; } public override string ToString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index ca7667dca..0f2fb31d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -14,6 +14,7 @@ namespace Barotrauma { public readonly EventSet RootSet; public readonly Dictionary MonsterCounts = new Dictionary(); + public float MonsterStrength; public EventDebugStats(EventSet rootSet) { @@ -63,6 +64,8 @@ namespace Barotrauma return GetAllEventPrefabs().Find(prefab => string.Equals(prefab.Identifier, identifer, StringComparison.Ordinal)); } + public readonly bool IsCampaignSet; + //0-100 public readonly float MinLevelDifficulty, MaxLevelDifficulty; @@ -193,6 +196,7 @@ namespace Barotrauma DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); + IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost); Commonness[""] = element.GetAttributeFloat("commonness", 1.0f); foreach (XElement subElement in element.Elements()) @@ -205,7 +209,7 @@ namespace Barotrauma { if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { - string levelType = overrideElement.GetAttributeString("leveltype", ""); + string levelType = overrideElement.GetAttributeString("leveltype", "").ToLowerInvariant(); if (!Commonness.ContainsKey(levelType)) { Commonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f)); @@ -227,8 +231,8 @@ namespace Barotrauma EventPrefabs.Add(new SubEventPrefab( debugIdentifier, identifiers, - commonness>=0f ? commonness : (float?)null, - probability>=0f ? probability : (float?)null)); + commonness >= 0f ? commonness : (float?)null, + probability >= 0f ? probability : (float?)null)); } else { @@ -347,7 +351,7 @@ namespace Barotrauma } } - public static List GetDebugStatistics(int simulatedRoundCount = 100) + public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null) { List debugLines = new List(); @@ -357,82 +361,75 @@ namespace Barotrauma for (int i = 0; i < simulatedRoundCount; i++) { var newStats = new EventDebugStats(eventSet); - CheckEventSet(newStats, eventSet); + CheckEventSet(newStats, eventSet, filter); stats.Add(newStats); } debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): "); LogEventStats(stats, debugLines); } - for (int difficulty = 0; difficulty <= 100; difficulty += 10) - { - debugLines.Add($"Event stats on difficulty level {difficulty}: "); - List stats = new List(); - for (int i = 0; i < simulatedRoundCount; i++) - { - EventSet selectedSet = List.Where(s => difficulty >= s.MinLevelDifficulty && difficulty <= s.MaxLevelDifficulty).GetRandom(); - if (selectedSet == null) { continue; } - var newStats = new EventDebugStats(selectedSet); - CheckEventSet(newStats, selectedSet); - stats.Add(newStats); - } - LogEventStats(stats, debugLines); - } - return debugLines; - static void CheckEventSet(EventDebugStats stats, EventSet thisSet) + static void CheckEventSet(EventDebugStats stats, EventSet thisSet, Func filter = null) { if (thisSet.ChooseRandom) { var unusedEvents = thisSet.EventPrefabs.ToList(); - for (int i = 0; i < thisSet.EventCount; i++) + if (unusedEvents.Any()) { - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced); - if (eventPrefab.Prefabs.Any(p => p != null)) + for (int i = 0; i < thisSet.EventCount; i++) { - AddEvents(stats, eventPrefab.Prefabs); - unusedEvents.Remove(eventPrefab); + var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList()); + if (eventPrefab.Prefabs.Any(p => p != null)) + { + AddEvents(stats, eventPrefab.Prefabs, filter); + unusedEvents.Remove(eventPrefab); + } } } + List values = thisSet.ChildSets.SelectMany(s => s.Commonness.Values).ToList(); + EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values); + if (childSet != null) + { + CheckEventSet(stats, childSet, filter); + } } else { foreach (var eventPrefab in thisSet.EventPrefabs) { - AddEvents(stats, eventPrefab.Prefabs); + AddEvents(stats, eventPrefab.Prefabs, filter); + } + foreach (var childSet in thisSet.ChildSets) + { + CheckEventSet(stats, childSet, filter); } - } - foreach (var childSet in thisSet.ChildSets) - { - CheckEventSet(stats, childSet); } } - static void AddEvents(EventDebugStats stats, IEnumerable eventPrefabs) - => eventPrefabs.ForEach(p => AddEvent(stats, p)); + static void AddEvents(EventDebugStats stats, IEnumerable eventPrefabs, Func filter = null) + => eventPrefabs.ForEach(p => AddEvent(stats, p, filter)); - static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab) + static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab, Func filter = null) { - if (eventPrefab.EventType == typeof(MonsterEvent)) + if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) { - float spawnProbability = eventPrefab.ConfigElement.GetAttributeFloat("spawnprobability", 1.0f); - if (Rand.Value(Rand.RandSync.Server) > spawnProbability) - { - return; - } + if (filter != null && !filter(monsterEvent)) { return; } - string character = eventPrefab.ConfigElement.GetAttributeString("characterfile", ""); - System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(character)); - int amount = eventPrefab.ConfigElement.GetAttributeInt("amount", 0); - int minAmount = eventPrefab.ConfigElement.GetAttributeInt("minamount", amount); - int maxAmount = eventPrefab.ConfigElement.GetAttributeInt("maxamount", amount); + float spawnProbability = monsterEvent.Prefab.Probability; + if (Rand.Value() > spawnProbability) { return; } - int count = Rand.Range(minAmount, maxAmount + 1); + string character = monsterEvent.speciesName; + int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } - if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; } stats.MonsterCounts[character] += count; + + var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai"); + if (aiElement != null) + { + stats.MonsterStrength += aiElement.GetAttributeFloat("combatstrength", 0) * count; + } } } @@ -445,16 +442,21 @@ namespace Barotrauma } else { - stats.Sort((s1, s2) => { return s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()); }); - - EventDebugStats minStats = stats.First(); - EventDebugStats maxStats = stats.First(); - debugLines.Add($" Minimum monster spawns: {stats.First().MonsterCounts.Values.Sum()}"); + stats.Sort((s1, s2) => s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum())); + debugLines.Add($" Minimum monster count: {stats.First().MonsterCounts.Values.Sum()}"); debugLines.Add($" {LogMonsterCounts(stats.First())}"); - debugLines.Add($" Median monster spawns: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}"); + debugLines.Add($" Median monster count: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}"); debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}"); - debugLines.Add($" Maximum monster spawns: {stats.Last().MonsterCounts.Values.Sum()}"); + debugLines.Add($" Maximum monster count: {stats.Last().MonsterCounts.Values.Sum()}"); debugLines.Add($" {LogMonsterCounts(stats.Last())}"); + debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))}"); + debugLines.Add($" "); + + stats.Sort((s1, s2) => s1.MonsterStrength.CompareTo(s2.MonsterStrength)); + debugLines.Add($" Minimum monster strength: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}"); + debugLines.Add($" Median monster strength: {StringFormatter.FormatZeroDecimal(stats[stats.Count / 2].MonsterStrength)}"); + debugLines.Add($" Maximum monster strength: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)}"); + debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))}"); debugLines.Add($" "); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 7869cd1b0..65d851338 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -161,6 +162,24 @@ namespace Barotrauma #endif } } + + //if any of the target items is a reactor, prevent exploding it from damaging the player's sub + foreach (Item item in items) + { + if (item.GetComponent() is Reactor reactor && (reactor.statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false)) + { + foreach (var statusEffect in reactor.statusEffectLists[ActionType.OnBroken]) + { + foreach (Explosion explosion in statusEffect.Explosions) + { + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.TeamID == CharacterTeamType.Team1) { explosion.IgnoredSubmarines.Add(sub); } + } + } + } + } + } } private void InitCharacters(Submarine submarine) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index d51ccd3fd..ce5311135 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -207,7 +207,7 @@ namespace Barotrauma var item = new Item(itemPrefab, position.Value, cargoRoomSub) { - SpawnedInOutpost = true, + SpawnedInCurrentOutpost = true, AllowStealing = false }; item.FindHull(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index c1f7f26be..30a7c608c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -1,4 +1,3 @@ -using Barotrauma.Extensions; using System.Collections.Generic; namespace Barotrauma @@ -6,8 +5,6 @@ namespace Barotrauma partial class CombatMission : Mission { private Submarine[] subs; - // TODO: not used - private List[] crews; private readonly string[] descriptions; private static string[] teamNames = { "Team A", "Team B" }; @@ -103,15 +100,16 @@ namespace Barotrauma subs[0].NeutralizeBallast(); subs[0].TeamID = CharacterTeamType.Team1; - subs[0].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team1); + subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1); subs[1].NeutralizeBallast(); subs[1].TeamID = CharacterTeamType.Team2; - subs[1].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team2); + subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2); subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition)); subs[1].FlipX(); - +#if SERVER crews = new List[] { new List(), new List() }; +#endif } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 39a4e2d01..f71ef9992 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -203,6 +203,14 @@ namespace Barotrauma enemySub.TeamID = CharacterTeamType.None; //make the enemy sub withstand atleast the same depth as the player sub enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth); + if (Level.Loaded != null) + { + //...and the depth of the patrol positions + 1000 m + foreach (var patrolPos in patrolPositions) + { + enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000); + } + } enemySub.ImmuneToBallastFlora = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 4cfd77e2f..e591f532c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -9,8 +9,8 @@ namespace Barotrauma { class MonsterEvent : Event { - private readonly string speciesName; - private readonly int minAmount, maxAmount; + public readonly string speciesName; + public readonly int minAmount, maxAmount; private List monsters; private readonly float scatter; @@ -20,7 +20,7 @@ namespace Barotrauma private bool disallowed; - private readonly Level.PositionType spawnPosType; + public readonly Level.PositionType SpawnPosType; private readonly string spawnPointTag; private bool spawnPending; @@ -77,15 +77,15 @@ namespace Barotrauma var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || - !Enum.TryParse(spawnPosTypeStr, true, out spawnPosType)) + !Enum.TryParse(spawnPosTypeStr, true, out SpawnPosType)) { - spawnPosType = Level.PositionType.MainPath; + SpawnPosType = Level.PositionType.MainPath; } //backwards compatibility if (prefab.ConfigElement.GetAttributeBool("spawndeep", false)) { - spawnPosType = Level.PositionType.Abyss; + SpawnPosType = Level.PositionType.Abyss; } spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty); @@ -143,7 +143,7 @@ namespace Barotrauma private List GetAvailableSpawnPositions() { - var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType)); + var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => SpawnPosType.HasFlag(p.PositionType)); var removals = new List(); foreach (var position in availablePositions) { @@ -188,8 +188,8 @@ namespace Barotrauma spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); - bool isRuinOrWreck = spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Wreck); - if (affectSubImmediately && !isRuinOrWreck && !spawnPosType.HasFlag(Level.PositionType.Abyss)) + bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck); + if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss)) { if (availablePositions.None()) { @@ -288,11 +288,14 @@ namespace Barotrauma spawnPos = chosenPosition.Position.ToVector2(); if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) { - var spawnPoint = - WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag); + bool ignoreSubmarine = chosenPosition.Ruin != null; + var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag, ignoreSubmarine: ignoreSubmarine); if (spawnPoint != null) { - System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine)); + if (!ignoreSubmarine) + { + System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine); + } spawnPos = spawnPoint.WorldPosition; } else @@ -303,32 +306,42 @@ namespace Barotrauma return; } } - else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath) - && offset > 0) + else if (chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath) { - Vector2 dir; - var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null); - var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault(); - if (nearestWaypoint != null) + if (offset > 0) { - int currentIndex = waypoints.IndexOf(nearestWaypoint); - var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)]; - dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition); - // Ensure that the spawn position is not offset to the left. - if (dir.X < 0) + Vector2 dir; + var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.Ruin == null); + var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault(); + if (nearestWaypoint != null) { - dir.X = 0; + int currentIndex = waypoints.IndexOf(nearestWaypoint); + var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)]; + dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition); + // Ensure that the spawn position is not offset to the left. + if (dir.X < 0) + { + dir.X = 0; + } + } + else + { + dir = new Vector2(1, Rand.Range(-1, 1)); + } + Vector2 targetPos = spawnPos.Value + dir * offset; + var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault(); + if (targetWaypoint != null) + { + spawnPos = targetWaypoint.WorldPosition; } } - else + // Ensure that the position is not inside a submarine (in practice wrecks). + if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(spawnPos.Value))) { - dir = new Vector2(1, Rand.Range(-1, 1)); - } - Vector2 targetPos = spawnPos.Value + dir * offset; - var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault(); - if (targetWaypoint != null) - { - spawnPos = targetWaypoint.WorldPosition; + //no suitable position found, disable the event + spawnPos = null; + Finished(); + return; } } spawnPending = true; @@ -371,7 +384,7 @@ namespace Barotrauma if (spawnPending) { //wait until there are no submarines at the spawnpos - if (spawnPosType.HasFlag(Level.PositionType.MainPath) || spawnPosType.HasFlag(Level.PositionType.SidePath) || spawnPosType.HasFlag(Level.PositionType.Abyss)) + if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss)) { foreach (Submarine submarine in Submarine.Loaded) { @@ -380,17 +393,29 @@ namespace Barotrauma if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } } } - - //if spawning in a ruin/cave, wait for someone to be close to it to spawning - //unnecessary monsters in places the players might never visit during the round - if (spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Cave) || spawnPosType.HasFlag(Level.PositionType.Wreck)) + float minDistance = Prefab.SpawnDistance; + if (minDistance <= 0) + { + if (SpawnPosType.HasFlag(Level.PositionType.Cave)) + { + minDistance = 8000; + } + else if (SpawnPosType.HasFlag(Level.PositionType.Ruin)) + { + minDistance = 5000; + } + else if (SpawnPosType.HasFlag(Level.PositionType.Wreck)) + { + minDistance = 3000; + } + } + if (minDistance > 0) { bool someoneNearby = false; - float minDist = Sonar.DefaultSonarRange * 0.8f; foreach (Submarine submarine in Submarine.Loaded) { if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance)) { someoneNearby = true; break; @@ -400,7 +425,7 @@ namespace Barotrauma { if (c == Character.Controlled || c.IsRemotePlayer) { - if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist) + if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance)) { someoneNearby = true; break; @@ -411,7 +436,7 @@ namespace Barotrauma } - if (spawnPosType.HasFlag(Level.PositionType.Abyss) || spawnPosType.HasFlag(Level.PositionType.AbyssCave)) + if (SpawnPosType.HasFlag(Level.PositionType.Abyss) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave)) { bool anyInAbyss = false; foreach (Submarine submarine in Submarine.Loaded) @@ -432,7 +457,7 @@ namespace Barotrauma int amount = Rand.Range(minAmount, maxAmount + 1); monsters = new List(); float scatterAmount = scatter; - if (spawnPosType.HasFlag(Level.PositionType.SidePath)) + if (SpawnPosType.HasFlag(Level.PositionType.SidePath)) { var sidePaths = Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath); if (sidePaths.Any()) @@ -444,7 +469,7 @@ namespace Barotrauma scatterAmount = scatter; } } - else if (!spawnPosType.HasFlag(Level.PositionType.MainPath)) + else if (!SpawnPosType.HasFlag(Level.PositionType.MainPath)) { scatterAmount = 0; } @@ -474,6 +499,27 @@ namespace Barotrauma } Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true); + var eventManager = GameMain.GameSession.EventManager; + if (eventManager != null) + { + if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath)) + { + eventManager.CumulativeMonsterStrengthMain += createdCharacter.Params.AI.CombatStrength; + eventManager.AddTimeStamp(this); + } + else if (SpawnPosType.HasFlag(Level.PositionType.Ruin)) + { + eventManager.CumulativeMonsterStrengthRuins += createdCharacter.Params.AI.CombatStrength; + } + else if (SpawnPosType.HasFlag(Level.PositionType.Wreck)) + { + eventManager.CumulativeMonsterStrengthWrecks += createdCharacter.Params.AI.CombatStrength; + } + else if (SpawnPosType.HasFlag(Level.PositionType.Cave)) + { + eventManager.CumulativeMonsterStrengthCaves += createdCharacter.Params.AI.CombatStrength; + } + } if (GameMain.GameSession.IsCurrentLocationRadiated()) { AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; @@ -490,6 +536,7 @@ namespace Barotrauma //this will do nothing if the monsters have no swarm behavior defined, //otherwise it'll make the spawned characters act as a swarm SwarmBehavior.CreateSwarm(monsters.Cast()); + DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true); } }, Rand.Range(0f, amount / 2f)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs index f16c75460..cb128e00d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs @@ -56,7 +56,7 @@ namespace Barotrauma public static string Format(this float value, int decimalCount) { - return value.ToString($"F{decimalCount.ToString()}", CultureInfo.InvariantCulture); + return value.ToString($"F{decimalCount}", CultureInfo.InvariantCulture); } public static string FormatSingleDecimal(this Vector2 value) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 8f0c37464..84d41b23a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -254,7 +254,7 @@ namespace Barotrauma if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine) { - SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost, + SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost, AllowStealing = validContainer.Key.Item.AllowStealing, Quality = quality, OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex, diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index a1dfda365..ef3ddc9f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -393,7 +393,7 @@ namespace Barotrauma /// protected abstract void LoadInitialLevel(); - protected abstract IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null); + protected abstract IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults = null); /// /// Which type of transition between levels is currently possible (if any) @@ -572,7 +572,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; } + if (!item.SpawnedInCurrentOutpost || item.OriginalModuleIndex < 0) { continue; } var owner = item.GetRootInventoryOwner(); if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost) { @@ -712,7 +712,7 @@ namespace Barotrauma } } - private IEnumerable DoCharacterWait(Character npc, Character interactor) + private IEnumerable DoCharacterWait(Character npc, Character interactor) { if (npc == null || interactor == null) { yield return CoroutineStatus.Failure; } @@ -907,7 +907,7 @@ namespace Barotrauma public int NumberOfMissionsAtLocation(Location location) { - return Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location)); + return Map?.CurrentLocation?.SelectedMissions?.Count(m => m.Locations.Contains(location)) ?? 0; } public void CheckTooManyMissions(Location currentLocation, Client sender) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 9523e5299..0c9628fc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -312,7 +312,7 @@ namespace Barotrauma return isRadiated; } - public void StartRound(string levelSeed, float? difficulty = null) + public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams levelGenerationParams = null) { LevelData randomLevel = null; foreach (Mission mission in Missions.Union(GameMode.Missions)) @@ -324,11 +324,11 @@ namespace Barotrauma { LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase))); CreateDummyLocations(locationType); - randomLevel = LevelData.CreateRandom(levelSeed, difficulty, requireOutpost: true); + randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; } } - randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty); + randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams); StartRound(randomLevel); } @@ -351,6 +351,8 @@ namespace Barotrauma return; } + Submarine.LockX = Submarine.LockY = false; + LevelData = levelData; Submarine.Unload(); @@ -511,10 +513,6 @@ namespace Barotrauma mpCampaign.UpgradeManager.ApplyUpgrades(); mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine); } - if (GameMode is CampaignMode) - { - Submarine.WarmStartPower(); - } } GameMain.Config.RecentlyEncounteredCreatures.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index ae34d86fe..636c96300 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; } float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax); - conditionIncrease += user.GetStatValue(StatTypes.GeneticMaterialRefineBonus); + conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f; if (item.Prefab == otherGeneticMaterial.item.Prefab) { item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 299afe98d..2233fd79a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -110,7 +110,9 @@ namespace Barotrauma.Items.Components if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; } ActivateNearbySleepingCharacters(); - reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed)); + reloadTimer = reload; + reloadTimer /= (1f + character.GetStatValue(StatTypes.MeleeAttackSpeed)); + reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier)); item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall; @@ -385,7 +387,7 @@ namespace Barotrauma.Items.Components { Attack.SetUser(User); Attack.DamageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); - Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.AttackMultiplier); + Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier); if (targetLimb != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 55170d2ea..f7fc8ec5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -142,7 +142,7 @@ namespace Barotrauma.Items.Components return false; } - private IEnumerable WaitForPick(Character picker, float requiredTime) + private IEnumerable WaitForPick(Character picker, float requiredTime) { activePicker = picker; picker.PickingItem = item; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index abb8dd7ef..89f3efb8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -149,7 +149,8 @@ namespace Barotrauma.Items.Components { float degreeOfFailure = 1.0f - DegreeOfSuccess(user); degreeOfFailure *= degreeOfFailure; - return MathHelper.ToRadians(MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure)); + float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction)); + return MathHelper.ToRadians(spread); } private readonly List limbBodies = new List(); @@ -203,7 +204,7 @@ namespace Barotrauma.Items.Components { lastProjectile?.Item.GetComponent()?.Snap(); } - float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier); + float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.StoppingPowerMultiplier); projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); if (i == 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index be0b65af9..5c8801e2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -632,6 +632,10 @@ namespace Barotrauma.Items.Components public virtual void FlipY(bool relativeToSub) { } + public bool IsLoaded(Character user, bool checkContainedItems = true) => + HasRequiredContainedItems(user, addMessage: false) && + (!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); + public bool HasRequiredContainedItems(Character user, bool addMessage, string msg = null) { if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 27b8542ff..5cad0a43c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -470,7 +470,9 @@ namespace Barotrauma.Items.Components if (!AllowDragAndDrop && user != null) { return false; } if (!slotRestrictions.Any(s => s.MatchesItem(item))) { return false; } if (user != null && !user.CanAccessInventory(Inventory)) { return false; } - + //genetic materials use special logic for combining, don't allow doing it by placing them inside each other here + if (item.GetComponent() != null) { return false; } + if (Inventory.TryPutItem(item, user)) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs index 9cf2e1842..b65b42486 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemLabel.cs @@ -13,6 +13,8 @@ namespace Barotrauma.Items.Components partial void OnStateChanged(); + private string prevColorSignal; + public override void ReceiveSignal(Signal signal, Connection connection) { switch (connection.Name) @@ -22,6 +24,13 @@ namespace Barotrauma.Items.Components Text = signal.value; OnStateChanged(); break; + case "set_text_color": + if (signal.value != prevColorSignal) + { + TextColor = XMLExtensions.ParseColor(signal.value, false); + prevColorSignal = signal.value; + } + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index f8eb62c33..a83b29b97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components if (targetItem == otherItem) { continue; } if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { - user.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); + user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); foreach (Character character in Character.GetFriendlyCrew(user)) { character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); @@ -264,6 +264,8 @@ namespace Barotrauma.Items.Components { Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { + spawnedItem.StolenDuringRound = targetItem.StolenDuringRound; + spawnedItem.AllowStealing = targetItem.AllowStealing; for (int i = 0; i < outputContainer.Capacity; i++) { var containedItem = outputContainer.Inventory.GetItemAt(i); @@ -283,7 +285,13 @@ namespace Barotrauma.Items.Components foreach (ItemContainer ic in targetItem.GetComponents()) { if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } - ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null)); + foreach (Item containedItem in ic.Inventory.AllItemsMod) + { + if (!outputContainer.Inventory.TryPutItem(containedItem, user: null)) + { + containedItem.Drop(dropper: null); + } + } } inputContainer.Inventory.RemoveItem(targetItem); Entity.Spawner.AddToRemoveQueue(targetItem); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index c7d83ec42..279644ef0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -20,6 +20,11 @@ namespace Barotrauma.Items.Components private string savedFabricatedItem; private float savedTimeUntilReady, savedRequiredTime; + private readonly Dictionary> availableIngredients = new Dictionary>(); + + const float RefreshIngredientsInterval = 1.0f; + private float refreshIngredientsTimer; + private bool hasPower; private Character user; @@ -174,6 +179,8 @@ namespace Barotrauma.Items.Components if (selectedItem == null) { return; } if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; } + RefreshAvailableIngredients(); + #if CLIENT itemList.Enabled = false; activateButton.Text = TextManager.Get("FabricatorCancel"); @@ -242,7 +249,13 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - var availableIngredients = GetAvailableIngredients(); + if (refreshIngredientsTimer <= 0.0f) + { + RefreshAvailableIngredients(); + refreshIngredientsTimer = RefreshIngredientsInterval; + } + refreshIngredientsTimer -= deltaTime; + if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) { CancelFabricating(); @@ -271,56 +284,87 @@ namespace Barotrauma.Items.Components State = FabricatorState.Active; } + float tinkeringStrength = 0f; var repairable = item.GetComponent(); if (repairable != null) { repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f; + if (repairable.IsTinkering) + { + tinkeringStrength = repairable.TinkeringStrength; + } } ApplyStatusEffects(ActionType.OnActive, deltaTime, null); if (powerConsumption <= 0) { Voltage = 1.0f; } - float tinkeringStrength = 0f; - if (repairable.IsTinkering) - { - tinkeringStrength = repairable.TinkeringStrength; - } float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f); UpdateRequiredTimeProjSpecific(); - if (timeUntilReady > 0.0f) { return; } + if (timeUntilReady <= 0.0f) + { + Fabricate(); + } + } + + private void Fabricate() + { + RefreshAvailableIngredients(); + if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) + { + CancelFabricating(); + return; + } + + bool ingredientsStolen = false; + bool ingredientsAllowStealing = true; if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - fabricatedItem.RequiredItems.ForEach(requiredItem => { + fabricatedItem.RequiredItems.ForEach(requiredItem => + { for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) { foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } - var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; - var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => + var availableItems = availableIngredients[requiredPrefab.Identifier]; + var availableItem = availableItems.FirstOrDefault(potentialPrefab => { return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; }); - if (availablePrefab == null) { continue; } + if (availableItem == null) { continue; } - if (requiredItem.UseCondition && availablePrefab.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 + ingredientsStolen |= availableItem.StolenDuringRound; + if (!availableItem.AllowStealing) { - availablePrefab.Condition -= availablePrefab.Prefab.Health * requiredItem.MinCondition; - continue; + ingredientsAllowStealing = false; } - availablePrefabs.Remove(availablePrefab); - Entity.Spawner.AddToRemoveQueue(availablePrefab); - inputContainer.Inventory.RemoveItem(availablePrefab); + //Leave it behind with reduced condition if it has enough to stay above 0 + if (requiredItem.UseCondition && availableItem.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) + { + availableItem.Condition -= availableItem.Prefab.Health * requiredItem.MinCondition; + continue; + } + if (availableItem.OwnInventory != null) + { + foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod) + { + containedItem.Drop(dropper: null); + } + } + + availableItems.Remove(availableItem); + Entity.Spawner.AddToRemoveQueue(availableItem); + inputContainer.Inventory.RemoveItem(availableItem); } } }); @@ -351,6 +395,9 @@ namespace Barotrauma.Items.Components onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); + spawnedItem.Quality = quality; + spawnedItem.StolenDuringRound = ingredientsStolen; + spawnedItem.AllowStealing = ingredientsAllowStealing; //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; }); @@ -361,6 +408,9 @@ namespace Barotrauma.Items.Components onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); + spawnedItem.Quality = quality; + spawnedItem.StolenDuringRound = ingredientsStolen; + spawnedItem.AllowStealing = ingredientsAllowStealing; //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; }); @@ -408,6 +458,7 @@ namespace Barotrauma.Items.Components CancelFabricating(); } + } private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) @@ -446,8 +497,8 @@ namespace Barotrauma.Items.Components var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; foreach (Item availablePrefab in availablePrefabs) { - if (availablePrefab.Condition / availablePrefab.Prefab.Health >= requiredItem.MinCondition && - availablePrefab.Condition / availablePrefab.Prefab.Health <= requiredItem.MaxCondition) + if (availablePrefab.ConditionPercentage / 100.0f >= requiredItem.MinCondition && + availablePrefab.ConditionPercentage / 100.0f <= requiredItem.MaxCondition) { availablePrefabsAmount++; } @@ -490,14 +541,10 @@ namespace Barotrauma.Items.Components return SkillRequirementMultiplier; } - /// - /// Get a list of all items available in the input container and linked containers - /// - /// - private Dictionary> GetAvailableIngredients() + private void RefreshAvailableIngredients() { - List availableIngredients = new List(); - availableIngredients.AddRange(inputContainer.Inventory.AllItems); + List itemList = new List(); + itemList.AddRange(inputContainer.Inventory.AllItems); foreach (MapEntity linkedTo in item.linkedTo) { if (linkedTo is Item linkedItem) @@ -511,34 +558,38 @@ namespace Barotrauma.Items.Components itemContainer = deconstructor.OutputContainer; } - availableIngredients.AddRange(itemContainer.Inventory.AllItems); + itemList.AddRange(itemContainer.Inventory.AllItems); + } + } + for (int i = 0; i < itemList.Count; i++) + { + var container = itemList[i].GetComponent(); + if (container != null) + { + itemList.AddRange(container.Inventory.AllItems); } } #if CLIENT if (Character.Controlled?.Inventory != null) { - availableIngredients.AddRange(Character.Controlled.Inventory.AllItems); + itemList.AddRange(Character.Controlled.Inventory.AllItems); } #else if (user?.Inventory != null) { - availableIngredients.AddRange(user.Inventory.AllItems); + itemList.AddRange(user.Inventory.AllItems); } #endif - - Dictionary> ingredientsDictionary = new Dictionary>(); - for (int i = 0; i < availableIngredients.Count; i++) + availableIngredients.Clear(); + foreach (Item item in itemList) { - var itemIdentifier = availableIngredients[i].prefab.Identifier; - if (!ingredientsDictionary.ContainsKey(itemIdentifier)) + var itemIdentifier = item.prefab.Identifier; + if (!availableIngredients.ContainsKey(itemIdentifier)) { - ingredientsDictionary[itemIdentifier] = new List(availableIngredients.Count); + availableIngredients[itemIdentifier] = new List(itemList.Count); } - - ingredientsDictionary[itemIdentifier].Add(availableIngredients[i]); + availableIngredients[itemIdentifier].Add(item); } - - return ingredientsDictionary; } /// @@ -552,7 +603,6 @@ namespace Barotrauma.Items.Components bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; - var availableIngredients = GetAvailableIngredients(); targetItem.RequiredItems.ForEach(requiredItem => { for (int i = 0; i < requiredItem.Amount; i++) { @@ -588,6 +638,7 @@ namespace Barotrauma.Items.Components } } }); + RefreshAvailableIngredients(); } public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 351e57ef4..4fa70bb68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -190,28 +190,38 @@ namespace Barotrauma.Items.Components #if CLIENT if (GameMain.Client != null) { return false; } #endif - - if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase)) + switch (objective.Option.ToLowerInvariant()) { + case "pumpout": #if SERVER - if (objective.Override || FlowPercentage > 0.0f) - { - item.CreateServerEvent(this); - } + if (objective.Override || !IsActive || FlowPercentage > -100.0f) + { + item.CreateServerEvent(this); + } #endif - IsActive = false; - FlowPercentage = 0.0f; - } - else - { + IsActive = true; + FlowPercentage = -100.0f; + break; + case "pumpin": #if SERVER - if (objective.Override || !IsActive || FlowPercentage > -100.0f) - { - item.CreateServerEvent(this); - } + if (objective.Override || !IsActive || FlowPercentage < 100.0f) + { + item.CreateServerEvent(this); + } #endif - IsActive = true; - FlowPercentage = -100.0f; + IsActive = true; + FlowPercentage = 100.0f; + break; + case "stoppumping": +#if SERVER + if (objective.Override || FlowPercentage > 0.0f) + { + item.CreateServerEvent(this); + } +#endif + IsActive = false; + FlowPercentage = 0.0f; + break; } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 8655b2618..273862942 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -12,7 +12,6 @@ namespace Barotrauma.Items.Components partial class Reactor : Powered, IServerSerializable, IClientSerializable { const float NetworkUpdateIntervalHigh = 0.5f; - const float NetworkUpdateIntervalLow = 10.0f; //the rate at which the reactor is being run on (higher rate -> higher temperature) private float fissionRate; @@ -38,9 +37,8 @@ namespace Barotrauma.Items.Components private float maxPowerOutput; - private Queue loadQueue = new Queue(); - private float load; - + private readonly Queue loadQueue = new Queue(); + private bool unsentChanges; private float sendUpdateTimer; @@ -158,11 +156,6 @@ namespace Barotrauma.Items.Components set { /*do nothing*/ } } - private float correctTurbineOutput; - - private float targetFissionRate; - private float targetTurbineOutput; - [Serialize(false, true, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public bool AutoTemp { @@ -181,6 +174,18 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, true)] public float AvailableFuel { get; set; } + [Serialize(0.0f, true)] + public new float Load { get; private set; } + + [Serialize(0.0f, true)] + public float TargetFissionRate { get; set; } + + [Serialize(0.0f, true)] + public float TargetTurbineOutput { get; set; } + + [Serialize(0.0f, true)] + public float CorrectTurbineOutput { get; set; } + public Reactor(Item item, XElement element) : base(item, element) { @@ -199,8 +204,8 @@ namespace Barotrauma.Items.Components { GameServer.Log(GameServer.CharacterLogName(lastUser) + " adjusted reactor settings: " + "Temperature: " + (int)(temperature * 100.0f) + - ", Fission rate: " + (int)targetFissionRate + - ", Turbine output: " + (int)targetTurbineOutput + + ", Fission rate: " + (int)TargetFissionRate + + ", Turbine output: " + (int)TargetTurbineOutput + (autoTemp ? ", Autotemp ON" : ", Autotemp OFF"), ServerLog.MessageType.ItemInteraction); @@ -223,7 +228,7 @@ namespace Barotrauma.Items.Components } #if CLIENT - if(PowerOn && AvailableFuel < 1) + if (PowerOn && AvailableFuel < 1) { HintManager.OnReactorOutOfFuel(this); } @@ -236,15 +241,15 @@ namespace Barotrauma.Items.Components //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f)) { - correctTurbineOutput += MathHelper.Clamp((load / MaxPowerOutput * 100.0f) - correctTurbineOutput, -10.0f, 10.0f) * deltaTime; + CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -10.0f, 10.0f) * deltaTime; } //calculate tolerances of the meters based on the skills of the user //more skilled characters have larger "sweet spots", making it easier to keep the power output at a suitable level float tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess); - optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); + optimalTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance); tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess); - allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); + allowedTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance); optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess); allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess); @@ -260,9 +265,9 @@ namespace Barotrauma.Items.Components Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff)); //if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime; - FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime); + FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime); - TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime); + TurbineOutput = MathHelper.Lerp(turbineOutput, TargetTurbineOutput, deltaTime); float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); currPowerConsumption = -MaxPowerOutput * Math.Min(turbineOutput / 100.0f, temperatureFactor); @@ -276,7 +281,7 @@ namespace Barotrauma.Items.Components float maxAutoAdjust = maxPowerOutput * 0.1f; autoAdjustAmount = MathHelper.Lerp( autoAdjustAmount, - MathHelper.Clamp(-load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust), + MathHelper.Clamp(-Load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust), deltaTime * 10.0f); } else @@ -287,8 +292,8 @@ namespace Barotrauma.Items.Components if (!PowerOn) { - targetFissionRate = 0.0f; - targetTurbineOutput = 0.0f; + TargetFissionRate = 0.0f; + TargetTurbineOutput = 0.0f; } else if (autoTemp) { @@ -317,56 +322,30 @@ namespace Barotrauma.Items.Components } } - if (!loadQueue.Any() && PowerOn) - { - //loadQueue is empty, round must've just started - //reset the fission rate, turbine output and - //temperature to optimal levels to prevent fires - //at the start of the round - correctTurbineOutput = MathUtils.NearlyEqual(MaxPowerOutput, 0.0f) ? 0.0f : currentLoad / MaxPowerOutput * 100.0f; - tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess); - optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); - tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess); - allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); - - DebugConsole.Log($"Degree of success: {degreeOfSuccess}"); - DebugConsole.Log($"Current load: {currentLoad}"); - DebugConsole.Log($"Max power output: {MaxPowerOutput}"); - DebugConsole.Log($"Available fuel: {AvailableFuel}"); - - float desiredTurbineOutput = MathHelper.Clamp(correctTurbineOutput, 0.0f, 100.0f); - DebugConsole.Log($"Turbine output reset: {targetTurbineOutput}, {turbineOutput} -> {desiredTurbineOutput}"); - targetTurbineOutput = desiredTurbineOutput; - turbineOutput = desiredTurbineOutput; - - float desiredTemperature = (optimalTemperature.X + optimalTemperature.Y) / 2.0f; - DebugConsole.Log($"Temperature reset: {temperature} -> {desiredTemperature}"); - temperature = desiredTemperature; - - float desiredFissionRate = GetFissionRateForTargetTemperatureAndTurbineOutput(desiredTemperature, desiredTurbineOutput); - DebugConsole.Log($"Fission rate reset: {targetFissionRate}, {fissionRate} -> {desiredFissionRate}"); - targetFissionRate = desiredFissionRate; - fissionRate = desiredFissionRate; - } - loadQueue.Enqueue(currentLoad); while (loadQueue.Count() > 60.0f) { - load = loadQueue.Average(); + Load = loadQueue.Average(); loadQueue.Dequeue(); } + float fuelLeft = 0.0f; + var containedItems = item.OwnInventory?.AllItems; + if (containedItems != null) + { + foreach (Item item in containedItems) + { + if (!item.HasTag("reactorfuel")) { continue; } + if (fissionRate > 0.0f) + { + item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + } + fuelLeft += item.ConditionPercentage; + } + } + if (fissionRate > 0.0f) { - var containedItems = item.OwnInventory?.AllItems; - if (containedItems != null) - { - foreach (Item item in containedItems) - { - if (!item.HasTag("reactorfuel")) { continue; } - item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; - } - } if (item.AiTarget != null && MaxPowerOutput > 0) { var aiTarget = item.AiTarget; @@ -385,8 +364,9 @@ namespace Barotrauma.Items.Components item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out"); item.SendSignal(((int)-CurrPowerConsumption).ToString(), "power_value_out"); - item.SendSignal(((int)load).ToString(), "load_value_out"); + item.SendSignal(((int)Load).ToString(), "load_value_out"); item.SendSignal(((int)AvailableFuel).ToString(), "fuel_out"); + item.SendSignal(((int)fuelLeft).ToString(), "fuel_percentage_left"); UpdateFailures(deltaTime); #if CLIENT @@ -407,8 +387,7 @@ namespace Barotrauma.Items.Components { item.CreateServerEvent(this); } -#endif -#if CLIENT +#elif CLIENT if (GameMain.Client != null) { item.CreateClientEvent(this); @@ -424,12 +403,6 @@ namespace Barotrauma.Items.Components return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f; } - private float GetFissionRateForTargetTemperatureAndTurbineOutput(float temperature, float turbineOutput) - { - if (MathUtils.NearlyEqual(AvailableFuel, 0f)) { return 0f; } - return (temperature + turbineOutput) / (AvailableFuel / 100f) / 2f; - } - /// /// Do we need more fuel to generate enough power to match the current load. /// @@ -438,7 +411,7 @@ namespace Barotrauma.Items.Components private bool NeedMoreFuel(float minimumOutputRatio, float minCondition = 0) { float remainingFuel = item.ContainedItems.Sum(i => i.Condition); - if (remainingFuel <= minCondition && load > 0.0f) + if (remainingFuel <= minCondition && Load > 0.0f) { return true; } @@ -455,7 +428,7 @@ namespace Barotrauma.Items.Components float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput; //maximum output not enough, we need more fuel - return theoreticalMaxOutput < load * minimumOutputRatio; + return theoreticalMaxOutput < Load * minimumOutputRatio; } private bool TooMuchFuel() @@ -467,7 +440,7 @@ namespace Barotrauma.Items.Components float minimumHeat = GetGeneratedHeat(optimalFissionRate.X); //if we need a very high turbine output to keep the engine from overheating, there's too much fuel - return minimumHeat > Math.Min(correctTurbineOutput * 1.5f, 90); + return minimumHeat > Math.Min(CorrectTurbineOutput * 1.5f, 90); } private void UpdateFailures(float deltaTime) @@ -514,26 +487,26 @@ namespace Barotrauma.Items.Components public void UpdateAutoTemp(float speed, float deltaTime) { float desiredTurbineOutput = (optimalTurbineOutput.X + optimalTurbineOutput.Y) / 2.0f; - targetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - targetTurbineOutput, -speed, speed) * deltaTime; - targetTurbineOutput = MathHelper.Clamp(targetTurbineOutput, 0.0f, 100.0f); + TargetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - TargetTurbineOutput, -speed, speed) * deltaTime; + TargetTurbineOutput = MathHelper.Clamp(TargetTurbineOutput, 0.0f, 100.0f); float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f; - targetFissionRate += MathHelper.Clamp(desiredFissionRate - targetFissionRate, -speed, speed) * deltaTime; + TargetFissionRate += MathHelper.Clamp(desiredFissionRate - TargetFissionRate, -speed, speed) * deltaTime; if (temperature > (optimalTemperature.X + optimalTemperature.Y) / 2.0f) { - targetFissionRate = Math.Min(targetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y); + TargetFissionRate = Math.Min(TargetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y); } - else if (-currPowerConsumption < load) + else if (-currPowerConsumption < Load) { - targetFissionRate = Math.Min(targetFissionRate + speed * 2 * deltaTime, 100.0f); + TargetFissionRate = Math.Min(TargetFissionRate + speed * 2 * deltaTime, 100.0f); } - targetFissionRate = MathHelper.Clamp(targetFissionRate, 0.0f, 100.0f); + TargetFissionRate = MathHelper.Clamp(TargetFissionRate, 0.0f, 100.0f); //don't push the target too far from the current fission rate //otherwise we may "overshoot", cranking the target fission rate all the way up because it takes a while //for the actual fission rate and temperature to follow - targetFissionRate = MathHelper.Clamp(targetFissionRate, FissionRate - 5, FissionRate + 5); + TargetFissionRate = MathHelper.Clamp(TargetFissionRate, FissionRate - 5, FissionRate + 5); } public void PowerUpImmediately() @@ -557,8 +530,8 @@ namespace Barotrauma.Items.Components currPowerConsumption = 0.0f; Temperature -= deltaTime * 1000.0f; - targetFissionRate = Math.Max(targetFissionRate - deltaTime * 10.0f, 0.0f); - targetTurbineOutput = Math.Max(targetTurbineOutput - deltaTime * 10.0f, 0.0f); + TargetFissionRate = Math.Max(TargetFissionRate - deltaTime * 10.0f, 0.0f); + TargetTurbineOutput = Math.Max(TargetTurbineOutput - deltaTime * 10.0f, 0.0f); #if CLIENT FissionRateScrollBar.BarScroll = 1.0f - FissionRate / 100.0f; TurbineOutputScrollBar.BarScroll = 1.0f - TurbineOutput / 100.0f; @@ -583,7 +556,6 @@ namespace Barotrauma.Items.Components containedItem.Condition = 0.0f; } } - #if SERVER GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction); if (GameMain.Server != null) @@ -696,15 +668,15 @@ namespace Barotrauma.Items.Components bool prevAutoTemp = autoTemp; bool prevPowerOn = _powerOn; - float prevFissionRate = targetFissionRate; - float prevTurbineOutput = targetTurbineOutput; + float prevFissionRate = TargetFissionRate; + float prevTurbineOutput = TargetTurbineOutput; if (shutDown) { PowerOn = false; AutoTemp = false; - targetFissionRate = 0.0f; - targetTurbineOutput = 0.0f; + TargetFissionRate = 0.0f; + TargetTurbineOutput = 0.0f; unsentChanges = true; return true; } @@ -730,8 +702,8 @@ namespace Barotrauma.Items.Components #endif if (autoTemp != prevAutoTemp || prevPowerOn != _powerOn || - Math.Abs(prevFissionRate - targetFissionRate) > 1.0f || - Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f) + Math.Abs(prevFissionRate - TargetFissionRate) > 1.0f || + Math.Abs(prevTurbineOutput - TargetTurbineOutput) > 1.0f) { unsentChanges = true; } @@ -767,32 +739,32 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "shutdown": - if (targetFissionRate > 0.0f || targetTurbineOutput > 0.0f) + if (TargetFissionRate > 0.0f || TargetTurbineOutput > 0.0f) { PowerOn = false; AutoTemp = false; - targetFissionRate = 0.0f; - targetTurbineOutput = 0.0f; + TargetFissionRate = 0.0f; + TargetTurbineOutput = 0.0f; if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } } break; case "set_fissionrate": if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) { - targetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f); + TargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f); if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } #if CLIENT - FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; + FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f; #endif } break; case "set_turbineoutput": if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) { - targetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f); + TargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f); if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } #if CLIENT - TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; + TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f; #endif } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index a656e622b..c0a64ef76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -17,6 +17,8 @@ namespace Barotrauma.Items.Components public const float DefaultSonarRange = 10000.0f; + public const float PassivePowerConsumption = 0.1f; + class ConnectedTransducer { public readonly SonarTransducer Transducer; @@ -150,7 +152,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * 0.1f; + currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption; UpdateOnActiveEffects(deltaTime); @@ -332,7 +334,9 @@ namespace Barotrauma.Items.Components if (connection.Name == "transducer_in") { var transducer = signal.source.GetComponent(); - if (transducer == null) return; + if (transducer == null) { return; } + + transducer.ConnectedSonar = this; var connectedTransducer = connectedTransducers.Find(t => t.Transducer == transducer); if (connectedTransducer == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs index 3d09af2aa..b4c9c7252 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs @@ -8,6 +8,8 @@ namespace Barotrauma.Items.Components private float sendSignalTimer; + public Sonar ConnectedSonar; + public SonarTransducer(Item item, XElement element) : base(item, element) { IsActive = true; @@ -17,7 +19,7 @@ namespace Barotrauma.Items.Components { UpdateOnActiveEffects(deltaTime); - CurrPowerConsumption = powerConsumption; + CurrPowerConsumption = powerConsumption * (ConnectedSonar?.CurrentMode == Sonar.Mode.Active ? 1.0f : Sonar.PassivePowerConsumption); if (Voltage >= MinVoltage) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 8cc3945de..b44026a4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -61,6 +61,8 @@ namespace Barotrauma.Items.Components public List IgnoredBodies; + private Character stickTargetCharacter; + private Character _user; public Character User { @@ -614,8 +616,8 @@ namespace Barotrauma.Items.Components return; } - //target very far from the item -> update the item's transform to make sure it's inside the same sub as the target (or outside) - if (Math.Abs(stickJoint.JointTranslation) > 100.0f) + // Update the item's transform to make sure it's inside the same sub as the target (or outside) + if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > 100.0f) { item.UpdateTransform(); } @@ -866,7 +868,7 @@ namespace Barotrauma.Items.Components (DoesStick || (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || (StickToStructures && target.Body.UserData is Structure) || - (StickToItems && target.Body.UserData is Item))) + (StickToItems && target.Body.UserData is Item))) { Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), @@ -965,9 +967,14 @@ namespace Barotrauma.Items.Components GameMain.World.Add(stickJoint); IsActive = true; + if (targetBody.UserData is Limb limb) + { + stickTargetCharacter = limb.character; + stickTargetCharacter.AttachedProjectiles.Add(this); + } } - private void Unstick() + public void Unstick() { StickTarget = null; if (stickJoint != null) @@ -979,25 +986,21 @@ namespace Barotrauma.Items.Components stickJoint = null; } if (!item.body.FarseerBody.IsBullet) { IsActive = false; } + item.GetComponent()?.Snap(); + if (stickTargetCharacter != null) + { + stickTargetCharacter.AttachedProjectiles.Remove(this); + stickTargetCharacter = null; + } } protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); - if (stickJoint != null) + if (IsStuckToTarget || stickJoint != null || stickTargetCharacter != null) { - try - { - GameMain.World.Remove(stickJoint); - } - catch - { - //the body that the projectile was stuck to has been removed - } - - stickJoint = null; + Unstick(); } - } partial void LaunchProjSpecific(Vector2 startLocation, Vector2 endLocation); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 5ffe84c32..75f82175c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -26,6 +26,10 @@ namespace Barotrauma.Items.Components RepairToolStructureRepairMultiplier, RepairToolStructureDamageMultiplier, RepairToolDeattachTimeMultiplier, + StoppingPowerMultiplier, + StrikingPowerMultiplier, + StrikingSpeedMultiplier, + FiringRateMultiplier, // unused as of now AttackMultiplier, AttackSpeedMultiplier, @@ -33,7 +37,6 @@ namespace Barotrauma.Items.Components RangedSpreadReduction, ChargeSpeedMultiplier, MovementSpeedMultiplier, - // generic stats to be used for various needs, declared just in case (localization) EffectivenessMultiplier, PowerOutputMultiplier, ConsumptionReductionMultiplier, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 38331c971..7a6f66a64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -105,11 +105,6 @@ namespace Barotrauma.Items.Components public bool IsTinkering { get; private set; } = false; - public float RepairIconThreshold - { - get { return RepairThreshold / 2; } - } - public Character CurrentFixer { get; private set; } private Item currentRepairItem; @@ -118,6 +113,9 @@ namespace Barotrauma.Items.Components public float TinkeringStrength => tinkeringStrength; + public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold; + public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2; + public enum FixActions : int { None = 0, @@ -179,8 +177,17 @@ namespace Barotrauma.Items.Components if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; } // unpowered (electrical) items can be repaired without a risk of electrical shock - if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)) && - item.GetComponent() is Powered powered && powered.Voltage < 0.1f) { return true; } + if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) + { + if (item.GetComponent() is Reactor reactor) + { + if (MathUtils.NearlyEqual(reactor.CurrPowerConsumption, 0.0f, 0.1f)) { return true; } + } + else if (item.GetComponent() is Powered powered && powered.Voltage < 0.1f) + { + return true; + } + } if (Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, requiredSkills)) { return true; } @@ -393,7 +400,7 @@ namespace Barotrauma.Items.Components float successFactor = requiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, requiredSkills); //item must have been below the repair threshold for the player to get an achievement or XP for repairing it - if (item.ConditionPercentage < RepairThreshold) + if (IsBelowRepairThreshold) { wasBroken = true; } @@ -524,7 +531,7 @@ namespace Barotrauma.Items.Components public void AdjustPowerConsumption(ref float powerConsumption) { - if (item.ConditionPercentage < RepairThreshold) + if (IsBelowRepairThreshold) { powerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 1e696bf14..432e96f31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -306,9 +306,13 @@ namespace Barotrauma.Items.Components forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f); } } - float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance)) : TargetPullForce; + float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce; targetBody?.ApplyForce(-forceDir * force); - targetCharacter?.AnimController.Collider.ApplyForce(-forceDir * force * 3); + var targetRagdoll = targetCharacter?.AnimController; + if (targetRagdoll != null && (targetRagdoll.InWater || targetRagdoll.OnGround)) + { + targetRagdoll.Collider.ApplyForce(-forceDir * force * 3); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index 70215590e..d54ec8b92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -19,6 +19,10 @@ namespace Barotrauma.Items.Components get { return timeFrame; } set { + if (value > timeFrame) + { + timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f); + } timeFrame = Math.Max(0.0f, value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index 98c580ec4..ecf63774b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -39,6 +39,10 @@ namespace Barotrauma.Items.Components get { return timeFrame; } set { + if (value > timeFrame) + { + timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f); + } timeFrame = Math.Max(0.0f, value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index e2f327502..3249973db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; - +using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { class DelayComponent : ItemComponent @@ -114,6 +114,18 @@ namespace Barotrauma.Items.Components }; signalQueue.Enqueue(prevQueuedSignal); break; + case "set_delay": + if (float.TryParse(signal.value, out float newDelay)) + { + newDelay = MathHelper.Clamp(newDelay, 0, 60); + if (signalQueue.Count > 0 && newDelay != Delay) + { + prevQueuedSignal = null; + signalQueue.Clear(); + } + Delay = newDelay; + } + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 6283a3449..19175aaf3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -62,6 +62,10 @@ namespace Barotrauma.Items.Components get { return timeFrame; } set { + if (value > timeFrame) + { + timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f); + } timeFrame = Math.Max(0.0f, value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 90814c3cb..876e785e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -43,7 +43,16 @@ namespace Barotrauma.Items.Components } } - public float Rotation; + private float rotation; + public float Rotation + { + get { return rotation; } + set + { + rotation = value; + SetLightSourceTransform(); + } + } [Editable, Serialize(true, true, description: "Should structures cast shadows when light from this light source hits them. " + "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.", alwaysUseInstanceValues: true)] @@ -246,39 +255,14 @@ namespace Barotrauma.Items.Components SetLightSourceState(false, 0.0f); return; } -#if CLIENT - if (ParentBody != null) - { - Light.Position = ParentBody.Position; - } - else if (turret != null) - { - Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y); - } - else - { - Light.Position = item.Position; - } -#endif + + SetLightSourceTransform(); + PhysicsBody body = ParentBody ?? item.body; - if (body != null) + if (body != null && !body.Enabled) { -#if CLIENT - Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; - Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically; -#endif - if (!body.Enabled) - { - SetLightSourceState(false, 0.0f); - return; - } - } - else - { -#if CLIENT - Light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation); - Light.LightSpriteEffect = item.SpriteEffects; -#endif + SetLightSourceState(false, 0.0f); + return; } currPowerConsumption = powerConsumption; @@ -333,6 +317,9 @@ namespace Barotrauma.Items.Components if (signal.value != prevColorSignal) { LightColor = XMLExtensions.ParseColor(signal.value, false); +#if CLIENT + SetLightSourceState(Light.Enabled, currentBrightness); +#endif prevColorSignal = signal.value; } break; @@ -350,5 +337,8 @@ namespace Barotrauma.Items.Components } partial void SetLightSourceState(bool enabled, float brightness); + + partial void SetLightSourceTransform(); + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index 045ffd45a..39ee77a58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -34,7 +34,12 @@ namespace Barotrauma.Items.Components } } - protected bool writeable = true; + [Editable, Serialize(true, true, description: "Can the value stored in the memory component be changed via signals.", alwaysUseInstanceValues: true)] + public bool Writeable + { + get; + set; + } public MemoryComponent(Item item, XElement element) : base(item, element) @@ -54,7 +59,7 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "signal_in": - if (writeable) + if (Writeable) { string prevValue = Value; Value = signal.value; @@ -66,7 +71,7 @@ namespace Barotrauma.Items.Components break; case "signal_store": case "lock_state": - writeable = signal.value == "1"; + Writeable = signal.value == "1"; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 0ead9540d..14abecf89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -131,6 +131,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, true, description: "Should the sensor trigger when the item itself moves.")] + public bool DetectOwnMotion + { + get; + set; + } + public MotionSensor(Item item, XElement element) : base(item, element) { @@ -168,7 +175,7 @@ namespace Barotrauma.Items.Components MotionDetected = false; updateTimer = UpdateInterval; - if (item.body != null && item.body.Enabled) + if (item.body != null && item.body.Enabled && DetectOwnMotion) { if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index d19349597..8e7ea8674 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -6,6 +6,8 @@ namespace Barotrauma.Items.Components { class RegExFindComponent : ItemComponent { + private static readonly TimeSpan timeout = TimeSpan.FromSeconds(Timing.Step); + private string expression; private string receivedSignal; @@ -67,7 +69,10 @@ namespace Barotrauma.Items.Components try { - regex = new Regex(@expression); + regex = new Regex( + @expression, + options: RegexOptions.None, + matchTimeout: timeout); } catch @@ -97,11 +102,14 @@ namespace Barotrauma.Items.Components previousResult = match.Success; previousGroups = UseCaptureGroup && previousResult ? match.Groups : null; previousReceivedSignal = receivedSignal; - } - catch + catch (Exception e) { - item.SendSignal("ERROR", "signal_out"); + item.SendSignal( + e is RegexMatchTimeoutException + ? "TIMEOUT" + : "ERROR", + "signal_out"); previousResult = false; return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs index 45189882f..5333bec84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs @@ -24,6 +24,10 @@ namespace Barotrauma.Items.Components get { return timeFrame; } set { + if (value > timeFrame) + { + timeSinceReceived[0] = timeSinceReceived[1] = Math.Max(value * 2.0f, 0.1f); + } timeFrame = Math.Max(0.0f, value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 335827046..1c4bb9b4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -2,16 +2,35 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { + readonly struct TerminalMessage + { + public readonly string Text; + public readonly Color Color; + + public TerminalMessage(string text, Color color) + { + Text = text; + Color = color; + } + + public void Deconstruct(out string text, out Color color) + { + text = Text; + color = Color; + } + } + partial class Terminal : ItemComponent { private const int MaxMessageLength = ChatMessage.MaxLength; private const int MaxMessages = 60; - private List messageHistory = new List(MaxMessages); + private List messageHistory = new List(MaxMessages); public string DisplayedWelcomeMessage { @@ -37,19 +56,39 @@ namespace Barotrauma.Items.Components /// public string ShowMessage { - get { return messageHistory.Count == 0 ? string.Empty : messageHistory.Last(); } + get { return messageHistory.Count == 0 ? string.Empty : messageHistory.Last().Text; } set { if (string.IsNullOrEmpty(value)) { return; } - ShowOnDisplay(value, addToHistory: true); + ShowOnDisplay(value, addToHistory: true, TextColor); } } [Editable, Serialize(false, true, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] public bool UseMonospaceFont { get; set; } + private Color textColor = Color.LimeGreen; + + [Editable, Serialize("50,205,50,255", true, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] + public Color TextColor + { + get => textColor; + set + { + textColor = value; +#if CLIENT + if (inputBox is { } input) + { + input.TextColor = value; + } +#endif + } + } + private string OutputValue { get; set; } + private string prevColorSignal; + public Terminal(Item item, XElement element) : base(item, element) { @@ -59,18 +98,39 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); - partial void ShowOnDisplay(string input, bool addToHistory); + partial void ShowOnDisplay(string input, bool addToHistory, Color color); public override void ReceiveSignal(Signal signal, Connection connection) { - if (connection.Name != "signal_in") { return; } - if (signal.value.Length > MaxMessageLength) + switch (connection.Name) { - signal.value = signal.value.Substring(0, MaxMessageLength); - } + case "set_text": - string inputSignal = signal.value.Replace("\\n", "\n"); - ShowOnDisplay(inputSignal, addToHistory: true); + if (signal.value.Length > MaxMessageLength) + { + signal.value = signal.value.Substring(0, MaxMessageLength); + } + + string inputSignal = signal.value.Replace("\\n", "\n"); + ShowOnDisplay(inputSignal, addToHistory: true, TextColor); + break; + case "set_text_color": + if (signal.value != prevColorSignal) + { + TextColor = XMLExtensions.ParseColor(signal.value, false); + prevColorSignal = signal.value; + } + break; + case "clear_text" when signal.value != "0": + messageHistory.Clear(); +#if CLIENT + if (historyBox?.Content is { } history) + { + history.ClearChildren(); + } +#endif + break; + } } public override void OnItemLoaded() @@ -83,7 +143,7 @@ namespace Barotrauma.Items.Components base.OnItemLoaded(); if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) { - ShowOnDisplay(DisplayedWelcomeMessage, addToHistory: !isSubEditor); + ShowOnDisplay(DisplayedWelcomeMessage, addToHistory: !isSubEditor, TextColor); DisplayedWelcomeMessage = ""; //remove welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) @@ -98,7 +158,8 @@ namespace Barotrauma.Items.Components var componentElement = base.Save(parentElement); for (int i = 0; i < messageHistory.Count; i++) { - componentElement.Add(new XAttribute("msg" + i, messageHistory[i])); + componentElement.Add(new XAttribute("msg" + i, messageHistory[i].Text)); + componentElement.Add(new XAttribute("color" + i, messageHistory[i].Color.ToStringHex())); } return componentElement; } @@ -109,8 +170,9 @@ namespace Barotrauma.Items.Components for (int i = 0; i < MaxMessages; i++) { string msg = componentElement.GetAttributeString("msg" + i, null); - if (msg == null) { break; } - ShowOnDisplay(msg, addToHistory: true); + if (msg is null) { break; } + Color color = componentElement.GetAttributeColor("color" + i, TextColor); + ShowOnDisplay(msg, addToHistory: true, color); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 7d3e3caad..6814915aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -17,8 +17,8 @@ namespace Barotrauma.Items.Components Atan, } - private float[] receivedSignal = new float[2]; - private float[] timeSinceReceived = new float[2]; + private readonly float[] receivedSignal = new float[2]; + private readonly float[] timeSinceReceived = new float[2]; [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 70d4403b1..06e12a07a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -125,7 +125,17 @@ namespace Barotrauma.Items.Components if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; - } + } + + //if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing + // = no point in receiving + if (!LinkToChat) + { + if (signalOutConnection == null || !signalOutConnection.Wires.Any(w => w != null)) + { + return false; + } + } if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 10ef02bdb..b7ff7d8bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -9,7 +9,7 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { - class TriggerComponent : ItemComponent + partial class TriggerComponent : ItemComponent { [Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] public float Force { get; set; } @@ -18,12 +18,32 @@ namespace Barotrauma.Items.Components private float Radius { get; set; } private float RadiusInDisplayUnits { get; set; } private bool TriggeredOnce { get; set; } - + private float CurrentForceFluctuation { get; set; } = 1.0f; public bool TriggerActive { get; private set; } + private float ForceFluctuationTimer { get; set; } + private static float TimeInLevel + { + get + { + if (GameMain.GameSession != null) + { + return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime); + } + else + { + return 0.0f; + } + } + } private readonly LevelTrigger.TriggererType triggeredBy; private readonly HashSet triggerers = new HashSet(); private readonly bool triggerOnce; + private readonly bool distanceBasedForce; + private readonly bool forceFluctuation; + private readonly float forceFluctuationStrength; + private readonly float forceFluctuationFrequency; + private readonly float forceFluctuationInterval; private readonly List statusEffectTargets = new List(); /// /// Effects applied to entities inside the trigger @@ -42,6 +62,15 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); } triggerOnce = element.GetAttributeBool("triggeronce", false); + distanceBasedForce = element.GetAttributeBool("distancebasedforce", false); + forceFluctuation = element.GetAttributeBool("forcefluctuation", false); + forceFluctuationStrength = element.GetAttributeFloat("forcefluctuationstrength", 1.0f); + forceFluctuationStrength = Math.Clamp(forceFluctuationStrength, 0.0f, 1.0f); + forceFluctuationFrequency = element.GetAttributeFloat("fluctuationfrequency", 1.0f); + forceFluctuationFrequency = Math.Max(forceFluctuationFrequency, 0.01f); + forceFluctuationInterval = element.GetAttributeFloat("fluctuationinterval", 0.01f); + forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f); + string parentDebugName = $"TriggerComponent in {item.Name}"; foreach (XElement subElement in element.Elements()) { @@ -128,6 +157,19 @@ namespace Barotrauma.Items.Components TriggerActive = triggerers.Any(); + if (forceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + { + ForceFluctuationTimer += deltaTime; + if (ForceFluctuationTimer >= forceFluctuationInterval) + { + float v = MathF.Sin(2 * MathF.PI * forceFluctuationFrequency * TimeInLevel); + float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v); + CurrentForceFluctuation = MathHelper.Lerp(1.0f - forceFluctuationStrength, 1.0f, amount); + ForceFluctuationTimer = 0.0f; + GameMain.NetworkMember?.CreateEntityEvent(this); + } + } + foreach (Entity triggerer in triggerers) { LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets); @@ -167,9 +209,9 @@ namespace Barotrauma.Items.Components { Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); if (diff.LengthSquared() < 0.0001f) { return; } - float distanceFactor = LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits); + float distanceFactor = distanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; if (distanceFactor <= 0.0f) { return; } - Vector2 force = distanceFactor * Force * Vector2.Normalize(diff); + Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff); if (force.LengthSquared() < 0.01f) { return; } body.ApplyForce(force); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 327c23181..9c4770659 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -68,6 +68,9 @@ namespace Barotrauma.Items.Components private const float TinkeringDamageIncrease = 0.2f; private const float TinkeringReloadDecrease = 0.2f; + public Character ActiveUser; + private float resetActiveUserTimer; + public float Rotation { get { return rotation; } @@ -362,7 +365,7 @@ namespace Barotrauma.Items.Components UpdateTransformedBarrelPos(); } - if (user != null && user.Removed) + if (user is { Removed: true }) { user = null; } @@ -371,6 +374,19 @@ namespace Barotrauma.Items.Components resetUserTimer -= deltaTime; if (resetUserTimer <= 0.0f) { user = null; } } + + if (ActiveUser is { Removed: true }) + { + ActiveUser = null; + } + else + { + resetActiveUserTimer -= deltaTime; + if (resetActiveUserTimer <= 0.0f) + { + ActiveUser = null; + } + } ApplyStatusEffects(ActionType.OnActive, deltaTime, null); @@ -744,8 +760,10 @@ namespace Barotrauma.Items.Components if (projectileComponent != null) { projectileComponent.Attacker = projectileComponent.User = user; - projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength); - + if (projectileComponent.Attack != null) + { + projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength); + } projectileComponent.Use(); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; @@ -956,10 +974,11 @@ namespace Barotrauma.Items.Components public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { - if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && - previousTarget.IsDead) + if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { - character.Speak(TextManager.Get("DialogTurretTargetDead"), identifier: "killedtarget" + previousTarget.ID, minDurationBetweenSimilar: 10.0f); + character.Speak(TextManager.Get("DialogTurretTargetDead"), + identifier: "killedtarget" + previousTarget.ID, + minDurationBetweenSimilar: 10.0f); character.AIController.SelectTarget(null); } @@ -986,7 +1005,9 @@ namespace Barotrauma.Items.Components } else { - character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"), identifier: "supercapacitorisbroken", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"), + identifier: "supercapacitorisbroken", + minDurationBetweenSimilar: 30.0f); canShoot = false; } } @@ -999,7 +1020,9 @@ namespace Barotrauma.Items.Components } if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0) { - character.Speak(TextManager.Get("DialogTurretHasNoPower"), identifier: "turrethasnopower", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get("DialogTurretHasNoPower"), + identifier: "turrethasnopower", + minDurationBetweenSimilar: 30.0f); canShoot = false; } } @@ -1039,7 +1062,9 @@ namespace Barotrauma.Items.Components { if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "cannotloadturret", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true), + identifier: "cannotloadturret", + minDurationBetweenSimilar: 30.0f); } return true; } @@ -1049,7 +1074,9 @@ namespace Barotrauma.Items.Components loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "loadturret", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true), + identifier: "loadturret", + minDurationBetweenSimilar: 30.0f); } loadItemsObjective.Abandoned += CheckRemainingAmmo; loadItemsObjective.Completed += CheckRemainingAmmo; @@ -1063,11 +1090,15 @@ namespace Barotrauma.Items.Components int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); if (remainingAmmo == 0) { - character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), + identifier: "outofammo", + minDurationBetweenSimilar: 30.0f); } else if (remainingAmmo < 3) { - character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), + identifier: "outofammo", + minDurationBetweenSimilar: 30.0f); } } } @@ -1090,7 +1121,8 @@ namespace Barotrauma.Items.Components float closestDistance = maxDistance * maxDistance; - if (currentTarget != null) + bool hadCurrentTarget = currentTarget != null; + if (hadCurrentTarget) { if (currentTarget.Removed || currentTarget.IsDead) { @@ -1222,24 +1254,32 @@ namespace Barotrauma.Items.Components { if (character.IsOnPlayerTeam) { - if (character.AIController.SelectedAiTarget == null) + if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName)) { - character.Speak(TextManager.Get("DialogNewTargetSpotted"), null, 0.0f, "newtargetspotted", 30.0f); + character.Speak(TextManager.Get("DialogNewTargetSpotted"), + identifier: "newtargetspotted", + minDurationBetweenSimilar: 30.0f); } else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) { - character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), null, 0.0f, "identifiedtargetspotted", 30.0f); + character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), + identifier: "identifiedtargetspotted", + minDurationBetweenSimilar: 30.0f); } else { - character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f); + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), + identifier: "unidentifiedtargetspotted", + minDurationBetweenSimilar: 5.0f); } } else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) { - character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f); + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), + identifier: "unidentifiedtargetspotted", + minDurationBetweenSimilar: 5.0f); } character.AddEncounter(closestEnemy); } @@ -1247,7 +1287,9 @@ namespace Barotrauma.Items.Components } else if (closestEnemy == null && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogIceSpireSpotted"), null, 0.0f, "icespirespotted", 60.0f); + character.Speak(TextManager.Get("DialogIceSpireSpotted"), + identifier: "icespirespotted", + minDurationBetweenSimilar: 60.0f); } character.CursorPosition = targetPos.Value; @@ -1289,7 +1331,9 @@ namespace Barotrauma.Items.Components if (!shoot) { return false; } if (character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); + character.Speak(TextManager.Get("DialogFireTurret"), + identifier: "fireturret", + minDurationBetweenSimilar: 30.0f); } character.SetInput(InputType.Shoot, true, true); } @@ -1389,6 +1433,7 @@ namespace Barotrauma.Items.Components crosshairSprite?.Remove(); crosshairSprite = null; crosshairPointerSprite?.Remove(); crosshairPointerSprite = null; moveSoundChannel?.Dispose(); moveSoundChannel = null; + WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite = null; #endif } @@ -1503,12 +1548,16 @@ namespace Barotrauma.Items.Components IsActive = true; } user = sender; + ActiveUser = sender; + resetActiveUserTimer = 1f; resetUserTimer = 10.0f; break; case "trigger_in": if (signal.value == "0") { return; } item.Use((float)Timing.Step, sender); user = sender; + ActiveUser = sender; + resetActiveUserTimer = 1f; resetUserTimer = 10.0f; //triggering the Use method through item.Use will fail if the item is not characterusable and the signal was sent by a character //so lets do it manually @@ -1521,12 +1570,18 @@ namespace Barotrauma.Items.Components if (lightComponent != null && signal.value != "0") { lightComponent.IsOn = !lightComponent.IsOn; + UpdateLightComponent(); } break; case "set_light": if (lightComponent != null) { - lightComponent.IsOn = signal.value != "0"; + bool shouldBeOn = signal.value != "0"; + if (shouldBeOn != lightComponent.IsOn) + { + lightComponent.IsOn = shouldBeOn; + UpdateLightComponent(); + } } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index c222b6a31..32a31cfe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -802,13 +802,13 @@ namespace Barotrauma if (otherIsEquipped) { - existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent)); - stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent)); + existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true)); + stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true)); } else { - stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent)); - existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent)); + stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true)); + existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true)); } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 2fd8b35e8..bcc30233d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -473,7 +473,12 @@ namespace Barotrauma public float HealthMultiplier { get => healthMultiplier; - set { healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); } + set + { + float prevConditionPercentage = ConditionPercentage; + healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); + Condition = MaxCondition * prevConditionPercentage / 100.0f; + } } private float maxRepairConditionMultiplier = 1.0f; @@ -564,21 +569,26 @@ namespace Barotrauma public bool StolenDuringRound; - private bool spawnedInOutpost; - public bool SpawnedInOutpost + private bool spawnedInCurrentOutpost; + public bool SpawnedInCurrentOutpost { - get { return spawnedInOutpost; } + get { return spawnedInCurrentOutpost; } set { - if (!spawnedInOutpost && value) + if (!spawnedInCurrentOutpost && value) { OriginalOutpost = GameMain.GameSession?.StartLocation?.BaseName ?? ""; } - spawnedInOutpost = value; + spawnedInCurrentOutpost = value; } } - public bool AllowStealing = true; + [Serialize(true, true, alwaysUseInstanceValues: true)] + public bool AllowStealing + { + get; + set; + } private string originalOutpost; [Serialize("", true, alwaysUseInstanceValues: true)] @@ -588,9 +598,9 @@ namespace Barotrauma set { originalOutpost = value; - if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.StartLocation?.BaseName == value) + if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && GameMain.GameSession?.StartLocation?.BaseName == value) { - spawnedInOutpost = true; + spawnedInCurrentOutpost = true; } } } @@ -1736,10 +1746,10 @@ namespace Barotrauma Submarine prevSub = Submarine; var projectile = GetComponent(); - if (projectile?.StickTarget?.UserData is Limb limb) + if (projectile?.StickTarget?.UserData is Limb limb && limb.character != null) { - Submarine = body.Submarine = limb.character?.Submarine; - currentHull = limb.character?.CurrentHull; + Submarine = body.Submarine = limb.character.Submarine; + currentHull = limb.character.CurrentHull; } else { @@ -2125,7 +2135,7 @@ namespace Barotrauma } - private IEnumerable DelaySignal(Signal signal, Connection connection) + private IEnumerable DelaySignal(Signal signal, Connection connection) { do { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index cf1870762..3870e883a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -103,6 +103,9 @@ namespace Barotrauma container.IsActive = true; container.OnItemContained(item); +#if SERVER + GameMain.Server?.KarmaManager?.OnItemContained(item, container.Item, user); +#endif } return wasPut; @@ -122,6 +125,9 @@ namespace Barotrauma container.IsActive = true; container.OnItemContained(item); +#if SERVER + GameMain.Server?.KarmaManager?.OnItemContained(item, container.Item, user); +#endif } return wasPut; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 8b501cd71..5090ae9ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -45,7 +45,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition."); } - RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", new string[0]); + RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", + element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]); RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); @@ -915,7 +916,7 @@ namespace Barotrauma string spriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - spriteFolder = Path.GetDirectoryName(filePath); + spriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); @@ -972,7 +973,7 @@ namespace Barotrauma string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - iconFolder = Path.GetDirectoryName(filePath); + iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true); UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f); @@ -983,7 +984,7 @@ namespace Barotrauma string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - iconFolder = Path.GetDirectoryName(filePath); + iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); } @@ -993,7 +994,7 @@ namespace Barotrauma string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - iconFolder = Path.GetDirectoryName(filePath); + iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); } @@ -1003,7 +1004,7 @@ namespace Barotrauma string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - iconFolder = Path.GetDirectoryName(filePath); + iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); @@ -1014,7 +1015,7 @@ namespace Barotrauma string iconFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - iconFolder = Path.GetDirectoryName(filePath); + iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); @@ -1024,7 +1025,7 @@ namespace Barotrauma string brokenSpriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - brokenSpriteFolder = Path.GetDirectoryName(filePath); + brokenSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } var brokenSprite = new BrokenItemSprite( @@ -1034,7 +1035,7 @@ namespace Barotrauma subElement.GetAttributePoint("offset", Point.Zero)); int spriteIndex = 0; - for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxCondition < brokenSprite.MaxCondition; i++) + for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++) { spriteIndex = i; } @@ -1044,7 +1045,7 @@ namespace Barotrauma string decorativeSpriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - decorativeSpriteFolder = Path.GetDirectoryName(filePath); + decorativeSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } int groupID = 0; @@ -1070,7 +1071,7 @@ namespace Barotrauma string containedSpriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) { - containedSpriteFolder = Path.GetDirectoryName(filePath); + containedSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); } var containedSprite = new ContainedItemSprite(subElement, containedSpriteFolder, lazyLoad: true); if (containedSprite.Sprite != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 47525f812..27bd6bcde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -7,7 +7,7 @@ using System.Text; namespace Barotrauma { - class Entity : ISpatialEntity + abstract class Entity : ISpatialEntity { public const ushort NullEntityID = 0; public const ushort EntitySpawnerID = ushort.MaxValue; @@ -16,8 +16,10 @@ namespace Barotrauma public const ushort ReservedIDStart = ushort.MaxValue - 3; + public const ushort MaxEntityCount = ushort.MaxValue - 2; //ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values + private static Dictionary dictionary = new Dictionary(); - public static IEnumerable GetEntities() + public static IReadOnlyCollection GetEntities() { return dictionary.Values; } @@ -28,11 +30,9 @@ namespace Barotrauma protected AITarget aiTarget; - private bool idFreed; + public bool Removed { get; private set; } - public virtual bool Removed { get; private set; } - - public bool IdFreed => idFreed; + public bool IdFreed { get; private set; } public readonly ushort ID; @@ -75,43 +75,65 @@ namespace Barotrauma this.Submarine = submarine; spawnTime = Timing.TotalTime; - if (id != NullEntityID && dictionary.ContainsKey(id)) - { - throw new Exception($"ID {id} is taken by {dictionary[id]}"); - } - //give a unique ID ID = DetermineID(id, submarine); - + + if (dictionary.ContainsKey(ID)) + { + throw new Exception($"ID {ID} is taken by {dictionary[ID]}"); + } + dictionary.Add(ID, this); } protected virtual ushort DetermineID(ushort id, Submarine submarine) { - return id != NullEntityID ? - id : - FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset); + return id != NullEntityID + ? id + : FindFreeId(submarine == null ? (ushort)1 : submarine.IdOffset); } - public static ushort FindFreeID(ushort idOffset = 0) + private static ushort FindFreeId(ushort idOffset) { - //ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values - if (dictionary.Count >= ushort.MaxValue - 2) + if (dictionary.Count >= MaxEntityCount) { - throw new Exception("Maximum amount of entities (" + (ushort.MaxValue - 1) + ") reached!"); + throw new Exception($"Maximum amount of entities ({MaxEntityCount}) reached!"); } - idOffset = Math.Max(idOffset, (ushort)1); - bool IDfound; ushort id = idOffset; - do + while (id < ReservedIDStart) { - id += 1; - IDfound = dictionary.ContainsKey(id); - } while (IDfound || id == NullEntityID || id > ReservedIDStart); + if (!dictionary.ContainsKey(id)) { break; } + id++; + }; return id; } + /// + /// Finds a contiguous block of free IDs of at least the given size + /// + /// The first ID in the found block, or zero if none are found + public static int FindFreeIdBlock(int minBlockSize) + { + int currentBlockSize = 0; + for (int i = 1; i < ReservedIDStart; i++) + { + if (dictionary.ContainsKey((ushort)i)) + { + currentBlockSize = 0; + } + else + { + currentBlockSize++; + if (currentBlockSize >= minBlockSize) + { + return i - (currentBlockSize-1); + } + } + } + return 0; + } + /// /// Find an entity based on the ID /// @@ -134,11 +156,11 @@ namespace Barotrauma } catch (Exception exception) { - DebugConsole.ThrowError("Error while removing entity \"" + e.ToString() + "\"", exception); + DebugConsole.ThrowError($"Error while removing entity \"{e}\"", exception); GameAnalyticsManager.AddErrorEventOnce( - "Entity.RemoveAll:Exception" + e.ToString(), + $"Entity.RemoveAll:Exception{e}", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace.CleanupStackTrace()); + $"Error while removing entity \"{e} ({exception.Message})\n{exception.StackTrace.CleanupStackTrace()}"); } } StringBuilder errorMsg = new StringBuilder(); @@ -167,7 +189,7 @@ namespace Barotrauma } catch (Exception exception) { - DebugConsole.ThrowError("Error while removing item \"" + item.ToString() + "\"", exception); + DebugConsole.ThrowError($"Error while removing item \"{item}\"", exception); } } Item.ItemList.Clear(); @@ -189,7 +211,7 @@ namespace Barotrauma } catch (Exception exception) { - DebugConsole.ThrowError("Error while removing character \"" + character.ToString() + "\"", exception); + DebugConsole.ThrowError($"Error while removing character \"{character}\"", exception); } } Character.CharacterList.Clear(); @@ -214,35 +236,33 @@ namespace Barotrauma /// public void FreeID() { - DebugConsole.Log("Removing entity " + ToString() + " (" + ID + ") from entity dictionary."); + if (IdFreed) { return; } + DebugConsole.Log($"Removing entity {ToString()} ({ID}) from entity dictionary."); if (!dictionary.TryGetValue(ID, out Entity existingEntity)) { - DebugConsole.Log("Entity " + ToString() + " (" + ID + ") not present in entity dictionary."); + DebugConsole.ThrowError($"Entity {ToString()} ({ID}) not present in entity dictionary."); GameAnalyticsManager.AddErrorEventOnce( - "Entity.FreeID:EntityNotFound" + ID, + $"Entity.FreeID:EntityNotFound{ID}", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace.CleanupStackTrace()); + $"Entity {ToString()} ({ID}) not present in entity dictionary.\n{Environment.StackTrace.CleanupStackTrace()}"); } else if (existingEntity != this) { - DebugConsole.Log("Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")"); + DebugConsole.ThrowError($"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})"); GameAnalyticsManager.AddErrorEventOnce("Entity.FreeID:EntityMismatch" + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Entity ID mismatch in entity dictionary. Entity " + existingEntity + " had the ID " + ID + " (expecting " + ToString() + ")"); - - foreach (var keyValuePair in dictionary.Where(kvp => kvp.Value == this).ToList()) - { - dictionary.Remove(keyValuePair.Key); - } + $"Entity ID mismatch in entity dictionary. Entity {existingEntity} had the ID {ID} (expecting {ToString()})"); } - - dictionary.Remove(ID); - idFreed = true; + else + { + dictionary.Remove(ID); + } + IdFreed = true; } public virtual void Remove() { - if (!idFreed) FreeID(); + FreeID(); Removed = true; } @@ -255,8 +275,8 @@ namespace Barotrauma List lines = new List(); for (int i = 0; i < count; i++) { - lines.Add(entities[i].ID + ": " + entities[i].ToString()); - DebugConsole.ThrowError(entities[i].ID + ": " + entities[i].ToString()); + lines.Add($"{entities[i].ID}: {entities[i]}"); + DebugConsole.ThrowError($"{entities[i].ID}: {entities[i]}"); } if (!string.IsNullOrWhiteSpace(filename)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 277b55925..e91c25f7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -39,6 +39,8 @@ namespace Barotrauma private readonly float itemRepairStrength; + public readonly HashSet IgnoredSubmarines = new HashSet(); + public float EmpStrength { get; set; } public float BallastFloraDamage { get; set; } @@ -149,7 +151,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f)) { - RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker); + RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines); } if (BallastFloraDamage > 0.0f) @@ -397,13 +399,14 @@ namespace Barotrauma /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// - public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null) + public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null) { List structureList = new List(); float dist = 600.0f; foreach (MapEntity entity in MapEntity.mapEntityList) { if (!(entity is Structure structure)) { continue; } + if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; } if (structure.HasBody && !structure.IsPlatform && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index b662b9c00..9d2873a8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -26,25 +26,25 @@ namespace Barotrauma } //a value between 0.0f-1.0f (0.0 = closed, 1.0f = open) - private float open; + private float open; //the force of the water flow which is exerted on physics bodies private Vector2 flowForce; private Hull flowTargetHull; private float openedTimer = 1.0f; - + private float higherSurface; private float lowerSurface; - + private Vector2 lerpedFlowForce; - + //if set to true, hull connections of this gap won't be updated when changes are being done to hulls public bool DisableHullRechecks; - + //can ambient light get through the gap even if it's not open public bool PassAmbientLight; - + //a collider outside the gap (for example an ice wall next to the sub) //used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub @@ -54,11 +54,11 @@ namespace Barotrauma public float Open { get { return open; } - set + set { if (float.IsNaN(value)) { return; } if (value > open) { openedTimer = 1.0f; } - open = MathHelper.Clamp(value, 0.0f, 1.0f); + open = MathHelper.Clamp(value, 0.0f, 1.0f); } } @@ -319,7 +319,7 @@ namespace Barotrauma if (openedTimer > 0.0f && flowForce.Length() > lerpedFlowForce.Length()) { //if the gap has just been opened/created, allow it to exert a large force instantly without any smoothing - lerpedFlowForce = flowForce; + lerpedFlowForce = flowForce; } else { @@ -375,7 +375,7 @@ namespace Barotrauma //make sure not to move more than what the room contains delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume)); - + //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume)); hull1.WaterVolume += delta; @@ -405,7 +405,7 @@ namespace Barotrauma { hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2); } - + flowForce = new Vector2(delta, 0.0f); } @@ -448,7 +448,7 @@ namespace Barotrauma hull2.WaterVolume -= delta; flowForce = new Vector2( - 0.0f, + 0.0f, Math.Min(Math.Min((hull2.Pressure + subOffset.Y) - hull1.Pressure, 200.0f), delta)); flowTargetHull = hull1; @@ -456,7 +456,7 @@ namespace Barotrauma if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + (hull2.Pressure + subOffset.Y)) / 2); - } + } } //there's water in the upper room, drop to lower @@ -494,7 +494,7 @@ namespace Barotrauma hull1.LethalPressure = avgLethality; hull2.LethalPressure = avgLethality; } - else + else { hull1.LethalPressure = 0.0f; hull2.LethalPressure = 0.0f; @@ -511,7 +511,7 @@ namespace Barotrauma float sizeModifier = size * open * open; float delta = 500.0f * sizeModifier * deltaTime; - + //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; @@ -526,7 +526,7 @@ namespace Barotrauma if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { flowForce = new Vector2(-delta, 0.0f); - + } else { @@ -554,7 +554,7 @@ namespace Barotrauma hull1.WaveVel[0] += vel; hull1.WaveVel[1] += vel; - } + } } else { @@ -651,12 +651,12 @@ namespace Barotrauma float totalOxygen = hull1.Oxygen + hull2.Oxygen; float totalVolume = (hull1.Volume + hull2.Volume); - + float deltaOxygen = (totalOxygen * hull1.Volume / totalVolume) - hull1.Oxygen; deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed, Hull.OxygenDistributionSpeed); hull1.Oxygen += deltaOxygen; - hull2.Oxygen -= deltaOxygen; + hull2.Oxygen -= deltaOxygen; } public static Gap FindAdjacent(IEnumerable gaps, Vector2 worldPos, float allowedOrthogonalDist) @@ -724,7 +724,7 @@ namespace Barotrauma { if (!DisableHullRechecks) FindHulls(); } - + public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = Rectangle.Empty; @@ -755,6 +755,8 @@ namespace Barotrauma { linkedToID = new List(), }; + + g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame).ToLower(), g.HiddenInGame); return g; } @@ -764,7 +766,8 @@ namespace Barotrauma element.Add( new XAttribute("ID", ID), - new XAttribute("horizontal", IsHorizontal ? "true" : "false")); + new XAttribute("horizontal", IsHorizontal ? "true" : "false"), + new XAttribute(nameof(HiddenInGame).ToLower(), HiddenInGame)); element.Add(new XAttribute("rect", (int)(rect.X - Submarine.HiddenSubPosition.X) + "," + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 47093bcf6..4268507a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1058,11 +1058,17 @@ namespace Barotrauma /// Does being exactly at the edge of the hull count as being inside? public static Hull FindHull(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true) { - if (EntityGrids == null) return null; + if (EntityGrids == null) + { + return null; + } if (guess != null) { - if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess; + if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) + { + return guess; + } } foreach (EntityGrid entityGrid in EntityGrids) @@ -1088,15 +1094,19 @@ namespace Barotrauma continue; } } - Vector2 transformedPosition = position; - if (useWorldCoordinates && entityGrid.Submarine != null) transformedPosition -= entityGrid.Submarine.Position; - + if (useWorldCoordinates && entityGrid.Submarine != null) + { + transformedPosition -= entityGrid.Submarine.Position; + } var entities = entityGrid.GetEntities(transformedPosition); - if (entities == null) continue; + if (entities == null) { continue; } foreach (Hull hull in entities) { - if (Submarine.RectContains(hull.rect, transformedPosition, inclusive)) return hull; + if (Submarine.RectContains(hull.rect, transformedPosition, inclusive)) + { + return hull; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 21f78b9af..5793f5b9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -145,8 +145,7 @@ namespace Barotrauma public static List PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false) { - int idOffset = Entity.FindFreeID(1); - if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); } + int idOffset = Entity.FindFreeIdBlock(configElement.Elements().Count()); List entities = MapEntity.LoadAll(sub, configElement, filePath, idOffset); if (entities.Count == 0) { return entities; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 698b5fbde..81ba3d49b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -202,10 +202,10 @@ namespace Barotrauma get { return startPosition.ToVector2(); } } - private Vector2 startExitPosition; + private Point startExitPosition; public Vector2 StartExitPosition { - get { return startExitPosition; } + get { return startExitPosition.ToVector2(); } } public Point Size @@ -218,10 +218,10 @@ namespace Barotrauma get { return endPosition.ToVector2(); } } - private Vector2 endExitPosition; + private Point endExitPosition; public Vector2 EndExitPosition { - get { return endExitPosition; } + get { return endExitPosition.ToVector2(); } } public int BottomPos @@ -366,9 +366,6 @@ namespace Barotrauma { this.LevelData = levelData; borders = new Rectangle(Point.Zero, levelData.Size); - - //remove from entity dictionary - //base.Remove(); } public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) @@ -462,12 +459,12 @@ namespace Barotrauma startPosition = new Point( (int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.StartPosition.X), (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.StartPosition.Y)); - startExitPosition = new Vector2(startPosition.X, borders.Bottom); + startExitPosition = new Point(startPosition.X, borders.Bottom); endPosition = new Point( (int)MathHelper.Lerp(minMainPathWidth, borders.Width - minMainPathWidth, GenerationParams.EndPosition.X), (int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y)); - endExitPosition = new Vector2(endPosition.X, borders.Bottom); + endExitPosition = new Point(endPosition.X, borders.Bottom); EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -486,25 +483,25 @@ namespace Barotrauma { startPath = new Tunnel( TunnelType.SidePath, - new List() { startExitPosition.ToPoint(), startPosition }, + new List() { startExitPosition, startPosition }, minWidth / 2, parentTunnel: mainPath); Tunnels.Add(startPath); } else { - startExitPosition = StartPosition; + startExitPosition = startPosition; } if (GenerationParams.EndPosition.Y < 0.5f && (Mirrored ? !HasStartOutpost() : !HasEndOutpost())) { endPath = new Tunnel( TunnelType.SidePath, - new List() { endPosition, endExitPosition.ToPoint() }, + new List() { endPosition, endExitPosition }, minWidth / 2, parentTunnel: mainPath); Tunnels.Add(endPath); } else { - endExitPosition = EndPosition; + endExitPosition = endPosition; } if (GenerationParams.CreateHoleNextToEnd) @@ -513,14 +510,14 @@ namespace Barotrauma { endHole = new Tunnel( TunnelType.SidePath, - new List() { startPosition, startExitPosition.ToPoint(), new Point(0, Size.Y) }, + new List() { startPosition, startExitPosition, new Point(0, Size.Y) }, minWidth / 2, parentTunnel: mainPath); } else { endHole = new Tunnel( TunnelType.SidePath, - new List() { endPosition, endExitPosition.ToPoint(), Size }, + new List() { endPosition, endExitPosition, Size }, minWidth / 2, parentTunnel: mainPath); } Tunnels.Add(endHole); @@ -799,8 +796,12 @@ namespace Barotrauma for (int i = 0; i < GenerationParams.RuinCount; i++) { Point ruinSize = new Point(5000); - ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, - limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize))); + int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2); + int limitRight = Math.Min(endPosition.X, Size.X - ruinSize.X / 2); + Rectangle limits = new Rectangle(limitLeft, ruinSize.Y, limitRight - limitLeft, Size.Y - ruinSize.Y); + Debug.Assert(limits.Width > 0); + Debug.Assert(limits.Height > 0); + ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, limits: limits)); CalculateTunnelDistanceField(ruinPositions); } @@ -1004,7 +1005,7 @@ namespace Barotrauma { if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; } if (pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) { continue; } - if (Math.Abs(pos.Position.X - StartPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - EndPosition.X) < minMainPathWidth * 2) { continue; } + if (Math.Abs(pos.Position.X - startPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - endPosition.X) < minMainPathWidth * 2) { continue; } if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) { continue; } iceChunkPositions.Add(pos.Position); } @@ -1187,19 +1188,19 @@ namespace Barotrauma startPosition = endPosition; endPosition = tempP; - Vector2 tempV = startExitPosition; + tempP = startExitPosition; startExitPosition = endExitPosition; - endExitPosition = tempV; + endExitPosition = tempP; } if (StartOutpost != null) { - startExitPosition = StartOutpost.WorldPosition; - startPosition = startExitPosition.ToPoint(); + startExitPosition = StartOutpost.WorldPosition.ToPoint(); + startPosition = startExitPosition; } if (EndOutpost != null) { - endExitPosition = EndOutpost.WorldPosition; - endPosition = endExitPosition.ToPoint(); + endExitPosition = EndOutpost.WorldPosition.ToPoint(); + endPosition = endExitPosition; } CreateWrecks(); @@ -2321,7 +2322,7 @@ namespace Barotrauma { if (l.Cell == null || l.Edge == null) { return false; } if (resourceInfo.IsIslandSpecifc && !l.Cell.Island) { return false; } - if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > StartPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; } + if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; } if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; } return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _); @@ -2738,10 +2739,26 @@ namespace Barotrauma allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter) .CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter))); var maxResourceOverlap = 0.4f; - // TODO: Find multiple locations if there's too many resources to fit on a sigle edge var selectedLocation = allValidLocations.FirstOrDefault(l => Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is float edgeLength && requiredAmount <= (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.Size.X))); + if (selectedLocation.Edge == null) + { + //couldn't find a long enough edge, find the largest one + float longestEdge = 0.0f; + foreach (var validLocation in allValidLocations) + { + if (Vector2.Distance(validLocation.Edge.Point1, validLocation.Edge.Point2) is float edgeLength && edgeLength > longestEdge) + { + selectedLocation = validLocation; + longestEdge = edgeLength; + } + } + } + if (selectedLocation.Edge == null) + { + throw new Exception("Failed to find a suitable level wall edge to place level resources on."); + } PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources); var edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell); rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); @@ -3197,12 +3214,12 @@ namespace Barotrauma public bool IsCloseToStart(Point position, float minDist) { - return MathUtils.LineSegmentToPointDistanceSquared(StartPosition.ToPoint(), StartExitPosition.ToPoint(), position) < minDist * minDist; + return MathUtils.LineSegmentToPointDistanceSquared(startPosition, startExitPosition, position) < minDist * minDist; } public bool IsCloseToEnd(Point position, float minDist) { - return MathUtils.LineSegmentToPointDistanceSquared(EndPosition.ToPoint(), EndExitPosition.ToPoint(), position) < minDist * minDist; + return MathUtils.LineSegmentToPointDistanceSquared(endPosition, endExitPosition, position) < minDist * minDist; } private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type) @@ -3214,6 +3231,7 @@ namespace Barotrauma var waypoints = WayPoint.WayPointList.Where(wp => wp.Submarine == null && wp.SpawnType == SpawnType.Path && + wp.WorldPosition.X < EndExitPosition.X && !IsCloseToStart(wp.WorldPosition, minDistance) && !IsCloseToEnd(wp.WorldPosition, minDistance)).ToList(); @@ -3971,7 +3989,7 @@ namespace Barotrauma var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server); var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); - corpse.AnimController.FindHull(worldPos, true); + corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; selectedPrefab.GiveItems(corpse, wreck); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 2cf68b72c..eb59a650b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -143,14 +143,20 @@ namespace Barotrauma var rand = new MTRandom(ToolBox.StringToInt(Seed)); InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble()); - - //minimum difficulty of the level before hunting grounds can appear - float huntingGroundsDifficultyThreshold = 25; - //probability of hunting grounds appearing in 100% difficulty levels - float maxHuntingGroundsProbability = 0.3f; - HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability; - - HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); + if (Biome.IsEndBiome) + { + HasHuntingGrounds = false; + HasBeaconStation = false; + } + else + { + //minimum difficulty of the level before hunting grounds can appear + float huntingGroundsDifficultyThreshold = 25; + //probability of hunting grounds appearing in 100% difficulty levels + float maxHuntingGroundsProbability = 0.3f; + HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability; + HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); + } IsBeaconActive = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index be359698a..64dc5acb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -35,9 +35,6 @@ namespace Barotrauma.RuinGeneration private static List paramsList; private readonly string filePath; - - public override string Name => "RuinGenerationParams"; - private RuinGenerationParams(XElement element, string filePath) : base(element, filePath) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 6b2fdba38..ef4e0c1cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -1,7 +1,7 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; -using System.Linq; using Voronoi2; namespace Barotrauma.RuinGeneration @@ -41,6 +41,9 @@ namespace Barotrauma.RuinGeneration Submarine.Info.Name = $"Ruin ({level.Seed})"; Submarine.Info.Type = SubmarineType.Ruin; Submarine.TeamID = CharacterTeamType.None; + + //prevent the ruin from extending above the level "ceiling" + position.Y = Math.Min(level.Size.Y - (Submarine.Borders.Height / 2) - 100, position.Y); Submarine.SetPosition(position.ToVector2()); if (mirror) @@ -52,9 +55,9 @@ namespace Barotrauma.RuinGeneration worldBorders.Location += Submarine.WorldPosition.ToPoint(); Area = new Rectangle(worldBorders.X, worldBorders.Y - worldBorders.Height, worldBorders.Width, worldBorders.Height); - List subWaypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine); + var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Ruin == this || wp.Submarine == Submarine); int interestingPosCount = 0; - foreach (WayPoint wp in subWaypoints) + foreach (WayPoint wp in waypoints) { if (wp.SpawnType != SpawnType.Enemy) { continue; } level.PositionsOfInterest.Add(new Level.InterestingPosition(wp.WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); @@ -63,8 +66,8 @@ namespace Barotrauma.RuinGeneration if (interestingPosCount == 0) { - //make sure there's at least on PositionsOfInterest in the ruins - level.PositionsOfInterest.Add(new Level.InterestingPosition(subWaypoints.GetRandom(Rand.RandSync.Server).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); + //make sure there's at least one PositionsOfInterest in the ruins + level.PositionsOfInterest.Add(new Level.InterestingPosition(waypoints.GetRandom(Rand.RandSync.Server).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index afc15291e..771e43965 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -175,9 +175,10 @@ namespace Barotrauma { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); LinkedSubmarine linkedSub; + idRemap.AssignMaxId(out ushort id); if (Screen.Selected == GameMain.SubEditorScreen) { - linkedSub = CreateDummy(submarine, element, pos, idRemap.AssignMaxId()); + linkedSub = CreateDummy(submarine, element, pos, id); linkedSub.saveElement = element; linkedSub.purchasedLostShuttles = false; } @@ -185,7 +186,7 @@ namespace Barotrauma { string levelSeed = element.GetAttributeString("location", ""); LevelData levelData = GameMain.GameSession?.Campaign?.NextLevel ?? GameMain.GameSession?.LevelData; - linkedSub = new LinkedSubmarine(submarine, idRemap.AssignMaxId()) + linkedSub = new LinkedSubmarine(submarine, id) { purchasedLostShuttles = GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles, saveElement = element diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 6c208d35b..7a8e2f397 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using StoreBalanceStatus = Barotrauma.LocationType.StoreBalanceStatus; namespace Barotrauma { @@ -87,16 +88,12 @@ namespace Barotrauma public int TurnsInRadiation { get; set; } #region Store - - private const float StoreMaxReputationModifier = 0.1f; - private const float StoreSellPriceModifier = 0.8f; - private const float DailySpecialPriceModifier = 0.5f; - private const float RequestGoodPriceModifier = 1.5f; - public const int StoreInitialBalance = 5000; - /// - /// In percentages - /// - private const int StorePriceModifierRange = 5; + private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier; + private float StoreSellPriceModifier => Type.StoreSellPriceModifier; + private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier; + private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier; + public int StoreInitialBalance => Type.StoreInitialBalance; + private int StorePriceModifierRange => Type.StorePriceModifierRange; /// /// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa. /// @@ -104,26 +101,7 @@ namespace Barotrauma public Color BalanceColor => ActiveStoreBalanceStatus.Color; public StoreBalanceStatus ActiveStoreBalanceStatus { get; private set; } - private static StoreBalanceStatus DefaultBalanceStatus { get; } = new StoreBalanceStatus(1.0f, 1.0f, Color.White); - private static List StoreBalanceStatuses { get; } = new List - { - new StoreBalanceStatus(0.5f, 0.75f, Color.Orange), - new StoreBalanceStatus(0.25f, 0.2f, Color.Red), - }; - - public struct StoreBalanceStatus - { - public float PercentageOfInitialBalance { get; } - public float SellPriceModifier { get; } - public Color Color { get; } - - public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color) - { - PercentageOfInitialBalance = percentage; - SellPriceModifier = sellPriceModifier; - Color = color; - } - } + private List StoreBalanceStatuses => Type.StoreBalanceStatuses; private int storeCurrentBalance; public int StoreCurrentBalance @@ -1111,15 +1089,16 @@ namespace Barotrauma } } - public static StoreBalanceStatus GetStoreBalanceStatus(int balance) + public StoreBalanceStatus GetStoreBalanceStatus(int balance) { - StoreBalanceStatus nextStatus = DefaultBalanceStatus; - foreach (var balanceStatus in StoreBalanceStatuses) + StoreBalanceStatus nextStatus = StoreBalanceStatuses[0]; + for (int i = 1; i < StoreBalanceStatuses.Count; i++) { - if (balanceStatus.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance && - ((float)balance / StoreInitialBalance) < balanceStatus.PercentageOfInitialBalance) + var status = StoreBalanceStatuses[i]; + if (status.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance && + ((float)balance / StoreInitialBalance) < status.PercentageOfInitialBalance) { - nextStatus = balanceStatus; + nextStatus = status; } } return nextStatus; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 88fe4d6f4..792cce9bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -68,6 +68,37 @@ namespace Barotrauma private set; } + public float StoreMaxReputationModifier { get; } = 0.1f; + public float StoreSellPriceModifier { get; } = 0.8f; + public float DailySpecialPriceModifier { get; } = 0.5f; + public float RequestGoodPriceModifier { get; } = 1.5f; + public int StoreInitialBalance { get; } = 5000; + /// + /// In percentages + /// + public int StorePriceModifierRange { get; } = 5; + + public List StoreBalanceStatuses { get; } = new List() + { + new StoreBalanceStatus(1.0f, 1.0f, Color.White), + new StoreBalanceStatus(0.5f, 0.75f, Color.Orange), + new StoreBalanceStatus(0.25f, 0.2f, Color.Red) + }; + + public struct StoreBalanceStatus + { + public float PercentageOfInitialBalance { get; } + public float SellPriceModifier { get; } + public Color Color { get; } + + public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color) + { + PercentageOfInitialBalance = percentage; + SellPriceModifier = sellPriceModifier; + Color = color; + } + } + public override string ToString() { return $"LocationType (" + Identifier + ")"; @@ -163,6 +194,26 @@ namespace Barotrauma portraits.Add(portrait); } break; + case "store": + StoreMaxReputationModifier = subElement.GetAttributeFloat("maxreputationmodifier", StoreMaxReputationModifier); + StoreSellPriceModifier = subElement.GetAttributeFloat("sellpricemodifier", StoreSellPriceModifier); + DailySpecialPriceModifier = subElement.GetAttributeFloat("dailyspecialpricemodifier", DailySpecialPriceModifier); + RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier); + StoreInitialBalance = subElement.GetAttributeInt("initialbalance", StoreInitialBalance); + StorePriceModifierRange = subElement.GetAttributeInt("pricemodifierrange", StorePriceModifierRange); + var balanceStatusElements = subElement.GetChildElements("balancestatus"); + if (balanceStatusElements.Any()) + { + StoreBalanceStatuses.Clear(); + foreach (var balanceStatusElement in balanceStatusElements) + { + float percentage = balanceStatusElement.GetAttributeFloat("percentage", 1.0f); + float modifier = balanceStatusElement.GetAttributeFloat("sellpricemodifier", 1.0f); + Color color = balanceStatusElement.GetAttributeColor("color", Color.White); + StoreBalanceStatuses.Add(new StoreBalanceStatus(percentage, modifier, color)); + } + } + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 53342d1aa..7609cf623 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -472,8 +472,11 @@ namespace Barotrauma foreach (LocationConnection connection in Connections) { - float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width); - connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f); + //float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width); + //connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f); + float difficulty = connection.CenterPos.X / Width * 100; + float random = difficulty > 10 ? 5 : 0; + connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.Server), 1.0f, 100.0f); } AssignBiomes(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index 766ef959e..9fd7a488e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma @@ -22,6 +23,8 @@ namespace Barotrauma public readonly Map Map; public readonly RadiationParams Params; + private Affliction radiationAffliction; + private float radiationTimer; private float increasedAmount; @@ -93,6 +96,8 @@ namespace Barotrauma increasedAmount = lastIncrease = amount; } + + public void UpdateRadiation(float deltaTime) { if (!(GameMain.GameSession?.IsCurrentLocationRadiated() ?? false)) { return; } @@ -105,6 +110,8 @@ namespace Barotrauma return; } + radiationAffliction ??= new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount); + radiationTimer = Params.RadiationDamageDelay; foreach (Character character in Character.CharacterList) @@ -113,7 +120,11 @@ namespace Barotrauma if (IsEntityRadiated(character)) { - health.ApplyAffliction(null, new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount)); + foreach (Limb limb in character.AnimController.Limbs) + { + AttackResult attackResult = limb.AddDamage(limb.SimPosition, radiationAffliction.ToEnumerable(), playSound: false); + character.CharacterHealth.ApplyDamage(limb, attackResult); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 3b0dfa429..39b5f6bf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -262,7 +262,7 @@ namespace Barotrauma item.GetComponent()?.InitializeLinks(); item.GetComponent()?.OnMapLoaded(); } - idOffset = moduleEntities.Max(e => e.ID); + idOffset = moduleEntities.Max(e => e.ID) + 1; var wallEntities = moduleEntities.Where(e => e is Structure).Cast(); var hullEntities = moduleEntities.Where(e => e is Hull).Cast(); @@ -1483,7 +1483,7 @@ namespace Barotrauma } characterInfo.TeamID = CharacterTeamType.FriendlyNPC; var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); - npc.AnimController.FindHull(gotoTarget.WorldPosition, true); + npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; npc.Prefab = humanPrefab; if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) @@ -1503,7 +1503,7 @@ namespace Barotrauma foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; - item.SpawnedInOutpost = true; + item.SpawnedInCurrentOutpost = true; } npc.GiveIdCardTags(gotoTarget as WayPoint); humanPrefab.InitializeCharacter(npc, gotoTarget); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs index 0964e41bc..ae5e402d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs @@ -44,7 +44,7 @@ namespace Barotrauma #endif } - private IEnumerable Update(List subs, Camera cam) + private IEnumerable Update(List subs, Camera cam) { if (!subs.Any()) yield return CoroutineStatus.Success; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 3273bb4c3..7084f3ada 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -274,14 +274,6 @@ namespace Barotrauma return "Barotrauma.Submarine (" + (Info?.Name ?? "[NULL INFO]") + ", " + IdOffset + ")"; } - public override bool Removed - { - get - { - return !loaded.Contains(this); - } - } - public int CalculateBasePrice() { int minPrice = 1000; @@ -1126,22 +1118,6 @@ namespace Barotrauma } } - /// - /// Run the power logic so the sub is already powered up at the start of the round (as long as the reactor was on) - /// - public void WarmStartPower() - { - for (int i = 0; i < 600; i++) - { - Powered.UpdatePower((float)Timing.Step); - foreach (Entity e in Item.ItemList) - { - if (!(e is Item item) || item.GetComponent() == null || e.Submarine != this) { continue; } - item.Update((float)Timing.Step, GameMain.GameScreen.Cam); - } - } - } - public void SetPrevTransform(Vector2 position) { prevPosition = position; @@ -1398,7 +1374,7 @@ namespace Barotrauma if (me.Submarine != this) { continue; } if (me is Item item) { - item.SpawnedInOutpost = info.OutpostGenerationParams != null; + item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null; item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true; if (item.GetComponent() != null && indestructible) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 49bfbf5f6..18443c725 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -575,7 +575,7 @@ namespace Barotrauma if (newHull != null) { CoroutineManager.Invoke(() => - character.AnimController.FindHull(newHull.WorldPosition, true)); + character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true)); } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index de504a73e..e5f56fdbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -194,7 +194,7 @@ namespace Barotrauma float minDist = 100.0f; float heightFromFloor = 110.0f; float hullMinHeight = 100; - var removals = new List(); + var removals = new HashSet(); foreach (Hull hull in Hull.hullList) { if (isFlooded) @@ -492,17 +492,18 @@ namespace Barotrauma { outsideWaypoints.RemoveAll(w => w.Item1 == wp); } + removals.ForEach(wp => wp.Remove()); for (int i = 0; i < outsideWaypoints.Count; i++) { WayPoint current = outsideWaypoints[i].Item1; - if (current.linkedTo.Count > 1) { continue; } + if (current.linkedTo.Count(l => !removals.Contains(l)) > 1) { continue; } WayPoint next = null; int maxConnections = 2; float tooFar = outSideWaypointInterval * 5; for (int j = 0; j < maxConnections; j++) { if (current.linkedTo.Count >= maxConnections) { break; } - tooFar /= current.linkedTo.Count; + tooFar /= current.linkedTo.Count(l => !removals.Contains(l)); next = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp.Item1 != next && wp.Item1.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.Item1.linkedTo.Count < 2 && wp.Item2 < i); if (next != null) { @@ -511,18 +512,19 @@ namespace Barotrauma } } } - foreach (Structure wall in Structure.WallList) + foreach (MapEntity mapEntity in mapEntityList.ToList()) { - if (wall.StairDirection == Direction.None) { continue; } + if (!(mapEntity is Structure structure)) { continue; } + if (structure.StairDirection == Direction.None) { continue; } WayPoint[] stairPoints = new WayPoint[3]; stairPoints[0] = new WayPoint( - new Vector2(wall.Rect.X - 32.0f, - wall.Rect.Y - (wall.StairDirection == Direction.Left ? 80 : wall.Rect.Height) + heightFromFloor), SpawnType.Path, submarine); + new Vector2(structure.Rect.X - 32.0f, + structure.Rect.Y - (structure.StairDirection == Direction.Left ? 80 : structure.Rect.Height) + heightFromFloor), SpawnType.Path, submarine); stairPoints[1] = new WayPoint( - new Vector2(wall.Rect.Right + 32.0f, - wall.Rect.Y - (wall.StairDirection == Direction.Left ? wall.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine); + new Vector2(structure.Rect.Right + 32.0f, + structure.Rect.Y - (structure.StairDirection == Direction.Left ? structure.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine); for (int i = 0; i < 2; i++) { @@ -869,10 +871,10 @@ namespace Barotrauma } } - public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false, string spawnPointTag = null) + public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false, string spawnPointTag = null, bool ignoreSubmarine = false) { return WayPointList.GetRandom(wp => - wp.Submarine == sub && + (ignoreSubmarine || wp.Submarine == sub) && wp.spawnType == spawnType && (string.IsNullOrEmpty(spawnPointTag) || wp.Tags.Any(t => t.Equals(spawnPointTag, StringComparison.OrdinalIgnoreCase))) && (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index 38a6c5f5e..00dea4a4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -105,6 +105,15 @@ namespace Barotrauma [Serialize(120.0f, true)] public float AllowedRetaliationTime { get; set; } + [Serialize(5.0f, true)] + public float DangerousItemContainKarmaDecrease { get; set; } + + [Serialize(defaultValue: true, true)] + public bool IsDangerousItemContainKarmaDecreaseIncremental { get; set; } + + [Serialize(30.0f, true)] + public float MaxDangerousItemContainKarmaDecrease { get; set; } + private readonly AfflictionPrefab herpesAffliction; public Dictionary Presets = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index 4a5441b57..4ab3a4c33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -27,7 +27,6 @@ namespace Barotrauma.Networking { WriteEvent(tempEventBuffer, e, recipient); } - catch (Exception exception) { DebugConsole.ThrowError("Failed to write an event for the entity \"" + e.Entity + "\"", exception); @@ -55,7 +54,7 @@ namespace Barotrauma.Networking tempBuffer.WriteVariableUInt32((uint)tempEventBuffer.LengthBytes); tempBuffer.Write(tempEventBuffer.Buffer, 0, tempEventBuffer.LengthBytes); tempBuffer.WritePadBits(); - sentEvents.Add(e); + sentEvents.Add(e); eventCount++; } @@ -68,6 +67,32 @@ namespace Barotrauma.Networking } } + protected static bool ValidateEntity(INetSerializable entity) + { + void error(string reason) + => DebugConsole.ThrowError($"Can't create an entity event for {entity} - {reason}.\n{Environment.StackTrace.CleanupStackTrace()}"); + + if (entity is Entity { Removed: var removed, IdFreed: var idFreed }) + { + if (removed) + { + error("the entity has been removed"); + return false; + } + if (idFreed) + { + error("the ID of the entity has been freed"); + return false; + } + } + else + { + error($"input is not of type {nameof(Entity)}"); + return false; + } + return true; + } + protected abstract void WriteEvent(IWriteMessage buffer, NetEntityEvent entityEvent, Client recipient = null); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index e4d53fc9c..8f73cfc90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -196,7 +196,7 @@ namespace Barotrauma.Networking partial void UpdateReturningProjSpecific(float deltaTime); - private IEnumerable ForceShuttleToPos(Vector2 position, float speed) + private IEnumerable ForceShuttleToPos(Vector2 position, float speed) { if (RespawnShuttle == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index d3d364a3b..8d27d37ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -42,6 +42,7 @@ namespace Barotrauma.Networking ServerMessage, ConsoleUsage, Karma, + Talent, Error, } @@ -56,6 +57,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, new Color(157, 225, 160) }, { MessageType.ConsoleUsage, new Color(0, 162, 232) }, { MessageType.Karma, new Color(75, 88, 255) }, + { MessageType.Talent, new Color(125, 125, 255) }, { MessageType.Error, Color.Red }, }; @@ -70,6 +72,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, "ServerMessage" }, { MessageType.ConsoleUsage, "ConsoleUsage" }, { MessageType.Karma, "Karma" }, + { MessageType.Talent, "Talent" }, { MessageType.Error, "Error" } }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 4d335dc73..304996d66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using System.Text; using System.Xml; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -48,7 +49,8 @@ namespace Barotrauma.Networking Message = 0x2, Properties = 0x4, Misc = 0x8, - LevelSeed = 0x10 + LevelSeed = 0x10, + HiddenSubs = 0x20 } public static readonly string PermissionPresetFile = "Data" + Path.DirectorySeparatorChar + "permissionpresets.xml"; @@ -284,6 +286,8 @@ namespace Barotrauma.Networking ExtraCargo = new Dictionary(); + HiddenSubs = new HashSet(); + PermissionPreset.LoadAll(PermissionPresetFile); InitProjSpecific(); @@ -369,6 +373,8 @@ namespace Barotrauma.Networking public Dictionary ExtraCargo { get; private set; } + public HashSet HiddenSubs { get; private set; } + private float selectedLevelDifficulty; private string password; @@ -506,6 +512,13 @@ namespace Barotrauma.Networking } } + [Serialize(Barotrauma.LosMode.Opaque, true)] + public LosMode LosMode + { + get; + set; + } + [Serialize(800, true)] public int LinesPerLogFile { @@ -1023,5 +1036,29 @@ namespace Barotrauma.Networking msg.Write((byte)kvp.Value); } } + + public void ReadHiddenSubs(IReadMessage msg) + { + HiddenSubs.Clear(); + uint count = msg.ReadVariableUInt32(); + for (int i = 0; i < count; i++) + { + string submarineName = msg.ReadString(); + HiddenSubs.Add(submarineName); + } + +#if SERVER + SelectNonHiddenSubmarine(); +#endif + } + + public void WriteHiddenSubs(IWriteMessage msg) + { + msg.WriteVariableUInt32((uint)HiddenSubs.Count); + foreach (string submarineName in HiddenSubs) + { + msg.Write(submarineName); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 709866603..ecab1d04a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -15,23 +16,22 @@ namespace Barotrauma public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 }; - private List> GetVoteList(VoteType voteType, List voters) + private IReadOnlyDictionary GetVoteCounts(VoteType voteType, List voters) { - List> voteList = new List>(); + Dictionary voteList = new Dictionary(); foreach (Client voter in voters) { - object vote = voter.GetVote(voteType); + T vote = voter.GetVote(voteType); if (vote == null) continue; - var existingVotable = voteList.Find(v => v.First == vote || v.First.Equals(vote)); - if (existingVotable == null) + if (!voteList.ContainsKey(vote)) { - voteList.Add(new Pair(vote, 1)); + voteList.Add(vote, 1); } else { - existingVotable.Second++; + voteList[vote]++; } } return voteList; @@ -42,16 +42,23 @@ namespace Barotrauma if (voteType == VoteType.Sub && !AllowSubVoting) return default(T); if (voteType == VoteType.Mode && !AllowModeVoting) return default(T); - List> voteList = GetVoteList(voteType, voters); + IReadOnlyDictionary voteList = GetVoteCounts(voteType, voters); T selected = default(T); int highestVotes = 0; - foreach (Pair votable in voteList) + foreach (KeyValuePair votable in voteList) { - if (selected == null || votable.Second > highestVotes) + if (voteType == VoteType.Sub + && votable.Key is SubmarineInfo subInfo + && GameMain.NetworkMember.ServerSettings.HiddenSubs.Contains(subInfo.Name)) { - highestVotes = votable.Second; - selected = (T)votable.First; + //This sub is hidden so it can't be voted for, skip + continue; + } + if (selected == null || votable.Value > highestVotes) + { + highestVotes = votable.Value; + selected = votable.Key; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 81086e854..4707fa46e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -226,7 +226,14 @@ namespace Barotrauma public bool PhysEnabled { get { return FarseerBody.Enabled; } - set { isPhysEnabled = value; if (Enabled) FarseerBody.Enabled = value; } + set + { + isPhysEnabled = value; + if (Enabled) + { + FarseerBody.Enabled = value; + } + } } public Vector2 SimPosition diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 059e082f4..a238947bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -72,6 +72,23 @@ namespace Barotrauma return prefabs.ContainsKey(identifier); } + /// + /// Returns true if a prefab with the identifier exists, false otherwise. + /// + /// Prefab identifier + /// The matching prefab (if one is found) + /// Whether a prefab with the identifier exists or not + public bool TryGetValue(string identifier, out T prefab) + { + if (!ContainsKey(identifier)) + { + prefab = default; + return false; + } + prefab = this[identifier]; + return true; + } + /// /// Add a prefab to the collection. /// If not marked as an override, fail if a prefab with the same diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 0d74a8e66..7bd12a932 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -68,13 +68,13 @@ namespace Barotrauma Projectile projectile = (entity as Item)?.GetComponent(); if (projectile == null) { - DebugConsole.NewMessage("Non-projectile using a delaytype of reachcursor", Color.Red, false, true); + DebugConsole.ShowError("Non-projectile using a delaytype of reachcursor"); return; } if (projectile.User == null) { - DebugConsole.NewMessage("Projectile: '" + projectile.Name + "' missing user to determine distance", Color.Red, false, true); + DebugConsole.ShowError("Projectile: '" + projectile.Name + "' missing user to determine distance"); return; } @@ -108,7 +108,7 @@ namespace Barotrauma if (projectile == null) { #if DEBUG - DebugConsole.NewMessage("Non-projectile using a delaytype of reachcursor", Color.Red, false, true); + DebugConsole.ShowError("Non-projectile using a delaytype of reachcursor"); #endif return; } @@ -116,7 +116,7 @@ namespace Barotrauma if (projectile.User == null) { #if DEBUG - DebugConsole.NewMessage("Projectile " + projectile.Name + "missing user", Color.Red, false, true); + DebugConsole.ShowError("Projectile " + projectile.Name + "missing user"); #endif return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 6bb5586c1..a165e9c50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -138,6 +138,10 @@ namespace Barotrauma public readonly ItemPrefab ItemPrefab; public readonly SpawnPositionType SpawnPosition; public readonly bool SpawnIfInventoryFull; + /// + /// Should the item spawn even if the container can't contain items of this type + /// + public readonly bool SpawnIfCantBeContained; public readonly float Speed; public readonly float Rotation; public readonly int Count; @@ -176,6 +180,7 @@ namespace Barotrauma } SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false); + SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); Speed = element.GetAttributeFloat("speed", 0.0f); Rotation = element.GetAttributeFloat("rotation", 0.0f); @@ -332,7 +337,7 @@ namespace Barotrauma private set; } - private readonly bool modifyAfflictionsByMaxVitality; + private readonly bool multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters { @@ -396,7 +401,7 @@ namespace Barotrauma ReduceAffliction = new List<(string affliction, float amount)>(); giveExperiences = new List(); giveSkills = new List<(string, float)>(); - modifyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); + multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); @@ -891,20 +896,26 @@ namespace Barotrauma { owner = ownerItem.ParentInventory?.Owner; } - if (owner is Item container && !HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) + { + //if we're checking for tags, just check the Item object, not the ItemComponents + if (!HasRequiredConditions((container as ISerializableEntity).ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return false; } + } + else + { + if (!HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return false; } + } + } if (owner is Character character && !HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return false; } } else { - foreach (ISerializableEntity target in targets) + var validTargets = targets; + if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) { - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) - { - if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) - { - continue; - } - } + validTargets = targets.Where(t => t is ItemComponent ic && ic.Name == pc.TargetItemComponentName); } if (targets.None(t => pc.Matches(t))) { return false; } } @@ -1200,7 +1211,7 @@ namespace Barotrauma if (target is Character character) { if (character.Removed) { continue; } - newAffliction = GetMultipliedAffliction(affliction, entity, character, deltaTime, modifyAfflictionsByMaxVitality); + newAffliction = GetMultipliedAffliction(affliction, entity, character, deltaTime, multiplyAfflictionsByMaxVitality); character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { @@ -1218,7 +1229,7 @@ namespace Barotrauma { if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } - newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, modifyAfflictionsByMaxVitality); + newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); RegisterTreatmentResults(entity, limb, affliction, result); @@ -1489,7 +1500,12 @@ namespace Barotrauma } else if (entity is Item item) { - inventory = item?.GetComponent()?.Inventory; + var itemContainer = item.GetComponent(); + inventory = itemContainer?.Inventory; + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + return; + } } if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) { @@ -1518,7 +1534,12 @@ namespace Barotrauma } else if (entity is Item item) { - thisInventory = item?.GetComponent()?.Inventory; + var itemContainer = item.GetComponent(); + thisInventory = itemContainer?.Inventory; + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + return; + } } if (thisInventory != null) { @@ -1629,14 +1650,14 @@ namespace Barotrauma if (target is Character character) { if (character.Removed) { continue; } - newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.modifyAfflictionsByMaxVitality); + newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); element.Parent.RegisterTreatmentResults(element.Entity, result.HitLimb, affliction, result); } else if (target is Limb limb) { if (limb.character.Removed || limb.Removed) { continue; } - newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.modifyAfflictionsByMaxVitality); + newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 2a62d2b4e..1562443c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -181,7 +181,6 @@ namespace Barotrauma var targetProperties = new Dictionary(); string nameIdentifier = element.GetAttributeString("nameidentifier", ""); - if (!string.IsNullOrWhiteSpace(nameIdentifier)) { Name = TextManager.Get($"UpgradeName.{nameIdentifier}", returnNull: true) ?? string.Empty; @@ -191,7 +190,12 @@ namespace Barotrauma Name = TextManager.Get($"UpgradeName.{Identifier}", returnNull: true) ?? string.Empty; } - if (string.IsNullOrWhiteSpace(Description)) + string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); + if (!string.IsNullOrWhiteSpace(descriptionIdentifier)) + { + Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}", returnNull: true) ?? string.Empty; + } + else if (string.IsNullOrWhiteSpace(Description)) { Description = TextManager.Get($"UpgradeDescription.{Identifier}", returnNull: true) ?? string.Empty; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index 6e086afb8..038c69e55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +#nullable enable +using Barotrauma.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -12,13 +13,13 @@ namespace Barotrauma private int maxId; - private readonly List> srcRanges; + private readonly List>? srcRanges; private readonly int destOffset; - public IdRemap(XElement parentElement, int offset) + public IdRemap(XElement? parentElement, int offset) { destOffset = offset; - if (parentElement != null && parentElement.HasElements) + if (parentElement is { HasElements: true }) { srcRanges = new List>(); foreach (XElement subElement in parentElement.Elements()) @@ -26,56 +27,60 @@ namespace Barotrauma int id = subElement.GetAttributeInt("ID", -1); if (id > 0) { InsertId(id); } } - maxId = GetOffsetId(srcRanges.Last().End) + 1; + maxId = GetOffsetId(srcRanges.Last().End); } else { - maxId = offset + 1; + maxId = offset; } } - public ushort AssignMaxId() + public void AssignMaxId(out ushort result) { maxId++; - return (ushort)maxId; + result = (ushort)maxId; } private void InsertId(int id) { - for (int i = 0; i < srcRanges.Count; i++) + if (srcRanges is null) { throw new NullReferenceException("Called InsertId when srcRanges is null"); } + + void tryMergeRangeWithNext(int indexA) { - if (srcRanges[i].Start > id) + int indexB = indexA + 1; + + if (indexA < 0 /* Index A out of bounds */ + || indexB >= srcRanges.Count /* Index B out of bounds */) { - if (srcRanges[i].Start == (id + 1)) - { - srcRanges[i] = new Range(id, srcRanges[i].End); - if (i > 0 && srcRanges[i].Start == srcRanges[i - 1].End) - { - srcRanges[i - 1] = new Range(srcRanges[i - 1].Start, srcRanges[i].End); - srcRanges.RemoveAt(i); - } - } - else - { - srcRanges.Insert(i, new Range(id, id)); - } return; } - else if (srcRanges[i].End < id) + + Range rangeA = srcRanges[indexA]; + Range rangeB = srcRanges[indexB]; + + if ((rangeA.End+1) >= rangeB.Start) //The end of range A is right before the start of range B, this should be one range { - if (srcRanges[i].End == (id - 1)) - { - srcRanges[i] = new Range(srcRanges[i].Start, id); - if (i < (srcRanges.Count - 1) && srcRanges[i].End == srcRanges[i + 1].Start) - { - srcRanges[i] = new Range(srcRanges[i].Start, srcRanges[i + 1].End); - srcRanges.RemoveAt(i + 1); - } - return; - } + srcRanges[indexA] = new Range(rangeA.Start, rangeB.End); + srcRanges.RemoveAt(indexB); } } - srcRanges.Add(new Range(id, id)); + + int insertIndex = srcRanges.Count; + for (int i = 0; i < srcRanges.Count; i++) + { + if (srcRanges[i].Contains(id)) //We already have a range that contains this ID, duplicates are invalid input! + { + throw new InvalidOperationException($"Duplicate ID: {id}"); + } + if (srcRanges[i].Start > id) //ID is between srcRanges[i-1] and srcRanges[i], insert at i + { + insertIndex = i; + break; + } + } + srcRanges.Insert(insertIndex, new Range(id, id)); //Insert new range consisting of solely the new ID + tryMergeRangeWithNext(insertIndex); //Try merging new range with the one that comes after it + tryMergeRangeWithNext(insertIndex - 1); //Try merging new range with the one that comes before it } public ushort GetOffsetId(XElement element) @@ -85,31 +90,47 @@ namespace Barotrauma public ushort GetOffsetId(int id) { - if (id <= 0) { return 0; } - if (destOffset < 0) { return 0; } - if (srcRanges == null) { return (ushort)(id + destOffset); } + if (id <= 0) //Input cannot be remapped because it's negative + { + return 0; + } + if (destOffset < 0) //Remapper has been defined to discard all input + { + return 0; + } + if (srcRanges is null) //Remapper defines no source ranges so it just adds an offset + { + return (ushort)(id + destOffset); + } + + int rangeSize(in Range r) + => r.End - r.Start + 1; int currOffset = destOffset; for (int i = 0; i < srcRanges.Count; i++) { - if (id >= srcRanges[i].Start && id <= srcRanges[i].End) + if (srcRanges[i].Contains(id)) { - return (ushort)(id - srcRanges[i].Start + 1 + currOffset); + //The source range for this ID has been found! + //The return value is such that all IDs that + //are returned by this remapper are contiguous, + //even if they weren't originally + return (ushort)(id - srcRanges[i].Start + currOffset); } - currOffset += srcRanges[i].End - srcRanges[i].Start + 1; + currOffset += rangeSize(srcRanges[i]); } return 0; } public static ushort DetermineNewOffset() { - ushort idOffset = 0; + int largestEntityId = 0; foreach (Entity e in Entity.GetEntities()) { if (e.ID > Entity.ReservedIDStart || e is Submarine) { continue; } - idOffset = Math.Max(idOffset, e.ID); + largestEntityId = Math.Max(largestEntityId, e.ID); } - return idOffset; + return (ushort)(largestEntityId+1); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs index 5d380e221..eaf89a4f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs @@ -1,8 +1,12 @@ +#nullable enable using System; namespace Barotrauma { - public struct Range where T : IComparable + /// + /// An inclusive range, i.e. [Start, End] where Start <= End + /// + public struct Range where T : notnull, IComparable { private T start; private T end; public T Start @@ -25,6 +29,9 @@ namespace Barotrauma } } + public bool Contains(in T v) + => start.CompareTo(v) <= 0 && end.CompareTo(v) >= 0; + private void VerifyStartLessThanEnd() { if (start.CompareTo(end) > 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.Start set to a value greater than End ({start} > {end})"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index de832af9f..94cfaac4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -425,7 +425,7 @@ namespace Barotrauma return buffer; } - public static T SelectWeightedRandom(IList objects, IList weights, Rand.RandSync randSync) + public static T SelectWeightedRandom(IList objects, IList weights, Rand.RandSync randSync = Rand.RandSync.Unsynced) { return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync)); } diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index c7ca33522..f4f87a18d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub new file mode 100644 index 000000000..f8c492f18 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index efb5e3678..9274b5c66 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/TintTest.png b/Barotrauma/BarotraumaShared/TintTest.png deleted file mode 100644 index 77afc5ae9..000000000 Binary files a/Barotrauma/BarotraumaShared/TintTest.png and /dev/null differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 997719739..493fde24f 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,142 @@ +--------------------------------------------------------------------------------------------------------- +v0.15.15.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed crashing when bots switch to the combat state. +- Removed stun tools from riot officer loadout, replaced with riot shotgun. +- Burns slowly heal by themselves, adjusted radiation poisoning accordingly. +- Fixed bots being unable to clean up items stolen from an outpost after leaving the outpost. +- Fixed incorrect base skill being used in skill reduction calculation. + +--------------------------------------------------------------------------------------------------------- +v0.15.14.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Adjusted gardening: the plants now require less continuous attention, making gardening a more viable "downtime activity" that you can focus on when there's nothing else to do, and ignore when you're busy without killing the plants. +- Added "Pump In" option to the contextual "Pump Water" order. +- Added "fuel_percentage_left" output to reactors. Outputs the sum of the fuel rods' condition percentage, as opposed to the total "heating power" of the rods like the "fuel_out" output. +- Powered down reactors don't zap the user when repairing. +- Improved weapon indicators on the status monitor (they now indicate the rotation of the turrets). +- Damaging outpost NPCs when there's a monster or an instigator nearby doesn't turn the outpost NPCs hostile (i.e. accidentally damaging one of the NPCs when you're fighting a hostile NPC doesn't trigger the guards). +- Damaging outpost NPCs when there's a monster or an instigator nearby doesn't lower outpost reputation. +- Adjustments to job gear and diving suit sprites and inventory icons. +- New depth charge tube sprite. +- Added a verification prompt when saving and quitting a campaign round (mp or sp). +- Added options to adjust karma penalty for containing dangerous items in the server settings. +- Made it a bit more viable to take out enemy humans stealthily: if an attack immediately kills/incapacitates the character, others won't be alerted unless they witness the attack. +- Option to end outpost rounds without saving. +- Made sonar transducer consume less power when the sonar is running in passive mode (or when the transducer isn't connected to anything). +- Treatment suggestions in the health interface take items in subinventories into account as well. +- Added tooltips to treatment suggestions to make it more clear the treatments can be applied by clicking on the suggestions. +- Added "condition_out" pin to fabricators, deconstructors and blank loader. +- Added "set_delay" input to delay component. +- Tagged boarding axe and assault rifle as medium items to allow storing them in cabinets. +- Removed "weapon" and "gun" tags from the bike horn and the syringe gun. +- Added a menu to hide submarines in the server lobby, allowing hosts to remove vanilla submarines from the list without replacing the content package. +- Added LOS effect to the server settings. +- Added line break support to the server message. + +Multiplayer fixes: +- Fixed campaign character resetting if the client, disconnects, rejoins and respawns on the same round after their character has gotten killed. +- Fixed skill penalties not getting applied when respawning during the same round. +- Fixed several issues (namely mission/submarine mismatches and desyncs) when clients take too long to receive an up-to-date campaign save at the start of a new round. +- Fixed "received data without a transfer initiation message" errors when a client disconnects and reconnects while a file transfer is in progress. +- Fixed respawn not triggering during multiplayer rounds if a client disconnects and rejoins after dying. Happened because the server was expecting the client to answer the "respawn with penalty?" prompt, which wouldn't be shown when the client rejoins mid-round. +- Fixed server making Client A switch their character to Client B's when Client B disconnects if the clients have the same connection endpoint (Steam ID or IP, for example when the players are using the same wifi connection), and Client B's character was created after Client A's. +- Fixed items sometimes teleporting from monster to another or dropping when trying to pick them up by double-clicking. Happened when the monster had selected (= grabbed, started to eat) another monster when they were still alive. +- Fixed crashing if you start the round as a spectator, take control of someone with console commands and try to access the tab menu's character tab. +- Fixed empty exploding coilgun ammo boxes exploding client-side when deconstructed. +- Fixed disabling friendly fire preventing buffs from being applied to crewmates, while allowing harmful afflictions to be applied (= the setting basically worked the wrong way around). + +Talent fixes: +- Fixed unlocking "all seeing eye" preventing you from unlocking further talents until you've gained enough EXP to compensate for the 3 "free" talents. +- "Scrap savant" and "scrounger" only spawn the scrap in items with the tag "container" and only if the container can hold scrap. Fixes ability to generate free scrap by placing containers like detonators and portable pumps in the wrecks. +- Fixed "canned heat" not increasing the quality of fabricated oxygenite tanks. +- Fixed "pyromaniac" applying the buff to the enemy you're damaging instead of your character. +- Fixed server ending the round if the last player alive is an assistant with the "still kicking" talent and said assistant falls unconscious. +- Electrochemist talent doesn't stun the attacker if the attack doesn't apply any harmful afflictions (e.g. if it only applies buffs or gives experience). +- Fixed high-quality items with whose max condition is above 100% not being accepted as fabrication ingredients. +- Fixed autofill not working on the new talent items. +- Added PreferredContainers to the new talent items to allow the bots to clean them up. +- Fixed "crew layabout" applying stat boosts to enemies as well. + +AI: +- New order: Find Weapon +- New order: Prepare for Expedition +- Added an option to pump in water. Only applicable to pumps that are not automatically controlled. +- Orders that cannot be completed should now be dismissed automatically instead of keeping active but doing nothing. +- The bots should now tell you when they can't follow an order, instead of always replying positively. +- Fixed bots getting stuck with invalid paths for too long. +- Fixed AIObjectiveGetItem not working properly when we try to ignore the items that already are in the inventory. In practice, only affects the find better weapons behavior in the combat objective (Fight Intruders). +- Bots don't automatically unequip PUCS when they don't need diving gear. +- Don't allow bots to heal pets (because they are likely to just kill them). +- Fixed bots trying to equip diving gear when they have spliced genes that give pressure immunity and/or remove the need for oxygen. + +Misc fixes: +- Exploding the abandoned outpost reactor doesn't damage the player sub in missions that require destroying it. +- Hopefully occasional fixed fires/meltdowns at the start of a round. Was caused by the "warm start" logic that simulates 10 seconds of power grid updates at the start of a round: if the reactor wasn't running on auto or was controlled by some custom circuit, it wouldn't adapt to the changes in the grid load during those simulated 10 seconds, which could lead to overloads. +- Fixed blips not disappearing from the sonar when it's been repaired above 100% condition by using talents. +- Fixed Security Officer Uniform's and Gunner Uniform's icons being swapped. +- Fixed RegExFindComponent handling some inputs incorrectly. +- Fixed sprite bleed in the harmonica inventory icon. +- Fixed crashing when equipping a handheld status monitor that spawned outside subs. +- Fixed ruins sometimes extending above the top of the level. +- Fixed submarine editor failing to generate waypoints on stairs. +- Fixed submarine editor failing to connect some of the waypoints around the sub. +- Fixed non-localized list formatting in the "I need [treatment1], [treatment2] or [treatment3]" bot dialog. +- Fixed crashing if a mineral mission fails to find a long enough edge to spawn the resources on. +- Fixed one of the hulls not covering the whole room in the "Alien_MaintenanceTunnels1" module. +- Widen a gap in the "Alien_Entrance2" module to cover the full width of the hole in the wall. +- Removed non-interactable black wire from inside one of the terminals in Alien_Chasm2. +- Fixed pirate subs sometimes not withstanding the pressure in the level in the deeper biomes. +- Fixed diving gear not affecting the depth at which huskified humans' get crushed by pressure. +- Fixed characters with husk genes + husk infection becoming huskified when they die. +- Fixed the ragdolls breaking when characters with a harpoon/guardian spear (or any other projectile) attached to their body exits a submarine/ruin. +- Fixed harpoon rope pulling with excessive forces when the target is on a platform. +- Always snap the rope between the harpoon and the harpoon gun when the harpoon is detached from the target. Possibly fixes cases where physics forces from the rope are still applied when the projectile has dropped. Also applies to fractal guardians. +- Fixed physics glitches with guardian's tail when it switches subs or goes outside. +- Fixed characters sometimes getting teleported to an invalid position when they are exiting/entering the sub and have more than one swarm feeder latched on them. +- Fixed Kastrull and Typhon 2 using different id card tags than the rest of the vanilla sub ("idjob" instead of "id_job"). +- Fixed artifact transport cases not reliably suppressing thermal artifact fires. +- Fixed ability to stack batteries in wrecked charging docks. +- Fixed certain signal components (boolean operators, artihmetic, equals, trigonometric, string) triggering at the start of a round when they're set to a timeframe larger than 0.1 s. +- Fabricator drops the items inside the fabrication materials instead of consuming them (e.g. fabricating a combat suit from a normal one with a tank inside doesn't make the tank disappear). +- Fixed deconstructor destroying items contained in the deconstructed item if they can't fit in the output slots. +- Fixed lights becoming full-brightness for one frame when their color is set using the "set_color" input. +- Fixed turret lights' rotation being wrong for one frame when the light is toggled on +- Fixed shuttles docked to a shuttle docked to the main sub appearing on sonar in PvP. +- Fixed detonator's contained item position. +- Fixed Jovian Radiation bypassing all damage modifiers, including wearable items that protect from radiation. +- Fixed outpost reactors not accepting volatile fulgurium rods as fuel. +- Fixed some ugly first frames when populating certain listboxes (e.g. server list). +- Fixed spineling spikes given by spineling genes launching in an incorrect direction when the character is mirrored (= facing left) in multiplayer. +- Characters don't consume hull oxygen when their need for oxygen has been removed with thresher genes or pressure stabilizer. +- Fixed cigar and captain's pipe giving practically no psychosis resistance. +- Fixed being able to sell items from inventories of characters who are on the player's sub. +- Fixed occasional "Attempted to set the anchor B of a limb's pull joint to an invalid value" errors when dragging someone underwater. +- Fixed oxygenite tanks not being affected by gas vents in caves. +- Fixed items disappearing when trying to combine stacks of partially consumed items. +- Fixed upgrading devices' max condition causing issues with repairing: the repair thresholds were being treated as condition values instead of percentages, meaning that the devices would need to deteriorate more before they become repairable. +- Fixed ability to combine genetic materials in normal deconstructors. +- Fixed harpoon guns spawning with only 5 harpoons and stun guns with 1 dart. +- Fixed "bombscare" outpost event giving the characters xp for a non-existent "engineering" skill. +- Restrict downwards movement when a character has reached the bottom of a ladder to fix phasing through the floor. +- Fixed characters' feet dangling in the air when moving from a ladder to another (e.g. when climbing from the sub to the outpost). +- Adjusted flamethrower and prototype steam cannon particles to get them to match the range of the weapons more closely. + +Modding: +- Fixed inability to override talent trees. +- Item variants try to load sprites from the base item's directory if the path isn't specified. +- Fixed crashing when trying to fire a projectile with no Attack configured with a turret. +- Made memory component's "writeable" field editable in the sub editor. +- Fixed all ruin generation params displaying "RuinGenerationParams" as their name in the level editor. +- Fixed PropertyConditionals with comparison type "And" that check a parent container's tags being very unintuitive to use because they checked the components of the item as well, not just the item object. +- Made several outpost store parameters editable separately for each location type (see locationTypes.xml). +- Fixed crashing when trying to create a non-humanoid that can walk in the character editor. +- Added a randomize button for level editor seed. +- Fixed fabricators without a repairable component crashing the game when activated. + --------------------------------------------------------------------------------------------------------- v0.15.13.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/steam_api64.dll b/Barotrauma/BarotraumaShared/steam_api64.dll deleted file mode 100644 index ad13f2b6c..000000000 Binary files a/Barotrauma/BarotraumaShared/steam_api64.dll and /dev/null differ