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..9d3900b80 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; @@ -63,7 +63,10 @@ namespace Barotrauma { foreach (var ic in character.MemState[0].SelectedItem.Components) { - if (ic.CanBeSelected) ic.Select(character); + if (ic.CanBeSelected) + { + ic.Select(character); + } } } character.SelectedConstruction = character.MemState[0].SelectedItem; @@ -98,6 +101,16 @@ namespace Barotrauma if (distSqrd > 10.0f || !character.CanMove) { Collider.TargetRotation = newRotation; + if (distSqrd > 10.0f) + { + //teleported very far - see if we need to move to another sub + Hull serverHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(newPosition), CurrentHull, newPosition.Y < lowestSubPos); + if (currentHull != null && serverHull != null && serverHull.Submarine != currentHull.Submarine) + { + character.Submarine = serverHull.Submarine; + character.CurrentHull = CurrentHull = serverHull; + } + } SetPosition(newPosition, lerp: distSqrd < 5.0f, ignorePlatforms: false); } else @@ -159,7 +172,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; @@ -194,7 +207,7 @@ namespace Barotrauma { if (character.SelectedConstruction != serverPos.SelectedItem) { - serverPos.SelectedItem.TryInteract(character, true, true); + serverPos.SelectedItem.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true); } character.SelectedConstruction = serverPos.SelectedItem; } @@ -448,8 +461,8 @@ namespace Barotrauma { DebugConsole.ThrowError("Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.Name + "\", removed: " + character.Removed + "\n" + Environment.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce("Ragdoll.Draw:LimbsRemoved", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.Name + "\", removed: " + character.Removed + "\n" + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.ErrorSeverity.Error, + "Failed to draw a ragdoll, limbs have been removed. Character: \"" + character.SpeciesName + "\", removed: " + character.Removed + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -460,12 +473,17 @@ namespace Barotrauma } float depthOffset = GetDepthOffset(); + if (!MathUtils.NearlyEqual(depthOffset, 0.0f)) + { + foreach (Limb limb in limbs) { limb.ActiveSprite.Depth += depthOffset; } + } for (int i = 0; i < limbs.Length; i++) { - var limb = inversedLimbDrawOrder[i]; - if (depthOffset != 0.0f) { limb.ActiveSprite.Depth += depthOffset; } - limb.Draw(spriteBatch, cam, color); - if (depthOffset != 0.0f) { limb.ActiveSprite.Depth -= depthOffset; } + inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color); + } + if (!MathUtils.NearlyEqual(depthOffset, 0.0f)) + { + foreach (Limb limb in limbs) { limb.ActiveSprite.Depth -= depthOffset; } } LimbJoints.ForEach(j => j.Draw(spriteBatch)); } @@ -486,7 +504,14 @@ namespace Barotrauma if (character.WorldPosition.X < character.SelectedConstruction.WorldPosition.X) { //at the left side of the ladder, needs to be drawn in front of the rungs - depthOffset = Math.Max(ladder.BackgroundSpriteDepth - 0.01f - maxDepth, 0.0f); + if (maxDepth > ladder.BackgroundSpriteDepth) + { + depthOffset = Math.Max(ladder.BackgroundSpriteDepth - 0.01f - maxDepth, 0.0f); + } + else + { + depthOffset = Math.Max(ladder.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth); + } } else { @@ -581,10 +606,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..62ebb226f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -305,9 +305,9 @@ namespace Barotrauma case 0: //NetEntityEvent.Type.InventoryState if (Inventory == null) { - string errorMsg = "Received an inventory update message for an entity with no inventory (" + Name + ", removed: " + Removed + ")"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")"; + DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName)); //read anyway to prevent messing up reading the rest of the message _ = msg.ReadUInt16(); @@ -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]; @@ -650,7 +651,7 @@ namespace Barotrauma { string errorMsg = $"Error in CharacterNetworking.ReadStatus: affliction not found ({afflictionName})"; causeOfDeathType = CauseOfDeathType.Unknown; - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:AfflictionIndexOutOfBounts", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:AfflictionIndexOutOfBounts", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } else { @@ -681,7 +682,7 @@ namespace Barotrauma if (severedJointIndex < 0 || severedJointIndex >= AnimController.LimbJoints.Length) { string errorMsg = $"Error in CharacterNetworking.ReadStatus: severed joint index out of bounds (index: {severedJointIndex}, joint count: {AnimController.LimbJoints.Length})"; - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:JointIndexOutOfBounts", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:JointIndexOutOfBounts", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 9ee0fa4ba..2660e8a10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -453,13 +453,14 @@ namespace Barotrauma { case Alignment.Left: healthWindow.RectTransform.SetPosition(Anchor.BottomLeft); + healthWindow.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.InventoryAreaLower.X, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); break; case Alignment.Right: healthWindow.RectTransform.SetPosition(Anchor.BottomRight); + healthWindow.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); break; } - healthWindow.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); healthWindow.RectTransform.RecalculateChildren(false); } @@ -648,8 +649,9 @@ namespace Barotrauma grainColor = oxygenLowGrainColor; } - foreach (Affliction affliction in afflictions) + foreach (KeyValuePair kvp in afflictions) { + var affliction = kvp.Key; distortStrength = Math.Max(distortStrength, affliction.GetScreenDistortStrength()); blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); @@ -662,16 +664,6 @@ namespace Barotrauma grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); } } - foreach (LimbHealth limbHealth in limbHealths) - { - foreach (Affliction affliction in limbHealth.Afflictions) - { - distortStrength = Math.Max(distortStrength, affliction.GetScreenDistortStrength()); - blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); - radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); - chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); - } - } Character.RadialDistortStrength = radialDistortStrength; Character.ChromaticAberrationStrength = chromaticAberrationStrength; @@ -777,7 +769,7 @@ namespace Barotrauma { // If no limb is selected or highlighted, select the one with the most critical afflictions. var affliction = SortAfflictionsBySeverity(GetAllAfflictions(a => a.Prefab.IndicatorLimb != LimbType.None)).FirstOrDefault(); - if (affliction.DamagePerSecond > 0 || affliction.Strength > 0) + if (affliction != null && (affliction.DamagePerSecond > 0 || affliction.Strength > 0)) { var limbHealth = GetMatchingLimbHealth(affliction); if (limbHealth != null) @@ -788,7 +780,7 @@ namespace Barotrauma else { // If no affliction is critical, select the limb which has most damage. - var limbHealth = limbHealths.OrderByDescending(l => l.TotalDamage).FirstOrDefault(); + var limbHealth = limbHealths.OrderByDescending(l => GetTotalDamage(l)).FirstOrDefault(); selectedLimbIndex = limbHealths.IndexOf(limbHealth); } } @@ -805,7 +797,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; @@ -951,8 +963,9 @@ namespace Barotrauma UpdateAlignment(); } - foreach (Affliction affliction in afflictions) + foreach (KeyValuePair kvp in afflictions) { + var affliction = kvp.Key; if (affliction.Prefab.AfflictionOverlay != null) { Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; @@ -964,7 +977,7 @@ namespace Barotrauma float damageOverlayAlpha = DamageOverlayTimer; if (Vitality < MaxVitality * 0.1f) { - damageOverlayAlpha = Math.Max(1.0f - (Vitality / maxVitality * 10.0f), damageOverlayAlpha); + damageOverlayAlpha = Math.Max(1.0f - (Vitality / UnmodifiedMaxVitality * 10.0f), damageOverlayAlpha); } else { @@ -1139,18 +1152,34 @@ namespace Barotrauma afflictionIconContainer.Content.ClearChildren(); return; } - var currentAfflictions = GetMatchingAfflictions(selectedLimb, a => a.ShouldShowIcon(Character)); - if (currentAfflictions.Any(a => !displayedAfflictions.Any(d => d.affliction == a)) || - displayedAfflictions.Any(a => !currentAfflictions.Contains(a.affliction))) + + if (afflictionsDirty()) { + var currentAfflictions = afflictions.Where(a => ShouldDisplayAfflictionOnLimb(a, selectedLimb)).Select(a => a.Key); CreateAfflictionInfos(currentAfflictions); CreateRecommendedTreatments(); } //update recommended treatments if the strength of some displayed affliction has changed by > 1 - else if (displayedAfflictions.Any(d => Math.Abs(d.strength - currentAfflictions.First(a => a == d.affliction).Strength) > 1.0f)) + else if (displayedAfflictions.Any(d => Math.Abs(d.strength - d.affliction.Strength) > 1.0f)) { CreateRecommendedTreatments(); } + + bool afflictionsDirty() + { + //not displaying one of the current afflictions -> dirty + foreach (KeyValuePair kvp in afflictions) + { + if (!ShouldDisplayAfflictionOnLimb(kvp, selectedLimb)) { continue; } + if (!displayedAfflictions.Any(d => d.affliction == kvp.Key)) { return true; } + } + //displaying an affliction we no longer have -> dirty + foreach ((Affliction affliction, float strength) in displayedAfflictions) + { + if (!afflictions.Any(a => a.Key == affliction)) { return true; } + } + return false; + } } private void CreateAfflictionInfos(IEnumerable afflictions) @@ -1158,7 +1187,7 @@ namespace Barotrauma afflictionIconContainer.ClearChildren(); displayedAfflictions.Clear(); - Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions).FirstOrDefault(); + Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions, excludeBuffs: false).FirstOrDefault(); GUIButton buttonToSelect = null; foreach (Affliction affliction in afflictions) @@ -1249,7 +1278,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 +1317,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 +1473,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; @@ -1585,7 +1613,7 @@ namespace Barotrauma int i = 0; foreach (LimbHealth limbHealth in limbHealths) { - if (limbHealth.IndicatorSprite == null) continue; + if (limbHealth.IndicatorSprite == null) { continue; } float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); @@ -1604,6 +1632,7 @@ namespace Barotrauma } } + private static readonly List afflictionsDisplayedOnLimb = new List(); private void DrawHealthWindow(SpriteBatch spriteBatch, Rectangle drawArea, bool allowHighlight) { if (Character.Removed) { return; } @@ -1614,21 +1643,32 @@ namespace Barotrauma int i = 0; foreach (LimbHealth limbHealth in limbHealths) { - if (limbHealth.IndicatorSprite == null) continue; + if (limbHealth.IndicatorSprite == null) { continue; } Rectangle limbEffectiveArea = new Rectangle(limbHealth.IndicatorSprite.SourceRect.X + limbHealth.HighlightArea.X, limbHealth.IndicatorSprite.SourceRect.Y + limbHealth.HighlightArea.Y, limbHealth.HighlightArea.Width, limbHealth.HighlightArea.Height); - float damageLerp = limbHealth.TotalDamage > 0.0f ? MathHelper.Lerp(0.2f, 1.0f, limbHealth.TotalDamage / 100.0f) : 0.0f; + float totalDamage = GetTotalDamage(limbHealth); - var tempAfflictions = GetMatchingAfflictions(limbHealth, a => true); + float damageLerp = totalDamage > 0.0f ? MathHelper.Lerp(0.2f, 1.0f, totalDamage / 100.0f) : 0.0f; - float negativeEffect = tempAfflictions.Where(a => !a.Prefab.IsBuff && a.ShouldShowIcon(Character)).Sum(a => a.Strength); - //float negativeMaxEffect = tempAfflictions.Where(a => !a.Prefab.IsBuff).Sum(a => a.Prefab.MaxStrength); - float positiveEffect = tempAfflictions.Where(a => a.Prefab.IsBuff && a.ShouldShowIcon(Character)).Sum(a => a.Strength * 0.2f); - //float positiveMaxEffect = tempAfflictions.Where(a => a.Prefab.IsBuff).Sum(a => a.Prefab.MaxStrength); + float negativeEffect = 0.0f, positiveEffect = 0.0f; + foreach (KeyValuePair kvp in afflictions) + { + if (kvp.Value != limbHealth) { continue; } + var affliction = kvp.Key; + if (!affliction.ShouldShowIcon(Character)) { continue; } + if (!affliction.Prefab.IsBuff) + { + negativeEffect += affliction.Strength; + } + else + { + positiveEffect += affliction.Strength * 0.2f; + } + } float midPoint = (float)limbEffectiveArea.Center.Y / (float)limbHealth.IndicatorSprite.Texture.Height; float fadeDist = 0.6f * (float)limbEffectiveArea.Height / (float)limbHealth.IndicatorSprite.Texture.Height; @@ -1695,7 +1735,7 @@ namespace Barotrauma drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X, drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y); - int frame = 0; + int frame; int frameCount = 17; if (limbIndicatorOverlayAnimState >= frameCount * 2) limbIndicatorOverlayAnimState = 0.0f; if (limbIndicatorOverlayAnimState < frameCount) @@ -1739,14 +1779,13 @@ namespace Barotrauma i = 0; foreach (LimbHealth limbHealth in limbHealths) { - IEnumerable thisAfflictions = limbHealth.Afflictions.Where(a => a.ShouldShowIcon(Character)); - thisAfflictions = thisAfflictions.Concat(afflictions.Where(a => + afflictionsDisplayedOnLimb.Clear(); + foreach (var affliction in afflictions) { - Limb indicatorLimb = Character.AnimController.GetLimb(a.Prefab.IndicatorLimb); - return indicatorLimb != null && indicatorLimb.HealthIndex == i && a.ShouldShowIcon(Character); - })); + if (ShouldDisplayAfflictionOnLimb(affliction, limbHealth)) { afflictionsDisplayedOnLimb.Add(affliction.Key); } + } - if (thisAfflictions.Count() <= 0) { i++; continue; } + if (!afflictionsDisplayedOnLimb.Any()) { i++; continue; } if (limbHealth.IndicatorSprite == null) { continue; } float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); @@ -1757,12 +1796,12 @@ namespace Barotrauma Vector2 iconPos = highlightArea.Center.ToVector2(); //Affliction mostSevereAffliction = thisAfflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !thisAfflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? thisAfflictions.FirstOrDefault(); - Affliction mostSevereAffliction = SortAfflictionsBySeverity(thisAfflictions, excludeBuffs: false).FirstOrDefault(); + Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictionsDisplayedOnLimb, excludeBuffs: false).FirstOrDefault(); if (mostSevereAffliction != null) { DrawLimbAfflictionIcon(spriteBatch, mostSevereAffliction, iconScale, ref iconPos); } - if (thisAfflictions.Count() > 1) + if (afflictionsDisplayedOnLimb.Count() > 1) { - string additionalAfflictionCount = $"+{thisAfflictions.Count() - 1}"; + string additionalAfflictionCount = $"+{afflictionsDisplayedOnLimb.Count() - 1}"; Vector2 displace = GUI.SubHeadingFont.MeasureString(additionalAfflictionCount); GUI.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f); GUI.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White); @@ -1785,6 +1824,22 @@ namespace Barotrauma } } + + private bool ShouldDisplayAfflictionOnLimb(KeyValuePair kvp, LimbHealth limbHealth) + { + if (!kvp.Key.ShouldShowIcon(Character)) { return false; } + if (kvp.Value == limbHealth) + { + return true; + } + else if (kvp.Value == null) + { + Limb indicatorLimb = Character.AnimController.GetLimb(kvp.Key.Prefab.IndicatorLimb); + return indicatorLimb != null && indicatorLimb.HealthIndex == limbHealths.IndexOf(limbHealth); + } + return false; + } + private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos) { if (!affliction.ShouldShowIcon(Character) || affliction.Prefab.Icon == null) { return; } @@ -1815,8 +1870,7 @@ namespace Barotrauma healthBarHolder.Visible = value; } - private readonly List<(AfflictionPrefab afflictionPrefab, float strength)> newAfflictions = new List<(AfflictionPrefab afflictionPrefab, float strength)>(); - private readonly List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)> newLimbAfflictions = new List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)>(); + private readonly List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)> newAfflictions = new List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)>(); private readonly List<(AfflictionPrefab.PeriodicEffect effect, float timer)> newPeriodicEffects = new List<(AfflictionPrefab.PeriodicEffect effect, float timer)>(); public void ClientRead(IReadMessage inc) @@ -1846,47 +1900,9 @@ namespace Barotrauma float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } - newAfflictions.Add((afflictionPrefab, afflictionStrength)); + newAfflictions.Add((null, afflictionPrefab, afflictionStrength)); } - foreach (Affliction affliction in afflictions) - { - //deactivate afflictions that weren't included in the network message - if (!newAfflictions.Any(a => a.afflictionPrefab == affliction.Prefab)) - { - affliction.Strength = 0.0f; - } - } - - foreach (var (afflictionPrefab, strength) in newAfflictions) - { - Affliction existingAffliction = afflictions.Find(a => a.Prefab == afflictionPrefab); - if (existingAffliction == null) - { - existingAffliction = afflictionPrefab.Instantiate(strength); - afflictions.Add(existingAffliction); - } - existingAffliction.SetStrength(strength); - if (existingAffliction == stunAffliction) - { - Character.SetStun(existingAffliction.Strength, true, true); - } - foreach (var periodicEffect in newPeriodicEffects) - { - if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } - //timer has wrapped around, apply the effect - if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) - { - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; - foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) - { - existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: null); - } - } - } - } - - newLimbAfflictions.Clear(); byte limbAfflictionCount = inc.ReadByte(); for (int i = 0; i < limbAfflictionCount; i++) { @@ -1912,43 +1928,50 @@ namespace Barotrauma float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } - newLimbAfflictions.Add((limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); + newAfflictions.Add((limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); } - foreach (LimbHealth limbHealth in limbHealths) + foreach (KeyValuePair kvp in afflictions) { - foreach (Affliction affliction in limbHealth.Afflictions) + //deactivate afflictions that weren't included in the network message + if (!newAfflictions.Any(a => kvp.Key.Prefab == a.afflictionPrefab && kvp.Value == a.limb)) { - //deactivate afflictions that weren't included in the network message - if (!newLimbAfflictions.Any(a => a.limb == limbHealth && a.afflictionPrefab == affliction.Prefab)) + kvp.Key.Strength = 0.0f; + } + } + + foreach (var (limb, afflictionPrefab, strength) in newAfflictions) + { + Affliction existingAffliction = null; + foreach (KeyValuePair kvp in afflictions) + { + if (kvp.Key.Prefab == afflictionPrefab && kvp.Value == limb) { - affliction.Strength = 0.0f; + existingAffliction = kvp.Key; + break; } } - - foreach (var (limb, afflictionPrefab, strength) in newLimbAfflictions) + if (existingAffliction == null) { - if (limb != limbHealth) { continue; } - Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == afflictionPrefab); - if (existingAffliction == null) + existingAffliction = afflictionPrefab.Instantiate(strength); + afflictions.Add(existingAffliction, limb); + } + existingAffliction.SetStrength(strength); + if (existingAffliction == stunAffliction) + { + Character.SetStun(existingAffliction.Strength, true, true); + } + foreach (var periodicEffect in newPeriodicEffects) + { + if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } + //timer has wrapped around, apply the effect + if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { - existingAffliction = afflictionPrefab.Instantiate(strength); - limbHealth.Afflictions.Add(existingAffliction); - } - existingAffliction.SetStrength(strength); - - foreach (var periodicEffect in newPeriodicEffects) - { - if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } - //timer has wrapped around, apply the effect - if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) + existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; + foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) { - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; - foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) - { - Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(limb)); - existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: targetLimb); - } + Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(limb)); + existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: targetLimb); } } } @@ -1966,14 +1989,13 @@ namespace Barotrauma limb.BurnOverlayStrength = 0.0f; limb.DamageOverlayStrength = 0.0f; - if (limbHealths[limb.HealthIndex].Afflictions.Count == 0) continue; - foreach (Affliction a in limbHealths[limb.HealthIndex].Afflictions) + foreach (KeyValuePair kvp in afflictions) { - limb.BurnOverlayStrength += a.Strength / Math.Min(a.Prefab.MaxStrength, 100) * a.Prefab.BurnOverlayAlpha; - limb.DamageOverlayStrength += a.Strength / Math.Min(a.Prefab.MaxStrength, 100) * a.Prefab.DamageOverlayAlpha; + if (kvp.Value != limbHealths[limb.HealthIndex]) { continue; } + var affliction = kvp.Key; + limb.BurnOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; + limb.DamageOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; } - limb.BurnOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count; - limb.DamageOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count; } } 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 517fcd516..2f1238322 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,27 @@ 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); + } + + if (SubmarineInfo.SavedSubmarines.None(s => s.Name.ToLowerInvariant() == subName.ToLowerInvariant())) + { + ThrowError($"Cannot find a sub that matches the name \"{subName}\"."); + return; + } + + 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 +757,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 +1433,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) @@ -1447,10 +1467,11 @@ namespace Barotrauma { foreach (ItemPrefab ingredientItemPrefab in ingredient.ItemPrefabs) { - NewMessage(" Its ingredient " + ingredientItemPrefab.Name + " has base cost " + ingredientItemPrefab.DefaultPrice.Price); - totalPrice += ingredientItemPrefab.DefaultPrice.Price; + int defaultPrice = ingredientItemPrefab.DefaultPrice?.Price ?? 0; + NewMessage(" Its ingredient " + ingredientItemPrefab.Name + " has base cost " + defaultPrice); + totalPrice += defaultPrice; totalBestPrice += ingredientItemPrefab.GetMinPrice(); - int basePrice = ingredientItemPrefab.DefaultPrice.Price; + int basePrice = defaultPrice; foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) { if (basePrice > ingredientItemLocationPrice.Value.Price) @@ -1616,7 +1637,7 @@ namespace Barotrauma var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == parentItem); int totalValue = 0; - NewMessage(parentItem.Name + " has the price " + parentItem.DefaultPrice.Price); + NewMessage(parentItem.Name + " has the price " + (parentItem.DefaultPrice?.Price ?? 0)); if (fabricationRecipe != null) { NewMessage(" It constructs from:"); @@ -1625,8 +1646,9 @@ namespace Barotrauma { foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs) { - NewMessage(" " + itemPrefab.Name + " has the price " + itemPrefab.DefaultPrice.Price); - totalValue += itemPrefab.DefaultPrice.Price; + int defaultPrice = itemPrefab.DefaultPrice?.Price ?? 0; + NewMessage(" " + itemPrefab.Name + " has the price " + defaultPrice); + totalValue += defaultPrice; } } NewMessage("Its total value was: " + totalValue); @@ -1637,10 +1659,16 @@ namespace Barotrauma { ItemPrefab itemPrefab = (MapEntityPrefab.Find(deconstructItem.ItemIdentifier, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + MapEntityPrefab.Find(null, identifier: deconstructItem.ItemIdentifier, showErrorMessages: false)) as ItemPrefab; + if (itemPrefab == null) + { + ThrowError($" Couldn't find deconstruct product \"{deconstructItem.ItemIdentifier}\"!"); + continue; + } - NewMessage(" " + itemPrefab.Name + " has the price " + itemPrefab.DefaultPrice.Price); - totalValue += itemPrefab.DefaultPrice.Price; + int defaultPrice = itemPrefab.DefaultPrice?.Price ?? 0; + NewMessage(" " + itemPrefab.Name + " has the price " + defaultPrice); + totalValue += defaultPrice; } NewMessage("Its deconstruct value was: " + totalValue); @@ -1792,7 +1820,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 +1885,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); @@ -2457,8 +2499,6 @@ namespace Barotrauma NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green); NewMessage("Fullscreen enabled", Color.Green); - GameSettings.ShowUserStatisticsPrompt = true; - GameSettings.VerboseLogging = false; if (GameMain.Config.MasterServerUrl != "http://www.undertowgames.com/baromaster") @@ -3134,7 +3174,7 @@ namespace Barotrauma { string errorMsg = "Failed to spawn a submarine. Arguments: \"" + string.Join(" ", args) + "\"."; ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); } }, () => 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..6e3ae862d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -363,7 +363,7 @@ namespace Barotrauma OnSecondaryClicked = (_, o) => { if (!(o is Client client)) { return false; } - GameMain.GameSession?.CrewManager?.CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + NetLobbyScreen.CreateModerationContextMenu(client); return true; }, Text = senderName @@ -397,6 +397,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen != null && GameMain.NetworkMember != null) { clickableArea.OnClick = GameMain.NetLobbyScreen.SelectPlayer; + clickableArea.OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu; } msgText.ClickableAreas.Add(clickableArea); } @@ -494,7 +495,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..0ca71144c 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) { @@ -438,7 +438,7 @@ namespace Barotrauma Color.White, Color.Black * 0.5f, 0, SmallFont); DrawString(spriteBatch, new Vector2(10, 40), - $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.FindAll(b => b.Awake && b.Enabled).Count} awake, {GameMain.World.BodyList.FindAll(b => b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled).Count} dynamic)", + $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)", Color.White, Color.Black * 0.5f, 0, SmallFont); if (Screen.Selected.Cam != null) @@ -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..7d932bcc9 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; @@ -63,6 +79,8 @@ namespace Barotrauma public bool AllowMouseWheelScroll { get; set; } = true; + public bool AllowArrowKeyScroll { get; set; } = true; + /// /// Scrolls the list smoothly /// @@ -103,7 +121,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 +150,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 +229,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 +270,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 +394,7 @@ namespace Barotrauma (child.UserData == null && userData == null)) { Select(i, force, autoScroll); - if (!SelectMultiple) return; + if (!SelectMultiple) { return; } } i++; } @@ -363,9 +403,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 +419,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 +487,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 +525,7 @@ namespace Barotrauma { CoroutineManager.StartCoroutine(ScrollCoroutine()); - IEnumerable ScrollCoroutine() + IEnumerable ScrollCoroutine() { if (BarSize >= 1.0f) { @@ -490,68 +549,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 +694,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 +708,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 +795,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 +825,6 @@ namespace Barotrauma } } - public void ForceUpdate() => Update((float)Timing.Step); - protected override void Update(float deltaTime) { if (!Visible) { return; } @@ -805,7 +919,7 @@ namespace Barotrauma } else { - ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; + ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * ScrollBar.UnclampedBarSize; } } @@ -870,6 +984,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 +995,8 @@ namespace Barotrauma if (!wasSelected) { return; } - if (SelectMultiple) + if (CurrentSelectMode == SelectMode.SelectMultiple || + (CurrentSelectMode == SelectMode.RequireShiftToSelectMultiple && PlayerInput.IsCtrlDown())) { if (selected.Contains(child)) { @@ -891,6 +1007,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 +1070,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 +1148,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 +1196,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 +1215,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); @@ -1106,10 +1256,16 @@ namespace Barotrauma switch (key) { case Keys.Down: - SelectNext(); + if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(); } break; case Keys.Up: - SelectPrevious(); + if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + break; + case Keys.Left: + if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(); } + break; + case Keys.Right: + if (isHorizontal && AllowArrowKeyScroll) { SelectNext(); } break; case Keys.Enter: case Keys.Space: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs index 33f42b55d..0fe0f0675 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs @@ -30,7 +30,7 @@ namespace Barotrauma { GameAnalyticsManager.AddErrorEventOnce( "GUIProgressBar.BarSize_setter", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Attempted to set the BarSize of a GUIProgressBar to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -105,7 +105,7 @@ namespace Barotrauma { GameAnalyticsManager.AddErrorEventOnce( "GUIProgressBar.Draw:GetProgress", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "ProgressGetter of a GUIProgressBar (" + ProgressGetter.Target.ToString() + " - " + ProgressGetter.Method.ToString() + ") returned an invalid value (" + newSize + ")\n" + Environment.StackTrace.CleanupStackTrace()); } else 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..6dc1d5f03 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 @@ -275,6 +276,7 @@ namespace Barotrauma public delegate void OnClickDelegate(GUITextBlock textBlock, ClickableArea area); public OnClickDelegate OnClick; + public OnClickDelegate OnSecondaryClick; } public List ClickableAreas { get; private set; } = new List(); @@ -376,6 +378,7 @@ namespace Barotrauma public void SetTextPos() { + cachedCaretPositions = ImmutableArray.Empty; if (text == null) { return; } censoredText = string.IsNullOrEmpty(text) ? "" : new string('\u2022', text.Length); @@ -389,7 +392,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,115 +480,56 @@ 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) { base.Update(deltaTime); - if (ClickableAreas.Any() && (GUI.MouseOn?.IsParentOf(this) ?? true)) + if (ClickableAreas.Any() && ((GUI.MouseOn?.IsParentOf(this) ?? true) || GUI.MouseOn == this)) { if (!Rect.Contains(PlayerInput.MousePosition)) { return; } int index = GetCaretIndexFromScreenPos(PlayerInput.MousePosition); @@ -598,6 +542,10 @@ namespace Barotrauma { clickableArea.OnClick?.Invoke(this, clickableArea); } + if (PlayerInput.SecondaryMouseButtonClicked()) + { + clickableArea.OnSecondaryClick?.Invoke(this, clickableArea); + } break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index ed8d20768..f3e9a7513 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -54,7 +54,7 @@ namespace Barotrauma private int? maxTextLength; private int _caretIndex; - private int CaretIndex + public int CaretIndex { get { return _caretIndex; } set @@ -353,34 +353,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; } @@ -393,6 +382,7 @@ namespace Barotrauma memento.Store(Text); } CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; + CalculateCaretPos(); ClearSelection(); selected = true; GUI.KeyboardDispatcher.Subscriber = this; @@ -548,59 +538,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 { @@ -738,8 +706,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(); @@ -749,8 +724,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(); @@ -813,6 +795,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..a5554ebf2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -562,7 +562,7 @@ namespace Barotrauma frame.OnSecondaryClicked += (component, data) => { - GameMain.GameSession?.CrewManager?.CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + NetLobbyScreen.CreateModerationContextMenu(client); return true; }; @@ -917,7 +917,8 @@ namespace Barotrauma textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() { Data = data, - OnClick = GameMain.NetLobbyScreen.SelectPlayer + OnClick = GameMain.NetLobbyScreen.SelectPlayer, + OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu }); } } @@ -1486,12 +1487,13 @@ namespace Barotrauma experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: GUI.Style.Green) { - IsHorizontal = true + IsHorizontal = true, }; experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUI.Font, textAlignment: Alignment.CenterRight) { - Shadow = true + Shadow = true, + ToolTip = TextManager.Get("experiencetooltip") }; talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUI.SubHeadingFont, parseRichText: true, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; @@ -1541,13 +1543,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 +1585,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}); @@ -1611,6 +1645,7 @@ namespace Barotrauma GameMain.Client.CreateEntityEvent(controlledCharacter, new object[] { NetEntityEvent.Type.UpdateTalents }); } } + selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); UpdateTalentButtons(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 9f9234a7e..1994a8d59 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -846,23 +846,27 @@ namespace Barotrauma var currentOrPending = item.PendingItemSwap ?? item.Prefab; string name = currentOrPending.Name; - string quantityText = ""; + string nameWithQuantity = ""; if (linkedItems.Count > 1) { foreach (ItemPrefab distinctItem in linkedItems.Select(it => it.Prefab).Distinct()) { - if (quantityText != string.Empty) + if (nameWithQuantity != string.Empty) { - quantityText += ", "; + nameWithQuantity += ", "; } int count = linkedItems.Count(it => it.Prefab == distinctItem); - quantityText += distinctItem.Name; + nameWithQuantity += distinctItem.Name; if (count > 1) { - quantityText += " " + TextManager.GetWithVariable("campaignstore.quantity", "[amount]", count.ToString()); + nameWithQuantity += " " + TextManager.GetWithVariable("campaignstore.quantity", "[amount]", count.ToString()); } } } + else + { + nameWithQuantity = name; + } bool isOpen = false; GUIButton toggleButton = new GUIButton(rectT(1f, 0.1f, parent.Content), text: string.Empty, style: "SlideDown") @@ -884,7 +888,7 @@ namespace Barotrauma new GUITextBlock(rectT(0.3f, 1f, buttonLayout), text: slotText, font: GUI.SubHeadingFont); GUILayoutGroup group = new GUILayoutGroup(rectT(0.7f, 1f, buttonLayout), isHorizontal: true) { Stretch = true }; - string title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : quantityText; + string title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : nameWithQuantity; GUITextBlock text = new GUITextBlock(rectT(0.7f, 1f, group), text: title, font: GUI.SubHeadingFont, textAlignment: Alignment.Right, parseRichText: true) { TextColor = GUI.Style.Orange @@ -907,7 +911,7 @@ namespace Barotrauma if (isUninstallPending) { canUninstall = false; } frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, - item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", quantityText), + item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), currentOrPending.Description, 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs new file mode 100644 index 000000000..b8663977e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Barotrauma.IO; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public static partial class GameAnalyticsManager + { + static partial void CreateConsentPrompt() + { + if (consentTextAvailable) + { + var background = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); + var frame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), background.RectTransform, Anchor.Center) { MaxSize = new Point(800, int.MaxValue) }); + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), frame.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(15) + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsentheader"), font: GUI.SubHeadingFont, textColor: Color.White); + var mainText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsenttext"), wrap: true, parseRichText: true); + + foreach (var data in mainText.RichTextData) + { + mainText.ClickableAreas.Add(new GUITextBlock.ClickableArea() + { + Data = data, + OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) => + { + GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/"); + } + }); + } + + string privacyPolicyText = File.ReadAllText("daedalic_privacypolicy.txt"); + var privacyPolicyBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(200)) }); + var privacyPolicy = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), privacyPolicyBox.Content.RectTransform), privacyPolicyText, wrap: true) + { + CanBeFocused = false + }; + privacyPolicy.RectTransform.MinSize = new Point(0, (int)privacyPolicy.TextSize.Y); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsentstatement"), wrap: true); + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), isHorizontal: true); + + void buttonContainerSpacing(float width) + => new GUIFrame(new RectTransform(new Vector2(width, 1.0f), buttonContainer.RectTransform), style: null); + + buttonContainerSpacing(0.1f); + var yesBtn = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), TextManager.Get("Yes")); + yesBtn.OnClicked += (btn, userdata) => + { + GUIMessageBox.MessageBoxes.Remove(background); + SetConsentInternal(Consent.Yes); + return true; + }; + yesBtn.Enabled = false; + + IEnumerable enableAfterTime(WaitForSeconds time, params GUIComponent[] components) + { + yield return time; + foreach (var c in components) + { + c.Enabled = true; + } + yield return CoroutineStatus.Success; + } + + buttonContainerSpacing(0.2f); + + var noBtn = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), TextManager.Get("No")); + noBtn.OnClicked += (btn, userdata) => + { + GUIMessageBox.MessageBoxes.Remove(background); + SetConsent(Consent.No); + return true; + }; + noBtn.Enabled = false; + + CoroutineManager.StartCoroutine(enableAfterTime(new WaitForSeconds(0.3f), yesBtn, noBtn)); + + buttonContainerSpacing(0.1f); + + buttonContainer.RectTransform.MinSize = new Point(0, yesBtn.RectTransform.MinSize.Y); + buttonContainer.RectTransform.MaxSize = new Point(int.MaxValue, yesBtn.RectTransform.MinSize.Y); + + foreach (var child in content.Children) + { + if (child is GUITextBlock textBlock) + { + textBlock.RectTransform.MinSize = new Point(0, (int)textBlock.TextSize.Y); + textBlock.RectTransform.MaxSize = new Point(int.MaxValue, (int)textBlock.TextSize.Y + GUI.IntScale(15)); + } + } + + frame.RectTransform.MaxSize = new Point( + frame.RectTransform.MaxSize.X, + (int)(content.Children.Sum(c => c.RectTransform.MaxSize.Y + content.AbsoluteSpacing) / content.RectTransform.RelativeSize.Y)); + + GUIMessageBox.MessageBoxes.Add(background); + } + else + { + //user statistics disabled by default if the prompt cannot be shown in the user's language + SetConsent(Consent.Unknown); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index dfb871e76..006f4a531 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -11,7 +11,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using GameAnalyticsSDK.Net; using Barotrauma.IO; using System.Threading; using Barotrauma.Tutorials; @@ -388,51 +387,6 @@ namespace Barotrauma loadingCoroutine = CoroutineManager.StartCoroutine(Load(canLoadInSeparateThread), "Load", canLoadInSeparateThread); } - private void InitUserStats() - { - return; - - if (GameSettings.ShowUserStatisticsPrompt) - { - if (TextManager.ContainsTag("statisticspromptheader") && TextManager.ContainsTag("statisticsprompttext")) - { - var userStatsPrompt = new GUIMessageBox( - TextManager.Get("statisticspromptheader"), - TextManager.Get("statisticsprompttext"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - userStatsPrompt.Buttons[0].OnClicked += (btn, userdata) => - { - GameSettings.ShowUserStatisticsPrompt = false; - GameSettings.SendUserStatistics = true; - GameAnalyticsManager.Init(); - Config.SaveNewPlayerConfig(); - return true; - }; - userStatsPrompt.Buttons[0].OnClicked += userStatsPrompt.Close; - userStatsPrompt.Buttons[1].OnClicked += (btn, userdata) => - { - GameSettings.ShowUserStatisticsPrompt = false; - GameSettings.SendUserStatistics = false; - Config.SaveNewPlayerConfig(); - return true; - }; - userStatsPrompt.Buttons[1].OnClicked += userStatsPrompt.Close; - } - else - { - //user statistics enabled by default if the prompt cannot be shown in the user's language - GameSettings.ShowUserStatisticsPrompt = false; - GameSettings.SendUserStatistics = true; - GameAnalyticsManager.Init(); - Config.SaveNewPlayerConfig(); - } - } - else if (GameSettings.SendUserStatistics) - { - GameAnalyticsManager.Init(); - } - } - public class LoadingException : Exception { public LoadingException(Exception e) : base("Loading was interrupted due to an error.", innerException: e) @@ -440,7 +394,7 @@ namespace Barotrauma } } - private IEnumerable Load(bool isSeparateThread) + private IEnumerable Load(bool isSeparateThread) { if (GameSettings.VerboseLogging) { @@ -526,21 +480,17 @@ namespace Barotrauma DebugConsole.Log("Selected content packages: " + string.Join(", ", Config.AllEnabledPackages.Select(cp => cp.Name))); } -#if DEBUG - GameSettings.ShowUserStatisticsPrompt = false; - GameSettings.SendUserStatistics = false; +#if !DEBUG + GameAnalyticsManager.InitIfConsented(); #endif - InitUserStats(); - - yield return CoroutineStatus.Running; + yield return CoroutineStatus.Running; 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++; @@ -846,6 +796,8 @@ namespace Barotrauma } #endif + NetworkMember?.Update((float)Timing.Step); + if (!hasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) { throw new LoadingException(loadingCoroutine.Exception); @@ -920,6 +872,11 @@ namespace Barotrauma { gameSession.ToggleTabMenu(); } + else if (GUIMessageBox.VisibleBox as GUIMessageBox != null && + GUIMessageBox.VisibleBox.UserData as string == "bugreporter") + { + ((GUIMessageBox)GUIMessageBox.VisibleBox).Close(); + } else if (GUI.PauseMenuOpen) { GUI.TogglePauseMenu(); @@ -1131,6 +1088,10 @@ namespace Barotrauma if (GameSession != null) { + double roundDuration = Timing.TotalTime - GameSession.RoundStartTime; + GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, + GameSession.GameMode?.Name ?? "none", + roundDuration); if (Tutorial.Initialized) { ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); @@ -1139,6 +1100,7 @@ namespace Barotrauma GUIMessageBox.CloseAll(); MainMenuScreen.Select(); GameSession = null; + } public void ShowCampaignDisclaimer(Action onContinue = null) @@ -1238,7 +1200,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; @@ -1262,8 +1224,8 @@ namespace Barotrauma DebugConsole.ThrowError("Error while cleaning unnecessary save files", e); } - if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } - if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); } + if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } + 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..43f30d3b4 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, @@ -330,7 +330,7 @@ namespace Barotrauma if (data == null) { return false; } if (GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data) is Client client) { - CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + NetLobbyScreen.CreateModerationContextMenu(client); return true; } return 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); @@ -777,7 +789,7 @@ namespace Barotrauma } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item as ISpatialEntity, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item, character, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -956,7 +968,8 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var orderInfo = (OrderInfo)userData; - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + int priority = GetManualOrderPriority(character, orderInfo.Order); + SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); return true; }, OnSecondaryClicked = (button, userData) => @@ -1141,105 +1154,6 @@ namespace Barotrauma } } - #region Context Menu - - public void CreateModerationContextMenu(Point mousePos, Client client) - { - if (GUIContextMenu.CurrentContextMenu != null) { return; } - if (IsSinglePlayer || client == null || ((!GameMain.Client?.PreviouslyConnectedClients?.Contains(client)) ?? true)) { return; } - - - bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, - canKick = GameMain.Client.HasPermission(ClientPermissions.Kick), - canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking, - canPromo = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); - - // Disable options if we are targeting ourselves - if (client.ID == GameMain.Client?.ID) - { - canKick = canBan = canPromo = false; - } - - List options = new List(); - - options.Add(new ContextMenuOption("ViewSteamProfile", isEnabled: hasSteam, onSelected: delegate - { - Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); - })); - - options.Add(new ContextMenuOption("ModerationMenu.UserDetails", isEnabled: true, onSelected: delegate - { - GameMain.NetLobbyScreen?.SelectPlayer(client); - })); - - - // Creates sub context menu options for all the ranks - List permissionOptions = new List(); - foreach (PermissionPreset rank in PermissionPreset.List) - { - permissionOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => - { - string label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, rank.Name }); - GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - - msgBox.Buttons[0].OnClicked = delegate - { - client.SetPermissions(rank.Permissions, rank.PermittedCommands); - GameMain.Client.UpdateClientPermissions(client); - msgBox.Close(); - return true; - }; - msgBox.Buttons[1].OnClicked = delegate - { - msgBox.Close(); - return true; - }; - }) { Tooltip = rank.Description }); - } - - options.Add(new ContextMenuOption("Permissions", isEnabled: canPromo, options: permissionOptions.ToArray())); - - Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White; - - if (GameMain.Client.ConnectedClients.Contains(client)) - { - options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.ID != GameMain.Client?.ID, onSelected: delegate - { - client.MutedLocally = !client.MutedLocally; - })); - - bool kickEnabled = client.ID != GameMain.Client?.ID && client.AllowKicking; - - // if the user can kick create a kick option else create the votekick option - ContextMenuOption kickOption; - if (canKick) - { - kickOption = new ContextMenuOption("Kick", isEnabled: kickEnabled, onSelected: delegate - { - GameMain.Client?.CreateKickReasonPrompt(client.Name, false); - }); - } - else - { - kickOption = new ContextMenuOption("VoteToKick", isEnabled: kickEnabled, onSelected: delegate - { - GameMain.Client?.VoteForKick(client); - }); - } - - options.Add(kickOption); - } - - options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate - { - GameMain.Client?.CreateKickReasonPrompt(client.Name, true); - })); - - GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray()); - } - - #endregion - public void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } @@ -1648,7 +1562,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); } @@ -1890,7 +1804,7 @@ namespace Barotrauma private Hull hullContext; private WallSection wallContext; private bool isContextual; - private readonly List contextualOrders = new List(); + private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; private const int maxShortcutNodeCount = 4; @@ -2593,7 +2507,7 @@ namespace Barotrauma { order = orders[i]; disableNode = !CanCharacterBeHeard() || - (order.MustSetTarget && (order.ItemComponentType != null || order.TargetItems.Length > 0) && + (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); optionNodes.Add(new Tuple( CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), @@ -2609,7 +2523,6 @@ namespace Barotrauma if (contextualOrders.None()) { string orderIdentifier; - // Check if targeting an item or a hull if (itemContext != null && itemContext.IsPlayerTeamInteractable) { @@ -2618,84 +2531,89 @@ namespace Barotrauma { targetComponent = null; if (p.UseController && itemContext.Components.None(c => c is Controller)) { continue; } - if ((p.TargetItems.Length > 0 && (p.TargetItems.Contains(itemContext.Prefab.Identifier) || itemContext.HasTag(p.TargetItems))) || - p.TryGetTargetItemComponent(itemContext, out targetComponent)) + if (p.HasOptionSpecificTargetItems) { - contextualOrders.Add(p.HasOptions ? p : new Order(p, itemContext, targetComponent, Character.Controlled)); + foreach (string option in p.Options) + { + if (p.TargetItemsMatchItem(itemContext, option)) + { + contextualOrders.Add(new OrderInfo(new Order(p, itemContext, targetComponent, Character.Controlled), option)); + } + } + } + else if (p.TargetItemsMatchItem(itemContext) || p.TryGetTargetItemComponent(itemContext, out targetComponent)) + { + contextualOrders.Add(new OrderInfo(p.HasOptions ? p : new Order(p, itemContext, targetComponent, Character.Controlled), null)); } } - // If targeting a periscope connected to a turret, show the 'operateweapons' order orderIdentifier = "operateweapons"; var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) + if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) { - var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems)) ?? - itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems)); - if (turret != null) { contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } + var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) ?? + itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)); + if (turret != null) + { + contextualOrders.Add(new OrderInfo(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled), null)); + } } - // 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(info => info.Order.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")))) { - contextualOrders.Add(new Order(Order.GetPrefab("repairelectrical"), itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("repairelectrical"), itemContext, targetItem: null, Character.Controlled), null)); } else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { - contextualOrders.Add(new Order(Order.GetPrefab("repairmechanical"), itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("repairmechanical"), itemContext, targetItem: null, Character.Controlled), null)); } else { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); } } - // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) orderIdentifier = "pumpwater"; - if (contextualOrders.FirstOrDefault(o => o.Identifier.Equals(orderIdentifier)) is Order o && - itemContext.Components.FirstOrDefault(c => c.GetType() == o.ItemComponentType) is Pump pump) + if (contextualOrders.FirstOrDefault(info => info.Order.Identifier.Equals(orderIdentifier)) is OrderInfo pumpOrderInfo && pumpOrderInfo.Order is Order pumpOrder && + itemContext.Components.FirstOrDefault(c => c.GetType() == pumpOrder.ItemComponentType) is Pump pump && pump.IsAutoControlled) { - if (pump.IsAutoControlled) { contextualOrders.Remove(o); } + contextualOrders.Remove(pumpOrderInfo); } - if (contextualOrders.None()) { orderIdentifier = "cleanupitems"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) { if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); } } } - AddIgnoreOrder(itemContext); } else if (hullContext != null) { - contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, targetItem: null, Character.Controlled)); - + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("fixleaks"), hullContext, targetItem: null, Character.Controlled), null)); if (wallContext != null) { AddIgnoreOrder(wallContext); } } - void AddIgnoreOrder(IIgnorable target) { var orderIdentifier = "ignorethis"; - if (!target.OrderedToBeIgnored && contextualOrders.None(o => o.Identifier == orderIdentifier)) + if (!target.OrderedToBeIgnored && contextualOrders.None(info => info.Order.Identifier == orderIdentifier)) { AddOrder(); } else { orderIdentifier = "unignorethis"; - if (target.OrderedToBeIgnored && contextualOrders.None(o => o.Identifier == orderIdentifier)) + if (target.OrderedToBeIgnored && contextualOrders.None(info => info.Order.Identifier == orderIdentifier)) { AddOrder(); } @@ -2705,64 +2623,61 @@ namespace Barotrauma { if (target is WallSection ws) { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled)); + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled), null)); } else { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), target as Entity, null, Character.Controlled)); + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), target as Entity, null, Character.Controlled), null)); } } } - orderIdentifier = "wait"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) { Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), new OrderTarget(position, hull), Character.Controlled)); + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), new OrderTarget(position, hull), Character.Controlled), null)); } - - if (contextualOrders.None(o => o.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) + if (contextualOrders.None(info => info.Order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) { orderIdentifier = "follow"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) { - contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); } } - // Show 'dismiss' order only when there are crew members with active orders orderIdentifier = "dismissed"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) + if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) { - contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); } } - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); bool disableNode = !CanCharacterBeHeard(); for (int i = 0; i < contextualOrders.Count; i++) { - optionNodes.Add(new Tuple( - CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), contextualOrders[i], (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), - !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); + var info = contextualOrders[i]; + int hotkey = (i + 1) % 10; + var component = string.IsNullOrEmpty(info.OrderOption) ? + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), info.Order, hotkey, disableNode: disableNode, checkIfOrderCanBeHeard: false) : + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), info.Order, info.OrderOption, info.Order.Prefab.GetOptionName(info.OrderOption), hotkey); + optionNodes.Add(new Tuple(component, !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } // TODO: there's duplicate logic here and above -> would be better to refactor so that the conditions are only defined in one place public static bool DoesItemHaveContextualOrders(Item item) { - if (Order.PrefabList.Any(o => o.TargetItems.Length > 0 && o.TargetItems.Contains(item.Prefab.Identifier))) { return true; } - if (Order.PrefabList.Any(o => item.HasTag(o.TargetItems))) { return true; } + if (Order.PrefabList.Any(o => o.TargetItemsMatchItem(item))) { return true; } if (Order.PrefabList.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } 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; } - 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))); + if (Order.GetPrefab("loaditems") is Order loadItemsPrefab && AIObjectiveLoadItems.IsValidTarget(item, Character.Controlled, targetContainerTags: loadItemsPrefab.GetTargetItems())) { return true; } + if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { return true; } + return Order.GetPrefab("operateweapons") is Order operateWeaponsPrefab && item.Components.Any(c => c is Controller) && + (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) || + item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item))); } /// Use a negative value (e.g. -1) if there should be no hotkey associated with the node @@ -2781,7 +2696,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 +2719,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) @@ -2819,7 +2734,8 @@ namespace Barotrauma o = new Order(o.Prefab, orderTargetEntity, orderTargetEntity.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: order.OrderGiver); } var character = !o.TargetAllCharacters ? characterContext ?? GetCharacterForQuickAssignment(o) : null; - SetCharacterOrder(character, o, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + int priority = GetManualOrderPriority(character, o); + SetCharacterOrder(character, o, null, priority, Character.Controlled); DisableCommandUI(); } return true; @@ -2925,9 +2841,15 @@ 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); + var character = characterContext ?? GetCharacterForQuickAssignment(o.Item1); + int priority = GetManualOrderPriority(character, o.Item1); + SetCharacterOrder(character, o.Item1, o.Item2, priority, Character.Controlled); DisableCommandUI(); } return true; @@ -2987,12 +2909,21 @@ 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 + { + var character = characterContext ?? GetCharacterForQuickAssignment(o.Item1); + int priority = GetManualOrderPriority(character, o.Item1); + SetCharacterOrder(character, o.Item1, o.Item2, priority, Character.Controlled); + DisableCommandUI(); + } return true; } }; @@ -3195,7 +3126,9 @@ namespace Barotrauma OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } - SetCharacterOrder(userData as Character, order.Item1, order.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + var character = userData as Character; + int priority = GetManualOrderPriority(character, order.Item1); + SetCharacterOrder(character, order.Item1, order.Item2, priority, Character.Controlled); DisableCommandUI(); return true; } @@ -3473,6 +3406,11 @@ namespace Barotrauma return order.Name; } + private int GetManualOrderPriority(Character character, Order order) + { + return character?.Info?.GetManualOrderPriority(order) ?? CharacterInfo.HighestManualOrderPriority; + } + #region Crew Member Assignment Logic private bool CanOpenManualAssignment(GUIComponent node) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index e7bd89023..95d8d567e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -65,7 +65,7 @@ namespace Barotrauma public override void ShowStartMessage() { - foreach (Mission mission in Missions) + foreach (Mission mission in Missions.ToList()) { new GUIMessageBox( mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 32605d848..a0876e3da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -60,7 +60,7 @@ namespace Barotrauma var newCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); var loadCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); - GameMain.NetLobbyScreen.CampaignSetupUI = new MultiPlayerCampaignSetupUI(newCampaignContainer, loadCampaignContainer, null, saveFiles); + GameMain.NetLobbyScreen.CampaignSetupUI = new MultiPlayerCampaignSetupUI(newCampaignContainer, loadCampaignContainer, saveFiles); var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform), TextManager.Get("NewCampaign"), style: "GUITabButton") @@ -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..3a22e29d8 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) || @@ -475,12 +475,12 @@ namespace Barotrauma ItemComponent targetItem = null; if (orderPrefab.MustSetTarget) { - targetEntity = orderPrefab.GetMatchingItems(true, interactableFor: Character.Controlled).FirstOrDefault(); + targetEntity = orderPrefab.GetMatchingItems(true, interactableFor: Character.Controlled, orderOption: orderInfo.option).FirstOrDefault(); if (targetEntity == null) { return; } targetItem = orderPrefab.GetTargetItemComponent(targetEntity); } var order = new Order(orderPrefab, targetEntity as Entity, targetItem, orderGiver: Character.Controlled); - GameMain.GameSession.CrewManager.SetCharacterOrder(Character.Controlled, order, orderInfo.option, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + GameMain.GameSession?.CrewManager?.SetCharacterOrder(Character.Controlled, order, orderInfo.option, CharacterInfo.HighestManualOrderPriority, Character.Controlled); }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index e69e11bae..f0db4ea5b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -635,9 +635,9 @@ namespace Barotrauma } else if (characterInfo.CauseOfDeath.Type == CauseOfDeathType.Affliction && characterInfo.CauseOfDeath.Affliction == null) { - string errorMsg = "Character \"" + characterInfo.Name + "\" had an invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified)."; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = "Character \"[name]\" had an invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified)."; + DebugConsole.ThrowError(errorMsg.Replace("[name]", characterInfo.Name)); + GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", characterInfo.SpeciesName)); statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); statusColor = GUI.Style.Red; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 49b528e7d..1d255522b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -1,12 +1,10 @@ -using Barotrauma.Extensions; -using Barotrauma.Networking; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using OpenAL; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Xml.Linq; @@ -365,6 +363,12 @@ namespace Barotrauma TextManager.Get("Settings"), textAlignment: Alignment.TopLeft, font: GUI.LargeFont) { ForceUpperCase = true }; + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), settingsTitle.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), style: "GUIBugButton") + { + ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})", + OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; } + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), TextManager.Get("ContentPackages"), font: GUI.SubHeadingFont); var corePackageDropdown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform)) @@ -480,7 +484,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; @@ -509,17 +513,56 @@ namespace Barotrauma ApplySettings(); GameMain.Instance.Exit(); return true; - }; msgBox.Buttons[1].OnClicked += (btn, userdata) => + }; + msgBox.Buttons[1].OnClicked += (btn, userdata) => { Language = prevLanguage; languageDD.SelectItem(Language); msgBox.Close(); return true; }; - return true; }; + var statisticsTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform), TextManager.Get("statisticsconsenttickbox")) + { + OnSelected = (GUITickBox tickBox) => + { + GameAnalyticsManager.SetConsent( + tickBox.Selected + ? GameAnalyticsManager.Consent.Ask + : GameAnalyticsManager.Consent.No); + return false; + } + }; +#if DEBUG + statisticsTickBox.Enabled = false; +#endif + void updateGATickBoxToolTip() + => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); + updateGATickBoxToolTip(); + + var cachedConsent = GameAnalyticsManager.Consent.Unknown; + var statisticsTickBoxUpdater = new GUICustomComponent( + new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform), + onUpdate: (deltaTime, component) => + { + bool shouldTickBoxBeSelected = GameAnalyticsManager.UserConsented == GameAnalyticsManager.Consent.Yes; + + bool shouldUpdateTickBoxState = cachedConsent != GameAnalyticsManager.UserConsented + || statisticsTickBox.Selected != shouldTickBoxBeSelected; + + if (!shouldUpdateTickBoxState) { return; } + + updateGATickBoxToolTip(); + cachedConsent = GameAnalyticsManager.UserConsented; + GUITickBox.OnSelectedHandler prevHandler = statisticsTickBox.OnSelected; + statisticsTickBox.OnSelected = null; + statisticsTickBox.Selected = shouldTickBoxBeSelected; + statisticsTickBox.OnSelected = prevHandler; + statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; + }); + // right panel -------------------------------------- var rightPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.99f - leftPanel.RectTransform.RelativeSize.X, leftPanel.RectTransform.RelativeSize.Y), @@ -556,13 +599,6 @@ namespace Barotrauma tabButtons[(int)tab].Text = ToolBox.LimitString(buttonText, tabButtons[(int)tab].Font, (int)(0.75f * tabWidth * tabButtonHolder.Rect.Width)); } - new GUIButton(new RectTransform(new Vector2(0.05f, 0.75f), tabButtonHolder.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.0f, 0.2f) }, style: "GUIBugButton") - { - ToolTip = TextManager.Get("bugreportbutton"), - OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; } - }; - - /// Graphics tab -------------------------------------------------------------- var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Graphics].RectTransform, Anchor.TopLeft) @@ -1168,7 +1204,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Failed to set voice capture mode.", e); - GameAnalyticsManager.AddErrorEventOnce("SetVoiceCaptureMode", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "Failed to set voice capture mode. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.AddErrorEventOnce("SetVoiceCaptureMode", GameAnalyticsManager.ErrorSeverity.Error, "Failed to set voice capture mode. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); VoiceSetting = VoiceMode.Disabled; } @@ -1767,7 +1803,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..f42e06aa0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -406,7 +406,7 @@ namespace Barotrauma.Items.Components DebugConsole.Log("Invalid sound volume (item " + item.Name + ", " + GetType().ToString() + "): " + newVolume); GameAnalyticsManager.AddErrorEventOnce( "ItemComponent.PlaySound:" + item.Name + GetType().ToString(), - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Invalid sound volume (item " + item.Name + ", " + GetType().ToString() + "): " + newVolume); return 0.0f; } @@ -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/Ladder.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs index 08958f682..52f925579 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml.Linq; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -11,7 +8,7 @@ namespace Barotrauma.Items.Components { public float BackgroundSpriteDepth { - get { return item.GetDrawDepth() + 0.1f; } + get { return item.GetDrawDepth() + 0.05f; } } public Vector2 DrawSize 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..1f2d6882f 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; @@ -637,44 +672,61 @@ namespace Barotrauma.Items.Components return; } - if (currentMode == MiniMapMode.HullStatus) + if (currentMode == MiniMapMode.HullStatus && item.Submarine != null) { Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; - if (item.Submarine != null) + var sprite = GUI.Style.UIGlowSolidCircular?.Sprite; + float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; + if (sprite != null) { - var sprite = GUI.Style.UIGlowSolidCircular?.Sprite; - float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; - if (sprite != null) + Vector2 spriteSize = sprite.size; + Rectangle worldBorders = item.Submarine.GetDockedBorders(); + worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); + foreach (Gap gap in Gap.GapList) { - Vector2 spriteSize = sprite.size; - Rectangle worldBorders = item.Submarine.GetDockedBorders(); - worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); - foreach (Gap gap in Gap.GapList) - { - if (gap.IsRoomToRoom || gap.Submarine != item.Submarine || gap.ConnectedDoor != null) { continue; } - RectangleF entityRect = ScaleRectToUI(gap, miniMapFrame.Rect, worldBorders); + if (gap.IsRoomToRoom || gap.linkedTo.Count == 0 || 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; + Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f; - Color color = ToolBox.GradientLerp(gap.Open, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorLow) * alpha; - sprite.Draw(spriteBatch, - miniMapFrame.Rect.Location.ToVector2() + entityRect.Center, - color, origin: sprite.Origin, rotate: 0.0f, scale: scale); - } + Color color = ToolBox.GradientLerp(gap.Open, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorLow) * alpha; + sprite.Draw(spriteBatch, + miniMapFrame.Rect.Location.ToVector2() + entityRect.Center, + color, origin: sprite.Origin, rotate: 0.0f, scale: scale); } - } + } - 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); + } } } @@ -936,15 +988,47 @@ namespace Barotrauma.Items.Components continue; } - hullData.HullOxygenAmount = RequireOxygenDetectors ? hullData.ReceivedOxygenAmount : hull.OxygenPercentage; - hullData.HullWaterAmount = RequireWaterDetectors ? hullData.ReceivedWaterAmount : Math.Min(hull.WaterVolume / hull.Volume, 1.0f); + if (RequireOxygenDetectors) + { + hullData.HullOxygenAmount = hullData.ReceivedOxygenAmount; + } + else if (hullData.LinkedHulls.Any()) + { + hullData.HullOxygenAmount = 0.0f; + foreach (Hull linkedHull in hullData.LinkedHulls) + { + hullData.HullOxygenAmount += linkedHull.OxygenPercentage; + } + hullData.HullOxygenAmount /= hullData.LinkedHulls.Count; + } + else + { + hullData.HullOxygenAmount = hull.OxygenPercentage; + } + if (RequireWaterDetectors) + { + hullData.HullWaterAmount = hullData.ReceivedWaterAmount; + } + else if (hullData.LinkedHulls.Any()) + { + hullData.HullWaterAmount = 0.0f; + foreach (Hull linkedHull in hullData.LinkedHulls) + { + hullData.HullWaterAmount += Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); + } + hullData.HullWaterAmount /= hullData.LinkedHulls.Count; + } + else + { + hullData.HullWaterAmount = Math.Min(hull.WaterVolume / hull.Volume, 1.0f); + } float gapOpenSum = 0.0f; 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)); } @@ -958,15 +1042,6 @@ namespace Barotrauma.Items.Components float? oxygenAmount = hullData.HullOxygenAmount, waterAmount = hullData.HullWaterAmount; - foreach (Hull linkedHull in hullData.LinkedHulls) - { - oxygenAmount += linkedHull.OxygenPercentage; - waterAmount += Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); - } - - oxygenAmount /= (hullData.LinkedHulls.Count + 1); - waterAmount /= (hullData.LinkedHulls.Count + 1); - string line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; Color line1Color = GUI.Style.Red; @@ -1039,10 +1114,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()); } @@ -1057,10 +1131,9 @@ namespace Barotrauma.Items.Components private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container) { - if (item.Submarine != null) - { - DrawSubmarine(spriteBatch); - } + if (item.Submarine == null) { return; } + + DrawSubmarine(spriteBatch); if (Voltage < MinVoltage) { return; } Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; @@ -1086,38 +1159,42 @@ 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 (!RequireWaterDetectors) { waterAmount = hull.WaterPercentage / 100.0f; } + if (hullFrame.Rect.Height * 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); + 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 +1298,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; @@ -1361,7 +1438,7 @@ namespace Barotrauma.Items.Components { if (linkedEntity is Hull linkedHull) { - if (linkedHulls.Contains(linkedHull)) { continue; } + if (linkedHulls.Contains(linkedHull) || linkedHull.HiddenInGame) { continue; } linkedHulls.Add(linkedHull); GetLinkedHulls(linkedHull, linkedHulls); } @@ -1541,7 +1618,7 @@ namespace Barotrauma.Items.Components bool IsPartofSub(MapEntity entity) { - if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine)) { return false; } + if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.HiddenInGame) { return false; } return !settings.IgnoreOutposts || sub.IsEntityFoundOnThisSub(entity, true); } 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..d8831b9db 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); @@ -1623,6 +1623,11 @@ namespace Barotrauma.Items.Components markerDistances.Add(targetIdentifier, cachedDistance); dist = path.TotalLength; } + else + { + var cachedDistance = new CachedDistance(transducerPosition, worldPosition, linearDist, Timing.TotalTime + Rand.Range(4.0f, 7.0f)); + markerDistances.Add(targetIdentifier, cachedDistance); + } } Vector2 position = worldPosition - transducerPosition; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index fbf133ced..2c2e02a2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -35,6 +35,13 @@ namespace Barotrauma.Items.Components private FixActions requestStartFixAction; + private bool qteSuccess; + + private float qteTimer; + private const float QteDuration = 0.5f; + private float qteCooldown; + private const float QteCooldownDuration = 0.5f; + public float FakeBrokenTimer; [Serialize("", false, description: "An optional description of the needed repairs displayed in the repair interface.")] @@ -55,14 +62,13 @@ namespace Barotrauma.Items.Components if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) { return false; } if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } - float maxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(character); - if (item.Condition / maxRepairConditionMultiplier < RepairThreshold) { return true; } + float defaultMaxCondition = item.MaxCondition / item.MaxRepairConditionMultiplier; + + if (MathUtils.Percentage(item.Condition, defaultMaxCondition) < RepairThreshold) { return true; } if (CurrentFixer == character) { - float condition = item.Condition / item.MaxRepairConditionMultiplier; - float maxCondition = item.MaxCondition / item.MaxRepairConditionMultiplier; - if (condition < maxCondition * maxRepairConditionMultiplier) + if (item.Condition < item.MaxCondition) { return true; } @@ -143,11 +149,14 @@ namespace Barotrauma.Items.Components progressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), progressBarHolder.RectTransform), color: GUI.Style.Green, barSize: 0.0f, style: "DeviceProgressBar"); + progressBarOverlayText = new GUITextBlock(new RectTransform(Vector2.One, progressBar.RectTransform), string.Empty, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) { IgnoreLayoutGroups = true }; + qteTimer = QteDuration; + repairButtonText = TextManager.Get("RepairButton"); repairingText = TextManager.Get("Repairing"); RepairButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), progressBarHolder.RectTransform, Anchor.TopCenter), repairButtonText) @@ -157,6 +166,11 @@ namespace Barotrauma.Items.Components requestStartFixAction = FixActions.Repair; item.CreateClientEvent(this); return true; + }, + OnButtonDown = () => + { + QTEAction(); + return true; } }; RepairButton.TextBlock.AutoScaleHorizontal = true; @@ -181,6 +195,11 @@ namespace Barotrauma.Items.Components requestStartFixAction = FixActions.Sabotage; item.CreateClientEvent(this); return true; + }, + OnButtonDown = () => + { + QTEAction(); + return true; } }; @@ -251,6 +270,20 @@ namespace Barotrauma.Items.Components { repairSoundChannel = SoundPlayer.PlaySound("repair", item.WorldPosition, hullGuess: item.CurrentHull); } + + if (qteCooldown > 0.0f) + { + qteCooldown -= deltaTime; + if (qteCooldown <= 0.0f) + { + qteTimer = QteDuration; + } + } + else + { + qteTimer -= deltaTime * (qteTimer / QteDuration); + if (qteTimer < 0.0f) { qteTimer = QteDuration; } + } } else { @@ -268,6 +301,26 @@ namespace Barotrauma.Items.Components progressBar.BarSize = item.Condition / defaultMaxCondition; progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green); + Rectangle sliderRect = progressBar.GetSliderRect(1.0f); + Color qteSliderColor = Color.White; + if (qteCooldown > 0.0f) + { + qteSliderColor = qteSuccess ? GUI.Style.Green : GUI.Style.Red * 0.5f; + progressBar.Color = ToolBox.GradientLerp(qteCooldown / QteCooldownDuration, progressBar.Color, qteSliderColor, Color.White); + } + else + { + if (qteTimer / QteDuration <= item.Condition / item.MaxCondition) + { + qteSliderColor = Color.Lerp(qteSliderColor, GUI.Style.Green, 0.5f); + } + } + + progressBar.Parent.Parent.Parent.DrawManually(spriteBatch, true); + GUI.DrawRectangle(spriteBatch, + new Rectangle(sliderRect.X + (int)((qteTimer / QteDuration) * sliderRect.Width), sliderRect.Y - 5, 2, sliderRect.Height + 10), + qteSliderColor, true); + if (item.Condition > defaultMaxCondition) { float extraCondition = item.MaxCondition * (item.MaxRepairConditionMultiplier - 1.0f); @@ -280,9 +333,9 @@ 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) && !item.IsFullCondition; + 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; @@ -350,6 +403,30 @@ namespace Barotrauma.Items.Components repairSoundChannel = null; } + private void QTEAction() + { + if (currentFixerAction == FixActions.Repair) + { + float defaultMaxCondition = item.MaxCondition / item.MaxRepairConditionMultiplier; + qteSuccess = qteCooldown <= 0.0f && qteTimer / QteDuration <= item.Condition / defaultMaxCondition; + } + else + { + return; + } + + if (!GameMain.IsMultiplayer) { RepairBoost(qteSuccess); } + + SoundPlayer.PlayUISound(qteSuccess ? GUISoundType.IncreaseQuantity : GUISoundType.DecreaseQuantity); + + //on failure during cooldown reset cursor to beginning + if (!qteSuccess && qteCooldown > 0.0f) { qteTimer = QteDuration; } + qteCooldown = QteCooldownDuration; + //this will be set on button down so we can reset it here + requestStartFixAction = FixActions.None; + item.CreateClientEvent(this); + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { deteriorationTimer = msg.ReadSingle(); @@ -361,11 +438,17 @@ namespace Barotrauma.Items.Components currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); + if (CurrentFixer == null) + { + qteTimer = QteDuration; + qteCooldown = 0.0f; + } } public void ClientWrite(IWriteMessage msg, object[] extraData = null) { msg.WriteRangedInteger((int)requestStartFixAction, 0, 2); + msg.Write(qteSuccess); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index a816f402a..f9c20a0ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -26,16 +26,11 @@ namespace Barotrauma.Items.Components AutoHideScrollBar = false }; - // Create fillerBlock to cover historyBox so new values appear at the bottom of historyBox - // This could be removed if GUIListBox supported aligning its children - fillerBlock = new GUITextBlock(new RectTransform(new Vector2(1, 1), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), string.Empty) - { - CanBeFocused = false - }; + CreateFillerBlock(); 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, @@ -55,6 +50,16 @@ namespace Barotrauma.Items.Components }; } + // Create fillerBlock to cover historyBox so new values appear at the bottom of historyBox + // This could be removed if GUIListBox supported aligning its children + public void CreateFillerBlock() + { + fillerBlock = new GUITextBlock(new RectTransform(new Vector2(1, 1), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), string.Empty) + { + CanBeFocused = false + }; + } + private void SendOutput(string input) { if (input.Length > MaxMessageLength) @@ -63,15 +68,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 +90,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 }; @@ -130,7 +135,12 @@ namespace Barotrauma.Items.Components public void ClientWrite(IWriteMessage msg, object[] extraData = null) { - msg.Write((string)extraData[2]); + if (extraData is null) { return; } + + if (extraData[2] is string str) + { + msg.Write(str); + } } public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) 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/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index acec84bc8..0c31c61a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -160,7 +160,7 @@ namespace Barotrauma.Items.Components { errorMsg += "\nTrying to dock the submarine to itself."; } - GameAnalyticsManager.AddErrorEventOnce("DockingPort.ClientRead:JointNotCreated", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("DockingPort.ClientRead:JointNotCreated", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } if (isLocked) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 2b50d7e4a..0d443f65d 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) @@ -314,7 +314,7 @@ namespace Barotrauma } } - string colorStr = XMLExtensions.ColorToString(!item.AllowStealing ? GUI.Style.Red : Color.White); + string colorStr = XMLExtensions.ColorToString(item.SpawnedInCurrentOutpost && !item.AllowStealing ? GUI.Style.Red : Color.White); toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; if (item.GetComponent() != null) @@ -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..60d8a0fc6 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; @@ -648,12 +648,18 @@ namespace Barotrauma if (linkedTo.Contains(otherEntity)) { linkedTo.Remove(otherEntity); - if (otherEntity.linkedTo != null && otherEntity.linkedTo.Contains(this)) otherEntity.linkedTo.Remove(this); + if (otherEntity.linkedTo != null && otherEntity.linkedTo.Contains(this)) + { + otherEntity.linkedTo.Remove(this); + } } else { linkedTo.Add(otherEntity); - if (otherEntity.Linkable && otherEntity.linkedTo != null) otherEntity.linkedTo.Add(this); + if (otherEntity.Linkable && otherEntity.linkedTo != null) + { + otherEntity.linkedTo.Add(this); + } } } } @@ -1445,7 +1451,7 @@ namespace Barotrauma #else if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif - GameAnalyticsManager.AddErrorEventOnce("Item.ClientReadPosition:nophysicsbody", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.ClientReadPosition:nophysicsbody", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -1586,7 +1592,7 @@ namespace Barotrauma string errorMsg = "Failed to spawn item, prefab not found (name: " + (itemName ?? "null") + ", identifier: " + (itemIdentifier ?? "null") + ")"; errorMsg += "\n" + string.Join(", ", GameMain.Config.AllEnabledPackages.Select(cp => cp.Name)); GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:PrefabNotFound" + (itemName ?? "null") + (itemIdentifier ?? "null"), - GameAnalyticsSDK.Net.EGAErrorSeverity.Critical, + GameAnalyticsManager.ErrorSeverity.Critical, errorMsg); DebugConsole.ThrowError(errorMsg); return null; @@ -1607,7 +1613,7 @@ namespace Barotrauma string errorMsg = $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"), - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); DebugConsole.ThrowError(errorMsg); inventory = parentItem.GetComponent()?.Inventory; @@ -1632,7 +1638,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..ad94444ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -95,10 +95,10 @@ 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) + while (light.Color.A > 0.0f && flashDuration > 0.0f && currBrightness > 0.0f) { light.Color = new Color(light.Color.R, light.Color.G, light.Color.B, (byte)(currBrightness * 255)); currBrightness -= 1.0f / flashDuration * CoroutineManager.DeltaTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs index 7fa3a7021..9a071dfb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs @@ -92,7 +92,7 @@ namespace Barotrauma DebugConsole.ThrowError("Invalid left normal"); #endif GameAnalyticsManager.AddErrorEventOnce("CaveGenerator.GenerateWallShapes:InvalidLeftNormal:" + level.Seed, - GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, + GameAnalyticsManager.ErrorSeverity.Warning, "Invalid left normal (leftedge: " + leftEdge + ", rightedge: " + rightEdge + ", normal: " + leftNormal + ", seed: " + level.Seed + ")"); if (cell.Body != null) @@ -127,7 +127,7 @@ namespace Barotrauma DebugConsole.ThrowError("Invalid right normal"); #endif GameAnalyticsManager.AddErrorEventOnce("CaveGenerator.GenerateWallShapes:InvalidRightNormal:" + level.Seed, - GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, + GameAnalyticsManager.ErrorSeverity.Warning, "Invalid right normal (leftedge: " + leftEdge + ", rightedge: " + rightEdge + ", normal: " + rightNormal + ", seed: " + level.Seed + ")"); if (cell.Body != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index f178bf7f5..f7826794f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -497,11 +497,13 @@ namespace Barotrauma.Lights return true; } + private readonly Dictionary visibleHulls = new Dictionary(); private Dictionary GetVisibleHulls(Camera cam) { - Dictionary visibleHulls = new Dictionary(); + visibleHulls.Clear(); foreach (Hull hull in Hull.hullList) { + if (hull.HiddenInGame) { continue; } var drawRect = hull.Submarine == null ? hull.Rect : diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index b3b2a653f..d8df8b2a9 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, @@ -536,7 +536,7 @@ namespace Barotrauma invalidMessage = true; string errorMsg = $"Error while reading a network event for the structure \"{Name} ({ID})\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; DebugConsole.NewMessage(errorMsg, Color.Red); - GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } for (int i = 0; i < sectionCount; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 6d4db8e52..2890e470f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -98,7 +98,7 @@ namespace Barotrauma { string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } @@ -124,7 +124,7 @@ namespace Barotrauma { string errorMsg = "Failed to load sound file \"" + filename + "\"."; DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + filename, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } } @@ -148,7 +148,7 @@ namespace Barotrauma { string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } } @@ -183,13 +183,14 @@ namespace Barotrauma visibleSubs.Clear(); foreach (Submarine sub in Loaded) { - if (sub.WorldPosition.Y < Level.MaxEntityDepth) continue; + if (sub.WorldPosition.Y < Level.MaxEntityDepth) { continue; } + int margin = 500; Rectangle worldBorders = new Rectangle( - sub.Borders.X + (int)sub.WorldPosition.X - 500, - sub.Borders.Y + (int)sub.WorldPosition.Y + 500, - sub.Borders.Width + 1000, - sub.Borders.Height + 1000); + sub.VisibleBorders.X + (int)sub.WorldPosition.X - margin, + sub.VisibleBorders.Y + (int)sub.WorldPosition.Y + margin, + sub.VisibleBorders.Width + margin * 2, + sub.VisibleBorders.Height + margin * 2); if (RectsOverlap(worldBorders, cam.WorldView)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index 95c090122..c50ae90a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -26,7 +26,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted.", e); - GameAnalyticsManager.AddErrorEventOnce("Submarine..ctor:PreviewImageLoadingFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("Submarine..ctor:PreviewImageLoadingFailed", GameAnalyticsManager.ErrorSeverity.Error, "Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted."); PreviewImage = null; } @@ -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 956829d0e..db3882907 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -462,7 +462,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; @@ -651,7 +651,7 @@ namespace Barotrauma.Networking { errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); } - GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); DebugConsole.ThrowError("Error while reading a message from server.", e); new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", new string[2] { "[message]", "[targetsite]" }, new string[2] { e.Message, e.TargetSite.ToString() })); Disconnect(); @@ -781,7 +781,7 @@ namespace Barotrauma.Networking #if DEBUG DebugConsole.ThrowError("Error while reading an ingame update message from server.", e); #endif - GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw; } break; @@ -793,7 +793,7 @@ namespace Barotrauma.Networking errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce( "GameClient.ReadDataMessage:VoipClientNull", - GameMain.Client == null ? GameAnalyticsSDK.Net.EGAErrorSeverity.Error : GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, + GameMain.Client == null ? GameAnalyticsManager.ErrorSeverity.Error : GameAnalyticsManager.ErrorSeverity.Warning, errorMsg); return; } @@ -863,8 +863,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; @@ -874,6 +874,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(); @@ -907,6 +908,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(); @@ -992,7 +994,7 @@ namespace Barotrauma.Networking { string errorMsg = "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server." + " There may have been an error in receiving the up-to-date submarine file from the server."; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -1013,7 +1015,7 @@ namespace Barotrauma.Networking if (!GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id))) { string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {string.Join(", ", serverMissionIdentifiers)}, client: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } GameMain.GameSession.EnforceMissionOrder(serverMissionIdentifiers); @@ -1034,7 +1036,7 @@ namespace Barotrauma.Networking ", seed: " + Level.Loaded.Seed + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } else @@ -1050,7 +1052,7 @@ namespace Barotrauma.Networking ", seed: " + Level.Loaded.Seed + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } } @@ -1120,8 +1122,8 @@ namespace Barotrauma.Networking { GameAnalyticsManager.AddErrorEventOnce( "GameClient.HandleDisconnectMessage", - GameAnalyticsSDK.Net.EGAErrorSeverity.Debug, - "Client received a disconnect message. Reason: " + disconnectReason.ToString() + ", message: " + disconnectMsg); + GameAnalyticsManager.ErrorSeverity.Debug, + "Client received a disconnect message. Reason: " + disconnectReason.ToString()); } if (disconnectReason == DisconnectReason.ServerFull) @@ -1241,7 +1243,7 @@ namespace Barotrauma.Networking } } - private IEnumerable WaitInServerQueue() + private IEnumerable WaitInServerQueue() { waitInServerQueueBox = new GUIMessageBox( TextManager.Get("ServerQueuePleaseWait"), @@ -1429,7 +1431,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RefreshEnabledElements(); } - private IEnumerable StartGame(IReadMessage inc) + private IEnumerable StartGame(IReadMessage inc) { Character?.Remove(); Character = null; @@ -1473,6 +1475,7 @@ namespace Barotrauma.Networking serverSettings.AllowFriendlyFire = inc.ReadBoolean(); serverSettings.LockAllDefaultWires = inc.ReadBoolean(); serverSettings.AllowRagdollButton = inc.ReadBoolean(); + serverSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); @@ -1535,7 +1538,7 @@ namespace Barotrauma.Networking gameStarted = true; GameMain.NetLobbyScreen.Select(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } @@ -1547,7 +1550,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.Select(); string errorMsg = "Failed to select shuttle \"" + shuttleName + "\" (hash: " + shuttleHash + ")."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectShuttle" + shuttleName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectShuttle" + shuttleName, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } @@ -1567,31 +1570,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 ? @@ -1670,10 +1689,6 @@ namespace Barotrauma.Networking } if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } - - clientPeer.Update((float)Timing.Step); - - if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } } catch (Exception e) { @@ -1749,7 +1764,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); @@ -1859,8 +1874,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")) @@ -2108,9 +2122,6 @@ namespace Barotrauma.Networking bool autoRestartEnabled = inc.ReadBoolean(); float autoRestartTimer = autoRestartEnabled ? inc.ReadSingle() : 0.0f; - bool radiationEnabled = inc.ReadBoolean(); - byte maxMissionCount = inc.ReadByte(); - //ignore the message if we already a more up-to-date one //or if we're still waiting for the initial update if (NetIdUtils.IdMoreRecent(updateID, GameMain.NetLobbyScreen.LastUpdateID) && @@ -2160,22 +2171,15 @@ namespace Barotrauma.Networking if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "campaign")) { GameMain.NetLobbyScreen.CampaignSubmarines.Add(sub); - } - } - - if (HasPermission(ClientPermissions.ManageCampaign) && !gameStarted && GameMain.NetLobbyScreen?.CampaignSetupUI != null) - { - GameMain.NetLobbyScreen.CampaignSetupUI.RefreshMultiplayerCampaignSubUI(GameMain.NetLobbyScreen.CampaignSubmarines); + } } } GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating); GameMain.NetLobbyScreen.LevelSeed = levelSeed; GameMain.NetLobbyScreen.SetLevelDifficulty(levelDifficulty); - GameMain.NetLobbyScreen.SetRadiationEnabled(radiationEnabled); GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode); GameMain.NetLobbyScreen.SetBotCount(botCount); - GameMain.NetLobbyScreen.SetMaxMissionCount(maxMissionCount); GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer); serverSettings.VoiceChatEnabled = voiceChatEnabled; @@ -2347,7 +2351,7 @@ namespace Barotrauma.Networking { errorLines.Add("[" + DebugConsole.Messages[i].Time + "] " + DebugConsole.Messages[i].Text); } - GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsSDK.Net.EGAErrorSeverity.Critical, string.Join("\n", errorLines)); + GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines)); DebugConsole.ThrowError("Writing object data to \"networkerror_data.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); @@ -2583,29 +2587,29 @@ namespace Barotrauma.Networking subElement.ToolTip = newSub.Description; } - if (GameMain.NetLobbyScreen.FailedSelectedSub != null && - GameMain.NetLobbyScreen.FailedSelectedSub.First == newSub.Name && - GameMain.NetLobbyScreen.FailedSelectedSub.Second == newSub.MD5Hash.Hash) + if (GameMain.NetLobbyScreen.FailedSelectedSub.HasValue && + GameMain.NetLobbyScreen.FailedSelectedSub.Value.Name == newSub.Name && + GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.Hash) { GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.Hash, GameMain.NetLobbyScreen.SubList); } - if (GameMain.NetLobbyScreen.FailedSelectedShuttle != null && - GameMain.NetLobbyScreen.FailedSelectedShuttle.First == newSub.Name && - GameMain.NetLobbyScreen.FailedSelectedShuttle.Second == newSub.MD5Hash.Hash) + if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue && + GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name && + GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.Hash) { GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.Hash, GameMain.NetLobbyScreen.ShuttleList.ListBox); } - Pair failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.First == newSub.Name && s.Second == newSub.MD5Hash.Hash); - if (failedCampaignSub != null) + NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); + if (failedCampaignSub != default) { GameMain.NetLobbyScreen.CampaignSubmarines.Add(newSub); GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub); } - Pair failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.First == newSub.Name && s.Second == newSub.MD5Hash.Hash); - if (failedOwnedSub != null) + NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); + if (failedOwnedSub != default) { GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(newSub); GameMain.NetLobbyScreen.FailedOwnedSubs.Remove(failedOwnedSub); @@ -2675,8 +2679,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) @@ -2941,7 +2948,6 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); msg.Write((UInt16)ClientPermissions.SelectSub); - msg.Write(false); msg.Write(isShuttle); msg.WritePadBits(); msg.Write((UInt16)subIndex); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); @@ -2949,23 +2955,6 @@ namespace Barotrauma.Networking clientPeer.Send(msg, DeliveryMethod.Reliable); } - /// - /// Tell the server to add / remove a purchasable submarine (permission required) - /// - public void RequestCampaignSub(SubmarineInfo sub, bool add) - { - if (!HasPermission(ClientPermissions.SelectSub) || sub == null) return; - IWriteMessage msg = new WriteOnlyMessage(); - msg.Write((byte)ClientPacketHeader.SERVER_COMMAND); - msg.Write((UInt16)ClientPermissions.SelectSub); - msg.Write(true); - msg.Write(sub.EqualityCheckVal); - msg.Write(add); - msg.Write((byte)ServerNetObject.END_OF_MESSAGE); - - clientPeer.Send(msg, DeliveryMethod.Reliable); - } - /// /// Tell the server to select a mode (permission required) /// @@ -3024,12 +3013,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); } @@ -3332,7 +3322,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) { @@ -3448,7 +3440,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..893cfe2e2 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,12 +225,15 @@ 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); - GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); //TODO: force the BitPosition to correct place? Having some entity in a potentially incorrect state is not as bad as a desync kick //msg.BitPosition = (int)(msgPosition + msgLength * 8); @@ -264,7 +252,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Failed to read event for entity \"" + entity.ToString() + "\"!", e); GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(), - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); msg.BitPosition = (int)(msgPosition + msgLength * 8); msg.ReadPadBits(); } 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/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index cd1c81315..0aacf802e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -115,13 +115,11 @@ namespace Barotrauma.Networking { if (!isActive) { return; } - byte incByte = inc.ReadByte(); - bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; - bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); //Console.WriteLine(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte); - if (isConnectionInitializationStep && initializationStep != ConnectionInitialization.Success) + if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success) { ReadConnectionInitializationStep(new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); } @@ -133,7 +131,7 @@ namespace Barotrauma.Networking initializationStep = ConnectionInitialization.Success; } UInt16 length = inc.ReadUInt16(); - IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, ServerConnection); + IReadMessage msg = new ReadOnlyMessage(inc.Data, packetHeader.IsCompressed(), inc.PositionInBytes, length, ServerConnection); OnMessageReceived?.Invoke(msg); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 911d2eb23..70e9b0728 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -112,33 +112,28 @@ namespace Barotrauma.Networking NetworkConnection.TimeoutThresholdInGame : NetworkConnection.TimeoutThreshold; - byte incByte = data[0]; - bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; - bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; - bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; - bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; - bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + PacketHeader packetHeader = (PacketHeader)data[0]; - if (!isServerMessage) { return; } + if (!packetHeader.IsServerMessage()) { return; } - if (isConnectionInitializationStep) + if (packetHeader.IsConnectionInitializationStep()) { ulong low = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8); ulong high = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8 + 32); ulong lobbyId = low + (high << 32); Steam.SteamManager.JoinLobby(lobbyId, false); - IReadMessage inc = new ReadOnlyMessage(data, false, 1 + 8, dataLength - 9, ServerConnection); + IReadMessage inc = new ReadOnlyMessage(data, false, 1 + 8, dataLength - (1 + 8), ServerConnection); if (initializationStep != ConnectionInitialization.Success) { incomingInitializationMessages.Add(inc); } } - else if (isHeartbeatMessage) + else if (packetHeader.IsHeartbeatMessage()) { return; //TODO: implement heartbeats } - else if (isDisconnectMessage) + else if (packetHeader.IsDisconnectMessage()) { IReadMessage inc = new ReadOnlyMessage(data, false, 1, dataLength - 1, ServerConnection); string msg = inc.ReadString(); @@ -147,10 +142,9 @@ namespace Barotrauma.Networking } else { - UInt16 length = data[1]; - length |= (UInt16)(((UInt32)data[2]) << 8); + UInt16 length = Lidgren.Network.NetBitWriter.ReadUInt16(data, 16, 8); - IReadMessage inc = new ReadOnlyMessage(data, isCompressed, 3, length, ServerConnection); + IReadMessage inc = new ReadOnlyMessage(data, packetHeader.IsCompressed(), 3, length, ServerConnection); incomingDataMessages.Add(inc); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 19b81611c..9f4bc841b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -147,18 +147,13 @@ namespace Barotrauma.Networking DeliveryMethod deliveryMethod = (DeliveryMethod)data[0]; - byte incByte = data[1]; - bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; - bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; - bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; - bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; - bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + PacketHeader packetHeader = (PacketHeader)data[1]; - if (!remotePeer.Authenticated & !remotePeer.Authenticating && isConnectionInitializationStep) + if (!remotePeer.Authenticated & !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) { remotePeer.DisconnectTime = null; - IReadMessage authMsg = new ReadOnlyMessage(data, isCompressed, 2, dataLength - 2, null); + IReadMessage authMsg = new ReadOnlyMessage(data, packetHeader.IsCompressed(), 2, dataLength - 2, null); ConnectionInitialization initializationStep = (ConnectionInitialization)authMsg.ReadByte(); if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { @@ -242,17 +237,11 @@ namespace Barotrauma.Networking int p2pDataStart = inc.BytePosition; - byte incByte = inc.ReadByte(); - - bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; - bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; - bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; - bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; - bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); if (recipientSteamId != selfSteamID) { - if (!isServerMessage) + if (!packetHeader.IsServerMessage()) { DebugConsole.ThrowError("Received non-server message meant for remote peer"); return; @@ -262,7 +251,7 @@ namespace Barotrauma.Networking if (peer == null) { return; } - if (isDisconnectMessage) + if (packetHeader.IsDisconnectMessage()) { DisconnectPeer(peer, inc.ReadString()); return; @@ -273,8 +262,8 @@ namespace Barotrauma.Networking { case DeliveryMethod.Reliable: case DeliveryMethod.ReliableOrdered: - //the documentation seems to suggest that the Reliable send type - //enforces packet order (TODO: verify) + //the documentation seems to suggest that the + //Reliable send type enforces packet order sendType = Steamworks.P2PSend.Reliable; break; default: @@ -284,17 +273,31 @@ namespace Barotrauma.Networking byte[] p2pData; - if (isConnectionInitializationStep) + if (packetHeader.IsConnectionInitializationStep()) { p2pData = new byte[inc.LengthBytes - p2pDataStart + 8]; p2pData[0] = inc.Buffer[p2pDataStart]; - Lidgren.Network.NetBitWriter.WriteUInt64(SteamManager.CurrentLobbyID, 64, p2pData, 8); - Array.Copy(inc.Buffer, p2pDataStart+1, p2pData, 9, inc.LengthBytes - p2pDataStart - 1); + Lidgren.Network.NetBitWriter.WriteUInt64(SteamManager.CurrentLobbyID, 8 * 8, p2pData, 1 * 8); + Array.Copy(inc.Buffer, p2pDataStart+1, p2pData, 1 + 8, inc.LengthBytes - p2pDataStart - 1); } else { p2pData = new byte[inc.LengthBytes - p2pDataStart]; Array.Copy(inc.Buffer, p2pDataStart, p2pData, 0, p2pData.Length); + + if (!packetHeader.IsHeartbeatMessage() && !packetHeader.IsDisconnectMessage()) + { + UInt16 length = Lidgren.Network.NetBitWriter.ReadUInt16(p2pData, 16, 8); + if (length > p2pData.Length - 2) + { + string errorMsg = $"Length written in message to send to client is larger than buffer size ({length} > {p2pData.Length - 2})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "SteamP2POwnerPeerLengthValidationFail", + GameAnalyticsManager.ErrorSeverity.Error, + errorMsg); + } + } } if (p2pData.Length + 4 >= MsgConstants.MTU) @@ -323,21 +326,21 @@ namespace Barotrauma.Networking } else { - if (isDisconnectMessage) + if (packetHeader.IsDisconnectMessage()) { DebugConsole.ThrowError("Received disconnect message from owned server"); return; } - if (!isServerMessage) + if (!packetHeader.IsServerMessage()) { DebugConsole.ThrowError("Received non-server message from owned server"); return; } - if (isHeartbeatMessage) + if (packetHeader.IsHeartbeatMessage()) { - return; //timeout is handled by Lidgren, ignore this message + return; //no timeout since we're using pipes, ignore this message } - if (isConnectionInitializationStep) + if (packetHeader.IsConnectionInitializationStep()) { IWriteMessage outMsg = new WriteOnlyMessage(); outMsg.Write(selfSteamID); @@ -358,7 +361,7 @@ namespace Barotrauma.Networking initializationStep = ConnectionInitialization.Success; } UInt16 length = inc.ReadUInt16(); - IReadMessage msg = new ReadOnlyMessage(inc.Buffer, isCompressed, inc.BytePosition, length, ServerConnection); + IReadMessage msg = new ReadOnlyMessage(inc.Buffer, packetHeader.IsCompressed(), inc.BytePosition, length, ServerConnection); OnMessageReceived?.Invoke(msg); return; 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/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 03e0618e0..8e18c1c82 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -242,7 +242,8 @@ namespace Barotrauma.Networking textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() { Data = data, - OnClick = GameMain.NetLobbyScreen.SelectPlayer + OnClick = GameMain.NetLobbyScreen.SelectPlayer, + OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu }); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 07f3a2f9a..d345d2a98 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -128,8 +128,18 @@ namespace Barotrauma.Networking { cachedServerListInfo = null; - ServerName = incMsg.ReadString(); - ServerMessageText = incMsg.ReadString(); + NetFlags requiredFlags = (NetFlags)incMsg.ReadByte(); + + if (requiredFlags.HasFlag(NetFlags.Name)) + { + ServerName = incMsg.ReadString(); + } + + if (requiredFlags.HasFlag(NetFlags.Message)) + { + ServerMessageText = incMsg.ReadString(); + } + PlayStyle = (PlayStyle)incMsg.ReadByte(); MaxPlayers = incMsg.ReadByte(); HasPassword = incMsg.ReadBoolean(); IsPublic = incMsg.ReadBoolean(); @@ -139,9 +149,13 @@ namespace Barotrauma.Networking TickRate = incMsg.ReadRangedInteger(1, 60); GameMain.NetworkMember.TickRate = TickRate; - ReadExtraCargo(incMsg); - - Voting.ClientRead(incMsg); + if (requiredFlags.HasFlag(NetFlags.Properties)) + { + ReadExtraCargo(incMsg); + } + + ReadHiddenSubs(incMsg); + GameMain.NetLobbyScreen.UpdateSubVisibility(); bool isAdmin = incMsg.ReadBoolean(); incMsg.ReadPadBits(); @@ -151,7 +165,7 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? radiationEnabled = null, bool? useRespawnShuttle = null, int maxMissionCount = 0) + public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -202,6 +216,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); @@ -216,8 +235,6 @@ namespace Barotrauma.Networking outMsg.Write(autoRestart != null); outMsg.Write(autoRestart ?? false); - outMsg.Write(radiationEnabled ?? RadiationEnabled); - outMsg.Write((byte)maxMissionCount + 1); outMsg.WritePadBits(); } @@ -288,7 +305,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 +380,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 +398,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 +406,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 +424,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 +442,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 +465,7 @@ namespace Barotrauma.Networking return true; } }; - GetPropertyData("SaveServerLogs").AssignGUIComponent(saveLogsBox); + GetPropertyData(nameof(SaveServerLogs)).AssignGUIComponent(saveLogsBox); //-------------------------------------------------------------------------------- // game settings @@ -480,20 +497,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 +520,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 +535,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 +562,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 +586,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 +602,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) { @@ -669,7 +719,6 @@ namespace Barotrauma.Networking RelativeSpacing = 0.05f }; - if (ip.InventoryIcon != null || ip.sprite != null) { GUIImage img = new GUIImage(new RectTransform(new Point(itemFrame.Rect.Height), itemFrame.RectTransform), @@ -692,7 +741,7 @@ namespace Barotrauma.Networking GUINumberInput.NumberType.Int, textAlignment: Alignment.CenterLeft) { MinValueInt = 0, - MaxValueInt = 100, + MaxValueInt = MaxExtraCargoItemsOfType, IntValue = cargoVal }; amountInput.OnValueChanged += (numberInput) => @@ -700,16 +749,26 @@ namespace Barotrauma.Networking if (ExtraCargo.ContainsKey(ip)) { ExtraCargo[ip] = numberInput.IntValue; - if (numberInput.IntValue <= 0) ExtraCargo.Remove(ip); + if (numberInput.IntValue <= 0) { ExtraCargo.Remove(ip); } } - else + else if (ExtraCargo.Keys.Count() < MaxExtraCargoItemTypes) { ExtraCargo.Add(ip, numberInput.IntValue); } + numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0; + CoroutineManager.Invoke(() => + { + foreach (var child in cargoFrame.Content.GetAllChildren()) + { + if (child.GetChild() is GUINumberInput otherNumberInput) + { + otherNumberInput.PlusButton.Enabled = ExtraCargo.Keys.Count() < MaxExtraCargoItemTypes && otherNumberInput.IntValue < otherNumberInput.MaxValueInt; + } + } + }, 0.0f); }; } - //-------------------------------------------------------------------------------- // antigriefing //-------------------------------------------------------------------------------- @@ -729,35 +788,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 +831,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 +843,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 +859,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) @@ -871,6 +930,7 @@ namespace Barotrauma.Networking //-------------------------------------------------------------------------------- Whitelist.CreateWhiteListFrame(settingsTabs[(int)SettingsTab.Whitelist]); + Whitelist.localEnabled = Whitelist.Enabled; } private void CreateLabeledSlider(GUIComponent parent, string labelTag, out GUIScrollBar slider, out GUITextBlock label) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 13604817e..29295f520 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -147,7 +147,14 @@ namespace Barotrauma.Steam if (currentLobby == null) { - DebugConsole.ThrowError("Failed to create Steam lobby"); + DebugConsole.ThrowError("Failed to create Steam lobby: returned lobby was null"); + lobbyState = LobbyState.NotConnected; + return; + } + + if (currentLobby.Value.Result != Steamworks.Result.OK) + { + DebugConsole.ThrowError($"Failed to create Steam lobby: result was {currentLobby.Value.Result}"); lobbyState = LobbyState.NotConnected; return; } @@ -525,18 +532,6 @@ namespace Barotrauma.Steam } #region Connecting to servers - private static Steamworks.AuthTicket currentTicket = null; - public static Steamworks.AuthTicket GetAuthSessionTicket() - { - if (!isInitialized) - { - return null; - } - - currentTicket?.Cancel(); - currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); - return currentTicket; - } public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) { @@ -884,9 +879,9 @@ namespace Barotrauma.Steam catch (Exception e) { - string errorMsg = "Failed to save workshop item preview image to \"" + previewImagePath + "\" when creating workshop item staging folder."; + string errorMsg = "Failed to save workshop item preview image when creating workshop item staging folder."; GameAnalyticsManager.AddErrorEventOnce("SteamManager.CreateWorkshopItemStaging:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); + GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.Message); } return true; @@ -935,7 +930,7 @@ namespace Barotrauma.Steam return workshopPublishStatus; } - private static IEnumerable PublishItem(WorkshopPublishStatus workshopPublishStatus) + private static IEnumerable PublishItem(WorkshopPublishStatus workshopPublishStatus) { if (!isInitialized) { @@ -1434,8 +1429,8 @@ namespace Barotrauma.Steam "\" not found. Could not combine path (" + (item.Directory ?? "directory name empty") + ")."; DebugConsole.ThrowError(errorMessage); GameAnalyticsManager.AddErrorEventOnce("SteamManager.CheckWorkshopItemInstalled:PathCombineException" + item.Title, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMessage); + GameAnalyticsManager.ErrorSeverity.Error, + "Metadata file for a Workshop item not found. Could not combine path."); return false; } @@ -1567,8 +1562,8 @@ namespace Barotrauma.Steam } GameAnalyticsManager.AddErrorEventOnce( "SteamManager.AutoUpdateWorkshopItems:" + e.Message, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.ErrorSeverity.Error, + "Failed to autoupdate workshop item. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); }); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 49058b640..a32ef3bd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -113,7 +113,7 @@ namespace Barotrauma.Networking UserData = "capturedevicenotfound" }; } - GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpenFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpenFailed", GameAnalyticsManager.ErrorSeverity.Error, "Alc.CaptureDeviceOpen(" + deviceName + ") failed. Error code: " + errorCode); GameMain.Config.VoiceSetting = GameSettings.VoiceMode.Disabled; Instance?.Dispose(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 31dc44d22..8d5406f96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -17,7 +17,11 @@ namespace Barotrauma allowSubVoting = value; GameMain.NetLobbyScreen.SubList.Enabled = value || (GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - GameMain.NetLobbyScreen.Frame.FindChild("subvotes", true).Visible = value; + var subVotesLabel = GameMain.NetLobbyScreen.Frame.FindChild("subvotes", true) as GUITextBlock; + subVotesLabel.Visible = value; + var subVisButton = GameMain.NetLobbyScreen.SubVisibilityButton; + subVisButton.RectTransform.AbsoluteOffset + = new Point(value ? (int)(subVotesLabel.TextSize.X + subVisButton.Rect.Width) : 0, 0); UpdateVoteTexts(null, VoteType.Sub); GameMain.NetLobbyScreen.SubList.Deselect(); @@ -66,10 +70,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/Networking/WhiteList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/WhiteList.cs index 0406f6703..554d32169 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/WhiteList.cs @@ -56,17 +56,12 @@ namespace Barotrauma.Networking { nameBox.Enabled = box.Selected; ipBox.Enabled = box.Selected; - addNewButton.Enabled = box.Selected; - + addNewButton.Enabled = box.Selected && !string.IsNullOrEmpty(ipBox.Text) && !string.IsNullOrEmpty(nameBox.Text); localEnabled = box.Selected; - - return true; } }; - localEnabled = Enabled; - var listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), whitelistFrame.RectTransform)); foreach (WhiteListedPlayer wlp in whitelistedPlayers) { 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..0f535c9d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -136,7 +136,7 @@ namespace Barotrauma.Particles Prefab = prefab; } - public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, Tuple tracerPoints = null) + public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) { if (GameMain.Client?.MidRoundSyncing ?? false) { return; } @@ -159,7 +159,7 @@ namespace Barotrauma.Particles for (float z = 0.0f; z < dist; z += Prefab.Properties.EmitAcrossRayInterval) { Vector2 pos = tracerPoints.Item1 + dir * z; - Emit(pos, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: null); + Emit(pos, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: null); } } } @@ -169,7 +169,7 @@ namespace Barotrauma.Particles float emitInterval = 1.0f / Prefab.Properties.ParticlesPerSecond; while (emitTimer > emitInterval) { - Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: tracerPoints); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: tracerPoints); emitTimer -= emitInterval; } } @@ -183,16 +183,19 @@ namespace Barotrauma.Particles } } - private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, Tuple tracerPoints = null) + private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) { 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) * (mirrorAngle ? -1 : 1); + 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/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index 9d444cd03..989e459a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -201,7 +201,7 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); #endif GameAnalyticsManager.AddErrorEventOnce("PhysicsBody.ClientRead:InvalidData" + parentDebugName, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 23a9d554d..cbc8188bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -1,11 +1,9 @@ #region Using Statements using System; -using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Text; -using GameAnalyticsSDK.Net; using Barotrauma.Steam; using System.Diagnostics; using System.Runtime.InteropServices; @@ -245,6 +243,13 @@ namespace Barotrauma } } + if (GameAnalyticsManager.SendUserStatistics) + { + //send crash report before appending debug console messages (which may contain non-anonymous information) + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, sb.ToString()); + GameAnalyticsManager.ShutDown(); + } + sb.AppendLine("Last debug messages:"); for (int i = DebugConsole.Messages.Count - 1; i >= 0; i--) { @@ -255,13 +260,11 @@ namespace Barotrauma File.WriteAllText(filePath, crashReport); - if (GameSettings.SaveDebugConsoleLogs) DebugConsole.SaveLogs(); + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } - if (GameSettings.SendUserStatistics) + if (GameAnalyticsManager.SendUserStatistics) { CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers.", filePath); - GameAnalytics.AddErrorEvent(EGAErrorSeverity.Critical, crashReport); - GameAnalytics.OnQuit(); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 81ead93e1..ff0f4ffc2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -7,14 +7,10 @@ namespace Barotrauma { protected readonly GUIComponent newGameContainer, loadGameContainer; - protected GUIListBox subList; protected GUIListBox saveList; - protected List subTickBoxes; protected GUITextBox saveNameBox, seedBox; - protected GUILayoutGroup subPreviewContainer; - protected GUIButton loadGameButton; public Action StartNewGame; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index e02424ee4..afe8fb55a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Xml.Linq; using System.Globalization; using Barotrauma.Extensions; +using Barotrauma.Networking; namespace Barotrauma { @@ -14,55 +15,79 @@ namespace Barotrauma { private GUIButton deleteMpSaveButton; - public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable saveFiles = null) : base(newGameContainer, loadGameContainer) { - var columnContainer = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: true) + var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: false) { Stretch = true, RelativeSpacing = 0.0f }; - var leftColumn = new GUILayoutGroup(new RectTransform(Vector2.One, columnContainer.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.015f - }; - - var rightColumn = new GUILayoutGroup(new RectTransform(Vector2.Zero, columnContainer.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.015f - }; - - columnContainer.Recalculate(); - - // New game left side - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont); - saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) + // New game + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) { textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft); + seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - // Spacing to fix the multiplayer campaign setup layout - CreateMultiplayerCampaignSubList(leftColumn.RectTransform); - - //spacing - //new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); - - // New game right side - subPreviewContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)) + GUIFrame radiationBoxContainer + = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), style: null); + GUITickBox radiationEnabledTickBox = null; + if (MapGenerationParams.Instance.RadiationParams != null) { - Stretch = true + radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), radiationBoxContainer.RectTransform, Anchor.Center), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + { + Selected = true, + OnSelected = box => true + }; + } + + var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), verticalLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true) + { + ToolTip = TextManager.Get("maxmissioncounttooltip") }; + int maxMissionCount = GameMain.NetworkMember.ServerSettings.MaxMissionCount; + var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + var maxMissionCountButtons = new GUIButton[2]; + maxMissionCountButtons[0] + = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), + style: "GUIButtonToggleLeft"); + var maxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), - leftColumn.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); + void updateMissionCountText() + { + maxMissionCount = MathHelper.Clamp(maxMissionCount, + CampaignSettings.MinMissionCountLimit, + CampaignSettings.MaxMissionCountLimit); + maxMissionCountText.Text = maxMissionCount.ToString(CultureInfo.InvariantCulture); + } + maxMissionCountButtons[1] + = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), + style: "GUIButtonToggleRight"); + maxMissionCountButtons[0].OnClicked = (button, o) => + { + maxMissionCount--; + updateMissionCountText(); + return false; + }; + maxMissionCountButtons[1].OnClicked = (button, o) => + { + maxMissionCount++; + updateMissionCountText(); + return false; + }; + updateMissionCountText(); + maxMissionCountSettingHolder.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); - StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, TextManager.Get("StartCampaignButton")) + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.04f), + verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); + + StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) { OnClicked = (GUIButton btn, object userData) => { @@ -85,20 +110,19 @@ namespace Barotrauma if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) { - ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; - subList.SelectedComponent.CanBeFocused = false; - subList.Deselect(); + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("nohashsubmarineselected")); return false; } string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = new CampaignSettings(); + CampaignSettings settings = new CampaignSettings + { + RadiationEnabled = radiationEnabledTickBox?.Selected ?? GameMain.NetworkMember.ServerSettings.RadiationEnabled, + MaxMissionCount = maxMissionCount + }; - settings.RadiationEnabled = GameMain.NetLobbyScreen.IsRadiationEnabled(); - settings.MaxMissionCount = GameMain.NetLobbyScreen.GetMaxMissionCount(); - if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { if (!hasRequiredContentPackages) @@ -148,7 +172,9 @@ namespace Barotrauma return true; } }; - + StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; + StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUI.Style.SmallFont, textColor: GUI.Style.Green) { TextGetter = () => @@ -163,115 +189,12 @@ namespace Barotrauma } }; - columnContainer.Recalculate(); - leftColumn.Recalculate(); - rightColumn.Recalculate(); + verticalLayout.Recalculate(); - if (submarines != null) { UpdateSubList(submarines); } UpdateLoadMenu(saveFiles); } - private void CreateMultiplayerCampaignSubList(RectTransform parent) - { - GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.725f), parent)) - { - RelativeSpacing = 0.005f, - Stretch = true - }; - - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("purchasablesubmarines", fallBackTag: "workshoplabelsubmarines"), font: GUI.SubHeadingFont); - - 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) => - { - foreach (GUIComponent child in subList.Content.Children) - { - if (!(child.UserData is SubmarineInfo sub)) { continue; } - child.Visible = string.IsNullOrEmpty(text) ? true : sub.DisplayName.ToLower().Contains(text.ToLower()); - } - return true; - }; - - subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)); - subTickBoxes = new List(); - - for (int i = 0; i < GameMain.Client.ServerSubmarines.Count; i++) - { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[i]; - - if (!sub.IsCampaignCompatible) continue; - - var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), subList.Content.RectTransform) { MinSize = new Point(0, 20) }, - style: "ListBoxElement") - { - ToolTip = sub.Description, - UserData = sub - }; - - int buttonSize = (int)(frame.Rect.Height * 0.8f); - - GUITickBox tickBox = new GUITickBox(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Content.Rect.Width - 65)) - { - UserData = sub, - OnSelected = (GUITickBox box) => - { - GameMain.Client.RequestCampaignSub(box.UserData as SubmarineInfo, box.Selected); - return true; - } - }; - subTickBoxes.Add(tickBox); - tickBox.Selected = GameMain.NetLobbyScreen.CampaignSubmarines.Contains(sub); - - frame.RectTransform.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); - - var subTextBlock = tickBox.TextBlock; - - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); - if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); - - if (matchingSub == null) - { - subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); - frame.ToolTip = TextManager.Get("SubNotFound"); - } - else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.Hash != sub.MD5Hash?.Hash) - { - subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); - frame.ToolTip = TextManager.Get("SubDoesntMatch"); - } - - if (!sub.RequiredContentPackagesInstalled) - { - subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); - frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.RawToolTip; - } - - var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) - { - TextColor = subTextBlock.TextColor * 0.8f, - ToolTip = subTextBlock.RawToolTip - }; - } - } - - public void RefreshMultiplayerCampaignSubUI(List campaignSubs) - { - for (int i = 0; i < subTickBoxes.Count; i++) - { - subTickBoxes[i].Selected = campaignSubs.Contains(subTickBoxes[i].UserData as SubmarineInfo); - } - } - - private IEnumerable WaitForCampaignSetup() + private IEnumerable WaitForCampaignSetup() { GUI.SetCursorWaiting(); string headerText = TextManager.Get("CampaignStartingPleaseWait"); @@ -298,64 +221,6 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void UpdateSubList(IEnumerable submarines) - { - List subsToShow; - string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); - - subsToShow.Sort((s1, s2) => - { - int p1 = s1.Price > CampaignMode.InitialMoney ? 10 : 0; - int p2 = s2.Price > CampaignMode.InitialMoney ? 10 : 0; - return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); - }); - - subList.ClearChildren(); - - foreach (SubmarineInfo sub in subsToShow) - { - var textBlock = new GUITextBlock( - new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), style: "ListBoxElement") - { - ToolTip = sub.Description, - UserData = sub - }; - - if (!sub.RequiredContentPackagesInstalled) - { - textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); - textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.RawToolTip; - } - - var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) - { - TextColor = sub.Price > CampaignMode.InitialMoney ? GUI.Style.Red : textBlock.TextColor * 0.8f, - ToolTip = textBlock.ToolTip - }; -#if !DEBUG - if (!GameMain.DebugDraw) - { - if (sub.Price > CampaignMode.InitialMoney || !sub.IsCampaignCompatible) - { - textBlock.CanBeFocused = false; - textBlock.TextColor *= 0.5f; - } - } -#endif - } - if (SubmarineInfo.SavedSubmarines.Any()) - { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.InitialMoney).ToList(); - if (validSubs.Count > 0) - { - subList.Select(validSubs[Rand.Int(validSubs.Count)]); - } - } - } - private List prevSaveFiles; public void UpdateLoadMenu(IEnumerable saveFiles = null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 17616623b..2ce5f1b82 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -12,6 +12,10 @@ namespace Barotrauma { class SinglePlayerCampaignSetupUI : CampaignSetupUI { + private GUIListBox subList; + + protected GUILayoutGroup subPreviewContainer; + public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; } private GUIButton nextButton; @@ -38,7 +42,7 @@ namespace Barotrauma pageContainer.BarScroll = targetScroll; } - for (int i=0; i { - 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 ffb0f46a4..5b5f36a44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -742,7 +742,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 @@ -782,7 +782,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) { @@ -795,9 +795,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(subName)) { DebugConsole.NewMessage($"Loading the predefined quick start sub \"{subName}\"", Color.White); - selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => - s.Name.ToLower() == subName.ToLower()); - + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLowerInvariant() == subName.ToLowerInvariant()); if (selectedSub == null) { DebugConsole.NewMessage($"Cannot find a sub that matches the name \"{subName}\".", Color.Red); @@ -814,7 +812,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" }; @@ -953,7 +951,7 @@ namespace Barotrauma } } - private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) + private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) { string originalText = messageBox.Text.Text; int doneCount = 0; @@ -1118,17 +1116,23 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); -#if !UNSTABLE - string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; - GUI.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); -#endif + if (selectedTab != Tab.Credits) { +#if !UNSTABLE + string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; + GUI.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); +#endif + string gameAnalyticsStatus = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); + Vector2 textSize = GUI.SmallFont.MeasureString(gameAnalyticsStatus).ToPoint().ToVector2(); + GUI.SmallFont.DrawString(spriteBatch, gameAnalyticsStatus, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.LineHeight * 2 - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); + + Vector2 textPos = new Vector2(GameMain.GraphicsWidth - HUDLayoutSettings.Padding, GameMain.GraphicsHeight - HUDLayoutSettings.Padding * 0.75f); for (int i = legalCrap.Length - 1; i >= 0; i--) { - Vector2 textSize = GUI.SmallFont.MeasureString(legalCrap[i]); - textSize = new Vector2((int)textSize.X, (int)textSize.Y); + textSize = GUI.SmallFont.MeasureString(legalCrap[i]) + .ToPoint().ToVector2(); bool mouseOn = i == 0 && PlayerInput.MousePosition.X > textPos.X - textSize.X && PlayerInput.MousePosition.X < textPos.X && PlayerInput.MousePosition.Y > textPos.Y - textSize.Y && PlayerInput.MousePosition.Y < textPos.Y; @@ -1184,8 +1188,8 @@ namespace Barotrauma DebugConsole.ThrowError("Copying the file \"" + selectedSub.FilePath + "\" failed. The file may have been deleted or in use by another process. Try again or select another submarine.", e); GameAnalyticsManager.AddErrorEventOnce( "MainMenuScreen.StartGame:IOException" + selectedSub.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Copying the file \"" + selectedSub.FilePath + "\" failed.\n" + e.Message + "\n" + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.ErrorSeverity.Error, + "Copying a submarine file failed. " + e.Message + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -1509,13 +1513,13 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError("Fetching remote content to the main menu failed.", e); #endif - GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.FetchRemoteContent:Exception", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.FetchRemoteContent:Exception", GameAnalyticsManager.ErrorSeverity.Error, "Fetching remote content to the main menu failed. " + e.Message); return; } } - private IEnumerable WairForRemoteContentReceived() + private IEnumerable WairForRemoteContentReceived() { while (true) { @@ -1552,7 +1556,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError("Reading received remote main menu content failed.", e); #endif - GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.WairForRemoteContentReceived:Exception", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.WairForRemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, "Reading received remote main menu content failed. " + e.Message); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 40b99af75..794643a21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using Barotrauma.Steam; namespace Barotrauma { @@ -18,9 +19,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; @@ -42,12 +41,6 @@ namespace Barotrauma private readonly GUIScrollBar levelDifficultyScrollBar; - private readonly GUITickBox radiationEnabledTickBox; - - private readonly GUIButton[] maxMissionCountButtons; - private readonly GUITextBlock maxMissionCountText; - private readonly GUITextBlock maxMissionCountDescription; - private readonly GUIButton[] traitorProbabilityButtons; private readonly GUITextBlock traitorProbabilityText; @@ -73,7 +66,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 +79,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 +158,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 +172,7 @@ namespace Barotrauma { if (HighlightedModeIndex == selectedModeIndex) { - modeList.Select(value); + ModeList.Select(value); } selectedModeIndex = value; } @@ -192,17 +180,17 @@ 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 IReadOnlyList GetSubList() + => SubList.Content.Children.Select(c => c.UserData as SubmarineInfo).ToArray(); + + public readonly GUIListBox PlayerList; public GUITextBox CharacterNameBox { @@ -228,16 +216,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 +234,7 @@ namespace Barotrauma public GameModePreset SelectedMode { - get { return modeList.SelectedData as GameModePreset; } + get { return ModeList.SelectedData as GameModePreset; } } public MissionType MissionType @@ -529,7 +510,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 +718,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 +789,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 }; @@ -796,7 +827,8 @@ namespace Barotrauma TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) { UserData = "subvotes", - Visible = false + Visible = false, + CanBeFocused = false }; //respawn shuttle / submarine preview ------------------------------------------------------------------ @@ -832,7 +864,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 +872,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 +903,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 +912,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 +970,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; } }; @@ -1152,45 +1184,6 @@ namespace Barotrauma } }; - if (MapGenerationParams.Instance.RadiationParams != null) - { - radiationEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) - { - Selected = true, - OnSelected = box => - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, radiationEnabled: box.Selected); - return true; - } - }; - } - - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true) - { - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, maxMissionCount: -1); - return true; - } - }; - maxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); - maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, maxMissionCount: 1); - return true; - } - }; - maxMissionCountSettingHolder.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); - List settingsElements = settingsContent.Children.ToList(); for (int i = 0; i < settingsElements.Count; i++) { @@ -1221,7 +1214,7 @@ namespace Barotrauma GUI.ClearCursorWait(); } - public IEnumerable WaitForStartRound(GUIButton startButton) + public IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); string headerText = TextManager.Get("RoundStartingPleaseWait"); @@ -1264,6 +1257,8 @@ namespace Barotrauma { if (GameMain.NetworkMember == null) { return; } + visibilityMenuOrder.Clear(); + CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; @@ -1337,16 +1332,6 @@ namespace Barotrauma } SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - if (radiationEnabledTickBox != null) - { - radiationEnabledTickBox.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - } - maxMissionCountDescription.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - maxMissionCountText.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - foreach (var button in maxMissionCountButtons) - { - button.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - } traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1362,7 +1347,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 +1355,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 +1544,7 @@ namespace Barotrauma }; } - UpdateJobPreferences(); + UpdateJobPreferences(characterInfo); appearanceFrame = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox") { @@ -1853,7 +1840,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 +1852,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 +1875,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 +1886,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 +1995,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,21 +2109,130 @@ 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); } } + private Client ExtractClientFromClickableArea(GUITextBlock.ClickableArea area) + { + if (!UInt64.TryParse(area.Data.Metadata, out UInt64 id)) { return null; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) + ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); + return client; + } + public void SelectPlayer(GUITextBlock component, GUITextBlock.ClickableArea area) { - if (!UInt64.TryParse(area.Data.Metadata, out UInt64 id)) { return; } - Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) - ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) - ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); - if (client == null) { return; } + var client = ExtractClientFromClickableArea(area); + if (client is null) { return; } GameMain.NetLobbyScreen.SelectPlayer(client); } + public void ShowPlayerContextMenu(GUITextBlock component, GUITextBlock.ClickableArea area) + { + var client = ExtractClientFromClickableArea(area); + if (client is null) { return; } + CreateModerationContextMenu(client); + } + + #region Context Menu + public static void CreateModerationContextMenu(Client client) + { + if (GUIContextMenu.CurrentContextMenu != null) { return; } + if (GameMain.IsSingleplayer || client == null || ((!GameMain.Client?.PreviouslyConnectedClients?.Contains(client)) ?? true)) { return; } + bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, + canKick = GameMain.Client.HasPermission(ClientPermissions.Kick), + canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking, + canPromo = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); + + // Disable options if we are targeting ourselves + if (client.ID == GameMain.Client?.ID) + { + canKick = canBan = canPromo = false; + } + + List options = new List(); + + options.Add(new ContextMenuOption("ViewSteamProfile", isEnabled: hasSteam, onSelected: delegate + { + Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); + })); + + options.Add(new ContextMenuOption("ModerationMenu.UserDetails", isEnabled: true, onSelected: delegate + { + GameMain.NetLobbyScreen?.SelectPlayer(client); + })); + + + // Creates sub context menu options for all the ranks + List permissionOptions = new List(); + foreach (PermissionPreset rank in PermissionPreset.List) + { + permissionOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => + { + string label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, rank.Name }); + GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + + msgBox.Buttons[0].OnClicked = delegate + { + client.SetPermissions(rank.Permissions, rank.PermittedCommands); + GameMain.Client.UpdateClientPermissions(client); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + }) { Tooltip = rank.Description }); + } + + options.Add(new ContextMenuOption("Permissions", isEnabled: canPromo, options: permissionOptions.ToArray())); + + Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White; + + if (GameMain.Client.ConnectedClients.Contains(client)) + { + options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.ID != GameMain.Client?.ID, onSelected: delegate + { + client.MutedLocally = !client.MutedLocally; + })); + + bool kickEnabled = client.ID != GameMain.Client?.ID && client.AllowKicking; + + // if the user can kick create a kick option else create the votekick option + ContextMenuOption kickOption; + if (canKick) + { + kickOption = new ContextMenuOption("Kick", isEnabled: kickEnabled, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, false); + }); + } + else + { + kickOption = new ContextMenuOption("VoteToKick", isEnabled: kickEnabled, onSelected: delegate + { + GameMain.Client?.VoteForKick(client); + }); + } + + options.Add(kickOption); + } + + options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, true); + })); + + GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray()); + } + + #endregion + public bool SelectPlayer(Client selectedClient) { bool myClient = selectedClient.ID == GameMain.Client.ID; @@ -2459,7 +2568,7 @@ namespace Barotrauma private bool ClosePlayerFrame(GUIButton button, object userData) { PlayerFrame = null; - playerList.Deselect(); + PlayerList.Deselect(); return true; } @@ -2520,7 +2629,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) { @@ -2713,7 +2822,8 @@ namespace Barotrauma msg.ClickableAreas.Add(new GUITextBlock.ClickableArea() { Data = data, - OnClick = GameMain.NetLobbyScreen.SelectPlayer + OnClick = GameMain.NetLobbyScreen.SelectPlayer, + OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu }); } } @@ -2752,13 +2862,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 +2928,7 @@ namespace Barotrauma } } - UpdateJobPreferences(); + UpdateJobPreferences(GameMain.Client.CharacterInfo ?? Character.Controlled?.Info); if (moveToNext) { @@ -3014,16 +3124,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 +3153,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 +3249,7 @@ namespace Barotrauma RefreshEnabledElements(); if (enabled) { - modeList.Select(GameModePreset.MultiPlayerCampaign, true); + ModeList.Select(GameModePreset.MultiPlayerCampaign, true); } } @@ -3147,17 +3257,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,11 +3276,12 @@ namespace Barotrauma break; } } - subList.OnSelected += VotableClicked; + SubList.OnSelected += VotableClicked; if (!displayed) { CreateSubPreview(submarine); } + UpdateSubVisibility(); } private bool ViewJobInfo(GUIButton button, object obj) @@ -3194,11 +3305,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 +3344,7 @@ namespace Barotrauma variantButton.OnClicked = (btn, obj) => { btn.Parent.UserData = obj; - UpdateJobPreferences(); + UpdateJobPreferences(characterInfo); return false; }; } @@ -3317,14 +3430,31 @@ namespace Barotrauma return btn; } - public Pair FailedSelectedSub; - public Pair FailedSelectedShuttle; + public readonly struct FailedSubInfo + { + public readonly string Name; + public readonly string Hash; + public FailedSubInfo(string name, string hash) { Name = name; Hash = hash; } + public void Deconstruct(out string name, out string hash) { name = Name; hash = Hash; } - public List> FailedCampaignSubs = new List>(); - public List> FailedOwnedSubs = new List>(); + private static bool StringsEqual(string a, string b) + => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); + + public static bool operator ==(FailedSubInfo a, FailedSubInfo b) + => StringsEqual(a.Name, b.Name) && StringsEqual(a.Hash, b.Hash); + public static bool operator !=(FailedSubInfo a, FailedSubInfo b) + => !(a == b); + } + + public FailedSubInfo? FailedSelectedSub; + public FailedSubInfo? FailedSelectedShuttle; + + public List FailedCampaignSubs = new List(); + public List FailedOwnedSubs = new List(); public bool TrySelectSub(string subName, string md5Hash, GUIListBox subList) { + UpdateSubVisibility(); if (GameMain.Client == null) { return false; } //already downloading the selected sub file @@ -3340,7 +3470,7 @@ namespace Barotrauma //matching sub found and already selected, all good if (sub != null) { - if (subList == this.subList) + if (subList == this.SubList) { CreateSubPreview(sub); } @@ -3389,9 +3519,13 @@ namespace Barotrauma //if we get to this point, a matching sub was not found or it has an incorrect MD5 hash if (subList == SubList) - FailedSelectedSub = new Pair(subName, md5Hash); + { + FailedSelectedSub = new FailedSubInfo(subName, md5Hash); + } else - FailedSelectedShuttle = new Pair(subName, md5Hash); + { + FailedSelectedShuttle = new FailedSubInfo(subName, md5Hash); + } string errorMsg = ""; if (sub == null || !SubmarineInfo.SavedSubmarines.Contains(sub)) @@ -3490,22 +3624,22 @@ namespace Barotrauma { UserData = "request" + serverSubmarine.Name }; - requestFileBox.Buttons[0].UserData = new string[] { serverSubmarine.Name, serverSubmarine.MD5Hash.Hash }; + requestFileBox.Buttons[0].UserData = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.Hash); requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => { - string[] fileInfo = (string[])userdata; + FailedSubInfo fileInfo = (FailedSubInfo)userdata; - if (deliveryData == "owned") + if (deliveryData == "owned") //owned!!!! { - FailedOwnedSubs.Add(new Pair(fileInfo[0], fileInfo[1])); + FailedOwnedSubs.Add(fileInfo); } else if (deliveryData == "campaign") { - FailedCampaignSubs.Add(new Pair(fileInfo[0], fileInfo[1])); + FailedCampaignSubs.Add(fileInfo); } - GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo[0], fileInfo[1]); + GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo.Name, fileInfo.Hash); return true; }; requestFileBox.Buttons[1].OnClicked += requestFileBox.Close; @@ -3533,6 +3667,251 @@ 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.DisplayName) + { + 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) + || (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.Name.Equals(sub.Name, StringComparison.OrdinalIgnoreCase))) + && (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..9c004b7e3 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); @@ -2375,10 +2376,9 @@ namespace Barotrauma } catch (Exception ex) { - string errorMsg = "Failed to ping a server (" + ip + ") - " + (ex?.InnerException?.Message ?? ex.Message); - GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ip, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ip, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); #if DEBUG - DebugConsole.NewMessage(errorMsg, Color.Red); + DebugConsole.NewMessage("Failed to ping a server (" + ip + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); #endif } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 350ff7ac5..94064f687 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) @@ -824,14 +824,13 @@ namespace Barotrauma } catch (Exception e) { - string errorMsg = "Failed to save workshop item preview image to \"" + previewImagePath + "\"."; GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); + GameAnalyticsManager.ErrorSeverity.Error, "Failed to save workshop item preview image.\n" + e.Message); return; } } - private IEnumerable WaitForItemPreviewDownloaded(Steamworks.Ugc.Item? item, GUIListBox listBox, string previewImagePath) + private IEnumerable WaitForItemPreviewDownloaded(Steamworks.Ugc.Item? item, GUIListBox listBox, string previewImagePath) { while (true) { @@ -1195,7 +1194,7 @@ namespace Barotrauma { string errorMsg = "Failed to edit workshop item (content package null)\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.ShowCreateItemFrame:ContentPackageNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.ShowCreateItemFrame:ContentPackageNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -1835,7 +1834,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/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 1fc36bc42..66e9507f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -358,7 +358,7 @@ namespace Barotrauma } else { - displayName = TextManager.Get(fallbackTag, true); + displayName = TextManager.Get(fallbackTag, true) ?? TextManager.Get($"sp.{fallbackTag}.name", true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 45a3e50bf..281b031f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -221,14 +221,14 @@ 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; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); float effectiveGain = gain; - if (category != null) effectiveGain *= Sound.Owner.GetCategoryGainMultiplier(category); + if (category != null) { effectiveGain *= Sound.Owner.GetCategoryGainMultiplier(category); } Al.Sourcef(alSource, Al.Gain, effectiveGain); int alError = Al.GetError(); @@ -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..9c8e2f0f4 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; @@ -470,14 +470,14 @@ namespace Barotrauma { string errorMsg = "Failed to update water ambience volume - submarine's movement value invalid (" + movementSoundVolume + ", sub velocity: " + sub.Velocity + ")"; DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); movementSoundVolume = 0.0f; } if (!MathUtils.IsValid(insideSubFactor)) { string errorMsg = "Failed to update water ambience volume - inside sub value invalid (" + insideSubFactor + ")"; DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); insideSubFactor = 0.0f; } } @@ -609,7 +609,7 @@ namespace Barotrauma flowSoundChannels[i] = FlowSounds[i].Play(1.0f, FlowSoundRange, soundPos); flowSoundChannels[i].Looping = true; } - flowSoundChannels[i].Gain = Math.Max(flowVolumeRight[i], flowVolumeLeft[i]); + flowSoundChannels[i].Gain = Math.Min(Math.Max(flowVolumeRight[i], flowVolumeLeft[i]), 1.0f); flowSoundChannels[i].Position = new Vector3(soundPos, 0.0f); } } @@ -790,7 +790,7 @@ namespace Barotrauma if (sound == null) { string errorMsg = "Error in SoundPlayer.PlaySound (sound was null)\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.PlaySound:SoundNull" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.PlaySound:SoundNull" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return 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/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index a65c33d7b..3d23f6590 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -64,6 +64,7 @@ namespace Barotrauma { float angle = 0.0f; float particleRotation = 0.0f; + bool mirrorAngle = false; if (emitter.Prefab.Properties.CopyEntityAngle) { Limb targetLimb = null; @@ -71,7 +72,11 @@ namespace Barotrauma { angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); particleRotation = -item.body.Rotation; - if (item.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; } + if (item.body.Dir < 0.0f) + { + particleRotation += MathHelper.Pi; + mirrorAngle = true; + } } else if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { @@ -85,11 +90,15 @@ namespace Barotrauma { angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); particleRotation = -targetLimb.body.Rotation; - if (targetLimb.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; } + if (targetLimb.body.Dir < 0.0f) + { + particleRotation += MathHelper.Pi; + mirrorAngle = true; + } } } - emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation); + emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation, mirrorAngle: mirrorAngle); } } @@ -108,7 +117,7 @@ namespace Barotrauma if (sound?.Sound == null) { string errorMsg = $"Error in StatusEffect.ApplyProjSpecific1 (sound \"{sound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling); @@ -135,7 +144,7 @@ namespace Barotrauma if (selectedSound?.Sound == null) { string errorMsg = $"Error in StatusEffect.ApplyProjSpecific2 (sound \"{selectedSound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } if (selectedSound.Sound.Disposed) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 327dac75a..ce055cf1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -421,7 +421,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError("Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace.CleanupStackTrace()); #endif - GameAnalyticsManager.AddErrorEventOnce("ToolBox.GradientLerp:EmptyColorArray", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("ToolBox.GradientLerp:EmptyColorArray", GameAnalyticsManager.ErrorSeverity.Error, "Empty color array passed to the GradientLerp method.\n" + Environment.StackTrace.CleanupStackTrace()); return Color.Black; } @@ -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 041fa9654..e765c91b8 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.21.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -55,6 +55,7 @@ + @@ -99,7 +100,6 @@ - @@ -109,7 +109,6 @@ - @@ -124,7 +123,7 @@ - + @@ -197,4 +196,9 @@ + + linux-x64 + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 3ef501bc4..d69b669ed 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.21.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -58,6 +58,7 @@ + @@ -100,7 +101,6 @@ - @@ -110,7 +110,6 @@ - @@ -125,7 +124,7 @@ - + @@ -208,4 +207,9 @@ + + osx-x64 + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index b54d4721f..52d7a6d1e 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.21.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -63,6 +63,7 @@ + @@ -103,7 +104,6 @@ - @@ -113,7 +113,6 @@ - @@ -128,7 +127,7 @@ - + @@ -228,4 +227,9 @@ + + win-x64 + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/app.manifest b/Barotrauma/BarotraumaClient/app.manifest index ff31a5f7c..54b3a73be 100644 --- a/Barotrauma/BarotraumaClient/app.manifest +++ b/Barotrauma/BarotraumaClient/app.manifest @@ -1,62 +1,62 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true/pm - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + + + + + + + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index bc8f50286..c891813ed 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.21.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -55,6 +55,7 @@ + @@ -62,27 +63,20 @@ - - - - - - - - - ..\..\Libraries\0Harmony.dll - - + + + + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index a2d5ddbc8..64cd14bfc 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.21.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -60,6 +60,7 @@ + @@ -69,18 +70,20 @@ - - + + + + @@ -88,15 +91,6 @@ PreserveNewest - - - - - - - ..\..\Libraries\0Harmony.dll - - diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 653db39e1..d1bd9faf7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -64,5 +64,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 04e53169e..d132e2a59 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) @@ -269,7 +269,7 @@ namespace Barotrauma { string errorMsg = "Failed to write input to command line (window width: " + Console.WindowWidth + ", window height: " + Console.WindowHeight + ")\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("DebugConsole.RewriteInputToCommandLine", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.RewriteInputToCommandLine", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } } @@ -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) @@ -1331,7 +1331,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) { @@ -1393,7 +1393,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(); })); @@ -1415,11 +1415,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); } })); @@ -1594,16 +1601,9 @@ 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); } } ); @@ -1795,7 +1795,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 { @@ -2386,6 +2386,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 253c2ee38..367cef101 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -1,7 +1,6 @@ using Barotrauma.Networking; using Barotrauma.Steam; using FarseerPhysics.Dynamics; -using GameAnalyticsSDK.Net; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -101,8 +100,9 @@ namespace Barotrauma Console.WriteLine("Initializing SteamManager"); SteamManager.Initialize(); - Console.WriteLine("Initializing GameAnalytics"); - if (GameSettings.SendUserStatistics) GameAnalyticsManager.Init(); + //TODO: figure out how consent is supposed to work for servers + //Console.WriteLine("Initializing GameAnalytics"); + //GameAnalyticsManager.InitIfConsented(); Console.WriteLine("Initializing GameScreen"); GameScreen = new GameScreen(); @@ -438,8 +438,8 @@ namespace Barotrauma SaveUtil.CleanUnnecessarySaveFiles(); - if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); } - if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } MainThread = null; } @@ -451,7 +451,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/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 9f3f034b7..8013b965b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -13,7 +13,7 @@ namespace Barotrauma get { return itemData != null; } } - public CharacterCampaignData(Client client, bool giveRespawnPenaltyAffliction = false) + public CharacterCampaignData(Client client) { Name = client.Name; ClientEndPoint = client.Connection.EndPointString; @@ -22,13 +22,6 @@ namespace Barotrauma healthData = new XElement("health"); client.Character?.CharacterHealth?.Save(healthData); - if (giveRespawnPenaltyAffliction) - { - var respawnPenaltyAffliction = RespawnManager.GetRespawnPenaltyAffliction(); - healthData.Add(new XElement("Affliction", - new XAttribute("identifier", respawnPenaltyAffliction.Identifier), - new XAttribute("strength", respawnPenaltyAffliction.Strength.ToString("G", CultureInfo.InvariantCulture)))); - } if (client.Character?.Inventory != null) { itemData = new XElement("inventory"); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index d3c2fbed0..a47a9f704 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -211,18 +211,6 @@ namespace Barotrauma { c.Character = null; } - - if (c.HasSpawned && c.CharacterInfo != null && c.CharacterInfo.CauseOfDeath != null && c.CharacterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) - { - //the client has opted to spawn this round with Reaper's Tax - if (c.WaitForNextRoundRespawn.HasValue && !c.WaitForNextRoundRespawn.Value) - { - c.CharacterInfo.StartItemsGiven = false; - characterData.RemoveAll(cd => cd.MatchesClient(c)); - characterData.Add(new CharacterCampaignData(c, giveRespawnPenaltyAffliction: true)); - continue; - } - } //use the info of the character the client is currently controlling // or the previously saved info if not (e.g. if the client has been spectating or died) var characterInfo = c.Character?.Info ?? characterData.Find(d => d.MatchesClient(c))?.CharacterInfo; @@ -231,6 +219,7 @@ namespace Barotrauma if (characterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { RespawnManager.ReduceCharacterSkills(characterInfo); + characterInfo.RemoveSavedStatValuesOnDeath(); } c.CharacterInfo = characterInfo; characterData.RemoveAll(cd => cd.MatchesClient(c)); @@ -264,7 +253,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 +270,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++; @@ -358,6 +347,7 @@ namespace Barotrauma } } } + UpdateCampaignSubs(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); PendingSubmarineSwitch = null; @@ -365,7 +355,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 +366,7 @@ namespace Barotrauma //-------------------------------------- - GameMain.Server.EndGame(transitionType); + GameMain.Server.EndGame(transitionType, wasSaved: true); ForceMapUI = false; @@ -400,20 +390,53 @@ namespace Barotrauma partial void InitProjSpecific() { - if (GameMain.Server != null) - { - CargoManager.OnItemsInBuyCrateChanged += () => { LastUpdateID++; }; - CargoManager.OnPurchasedItemsChanged += () => { LastUpdateID++; }; - CargoManager.OnSoldItemsChanged += () => { LastUpdateID++; }; - UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; }; - Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; }; - Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; - Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; - } + CargoManager.OnItemsInBuyCrateChanged += () => { LastUpdateID++; }; + CargoManager.OnPurchasedItemsChanged += () => { LastUpdateID++; }; + CargoManager.OnSoldItemsChanged += () => { LastUpdateID++; }; + UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; }; + Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; }; + Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; + Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; + + UpdateCampaignSubs(); + //increment save ID so clients know they're lacking the most up-to-date save file LastSaveID++; } + public static void UpdateCampaignSubs() + { + bool isSubmarineVisible(SubmarineInfo s) + => !GameMain.Server.ServerSettings.HiddenSubs.Any(h + => s.Name.Equals(h, StringComparison.OrdinalIgnoreCase)); + + List availableSubs = + SubmarineInfo.SavedSubmarines + .Where(s => + s.IsCampaignCompatible + && isSubmarineVisible(s)) + .ToList(); + + if (!availableSubs.Any()) + { + //None of the available subs were marked as campaign-compatible, just include all visible subs + availableSubs.AddRange( + SubmarineInfo.SavedSubmarines + .Where(isSubmarineVisible)); + } + + if (!availableSubs.Any()) + { + //No subs are visible at all! Just make the selected one available + availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub); + } + + GameMain.NetLobbyScreen.CampaignSubmarines = availableSubs; + } + + public bool CanPurchaseSub(SubmarineInfo info) + => info.Price <= Money && GameMain.NetLobbyScreen.CampaignSubmarines.Contains(info); + public void DiscardClientCharacterData(Client client) { characterData.RemoveAll(cd => cd.MatchesClient(client)); @@ -1030,14 +1053,6 @@ namespace Barotrauma new XAttribute("points", savedExperiencePoint.ExperiencePoints))); } - // save available submarines - XElement availableSubsElement = new XElement("AvailableSubs"); - for (int i = 0; i < GameMain.NetLobbyScreen.CampaignSubmarines.Count; i++) - { - availableSubsElement.Add(new XElement("Sub", new XAttribute("name", GameMain.NetLobbyScreen.CampaignSubmarines[i].Name))); - } - modeElement.Add(availableSubsElement); - element.Add(modeElement); //save character data to a separate file 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/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 129d6e622..23c145bbf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -14,6 +14,7 @@ namespace Barotrauma.Items.Components { if (c.Character == null) { return; } var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); + var QTESuccess = msg.ReadBoolean(); if (requestedFixAction != FixActions.None) { if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage) @@ -31,6 +32,11 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } } + else + { + RepairBoost(QTESuccess); + item.CreateServerEvent(this); + } } public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) 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..07117c718 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -41,7 +41,7 @@ namespace Barotrauma } msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:InvalidData" + Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:InvalidData" + Name, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -186,7 +186,7 @@ namespace Barotrauma msg.LengthBits = initialWritePos; msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:" + errorMsg, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:" + errorMsg, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } } @@ -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); @@ -402,7 +402,7 @@ namespace Barotrauma { string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -423,7 +423,7 @@ namespace Barotrauma { string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } 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/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 4a95adecd..ad2438c2d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -274,6 +274,8 @@ namespace Barotrauma.Networking public void Save() { GameServer.Log("Saving banlist", ServerLog.MessageType.ServerMessage); + + GameMain.Server?.ServerSettings?.UpdateFlag(ServerSettings.NetFlags.Properties); bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value); @@ -344,7 +346,7 @@ namespace Barotrauma.Networking catch (Exception e) { string errorMsg = "Error while writing banlist. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Banlist.ServerAdminWrite", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Banlist.ServerAdminWrite", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index cfb20d850..6e7a4fc84 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -10,6 +10,9 @@ namespace Barotrauma.Networking public UInt16 LastRecvClientListUpdate = 0; + public UInt16 LastSentServerSettingsUpdate = 0; + public UInt16 LastRecvServerSettingsUpdate = 0; + public UInt16 LastRecvLobbyUpdate = 0; public UInt16 LastSentChatMsgID = 0; //last msg this client said @@ -133,12 +136,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..e1a4c0d60 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) @@ -271,7 +278,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("FileSender threw an exception when trying to send data", e); GameAnalyticsManager.AddErrorEventOnce( "FileSender.Update:Exception", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); transfer.Status = FileTransferStatus.Error; return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 5bb6def3f..daf732cf4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -77,6 +77,11 @@ namespace Barotrauma.Networking private readonly ServerEntityEventManager entityEventManager; private FileSender fileSender; + + public FileSender FileSender + { + get { return fileSender; } + } #if DEBUG public void PrintSenderTransters() { @@ -141,7 +146,7 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(StartServer(isPublic)); } - private IEnumerable StartServer(bool isPublic) + private IEnumerable StartServer(bool isPublic) { bool error = false; try @@ -401,7 +406,7 @@ namespace Barotrauma.Networking 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) { @@ -496,7 +501,7 @@ namespace Barotrauma.Networking else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode)) { #if !DEBUG - endRoundDelay = 1.0f; + endRoundDelay = 2.0f; endRoundTimer += deltaTime; #endif } @@ -527,7 +532,7 @@ namespace Barotrauma.Networking { Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage); } - EndGame(); + EndGame(wasSaved: false); return; } } @@ -605,16 +610,19 @@ namespace Barotrauma.Networking //constantly increase AFK timer if the client is controlling a character (gets reset to zero every time an input is received) if (gameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated) { - if (c.Connection != OwnerConnection) c.KickAFKTimer += deltaTime; + if (c.Connection != OwnerConnection && c.Permissions != ClientPermissions.All) { c.KickAFKTimer += deltaTime; } } } - IEnumerable kickAFK = connectedClients.FindAll(c => - c.KickAFKTimer >= serverSettings.KickAFKTime && - (OwnerConnection == null || c.Connection != OwnerConnection)); - foreach (Client c in kickAFK) + if (connectedClients.Any(c => c.KickAFKTimer >= serverSettings.KickAFKTime)) { - KickClient(c, "DisconnectMessage.AFK"); + IEnumerable kickAFK = connectedClients.FindAll(c => + c.KickAFKTimer >= serverSettings.KickAFKTime && + (OwnerConnection == null || c.Connection != OwnerConnection)); + foreach (Client c in kickAFK) + { + KickClient(c, "DisconnectMessage.AFK"); + } } serverPeer.Update(deltaTime); @@ -637,7 +645,7 @@ namespace Barotrauma.Networking { DebugConsole.ThrowError("Failed to write a network message for the client \"" + c.Name + "\"!", e); - string errorMsg = "Failed to write a network message for the client \"" + c.Name + "\"! (MidRoundSyncing: " + c.NeedsMidRoundSync + ")\n" + string errorMsg = "Failed to write a network message for a client! (MidRoundSyncing: " + c.NeedsMidRoundSync + ")\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); if (e.InnerException != null) { @@ -646,7 +654,7 @@ namespace Barotrauma.Networking GameAnalyticsManager.AddErrorEventOnce( "GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(), - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } } @@ -796,6 +804,8 @@ namespace Barotrauma.Networking string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { + ServerSettings.RadiationEnabled = settings.RadiationEnabled; + ServerSettings.MaxMissionCount = settings.MaxMissionCount; MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); } } @@ -862,6 +872,7 @@ namespace Barotrauma.Networking private void HandleClientError(IReadMessage inc, Client c) { string errorStr = "Unhandled error report"; + string errorStrNoName = errorStr; ClientNetError error = (ClientNetError)inc.ReadByte(); switch (error) @@ -869,7 +880,7 @@ namespace Barotrauma.Networking case ClientNetError.MISSING_EVENT: UInt16 expectedID = inc.ReadUInt16(); UInt16 receivedID = inc.ReadUInt16(); - errorStr = "Expecting event id " + expectedID.ToString() + ", received " + receivedID.ToString(); + errorStr = errorStrNoName = "Expecting event id " + expectedID.ToString() + ", received " + receivedID.ToString(); break; case ClientNetError.MISSING_ENTITY: UInt16 eventID = inc.ReadUInt16(); @@ -877,25 +888,26 @@ namespace Barotrauma.Networking Entity entity = Entity.FindEntityByID(entityID); if (entity == null) { - errorStr = "Received an update for an entity that doesn't exist (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = errorStrNoName = "Received an update for an entity that doesn't exist (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } else if (entity is Character character) { errorStr = "Missing character " + character.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStrNoName = "Missing character " + character.SpeciesName + "(event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } else if (entity is Item item) { - errorStr = "Missing item " + item.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = errorStrNoName = "Missing item " + item.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } else { - errorStr = "Missing entity " + entity.ToString() + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = errorStrNoName = "Missing entity " + entity.ToString() + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; } break; } - Log(GameServer.ClientLogName(c) + " has reported an error: " + errorStr, ServerLog.MessageType.Error); - GameAnalyticsManager.AddErrorEventOnce("GameServer.HandleClientError:" + errorStr, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorStr); + Log(ClientLogName(c) + " has reported an error: " + errorStr, ServerLog.MessageType.Error); + GameAnalyticsManager.AddErrorEventOnce("GameServer.HandleClientError:" + errorStrNoName, GameAnalyticsManager.ErrorSeverity.Error, errorStr); try { @@ -909,7 +921,7 @@ namespace Barotrauma.Networking if (c.Connection == OwnerConnection) { SendDirectChatMessage(errorStr, c, ChatMessageType.MessageBox); - EndGame(); + EndGame(wasSaved: false); } else { @@ -1002,8 +1014,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() @@ -1034,6 +1049,11 @@ namespace Barotrauma.Networking case ClientNetObject.SYNC_IDS: //TODO: might want to use a clever class for this c.LastRecvLobbyUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvLobbyUpdate, GameMain.NetLobbyScreen.LastUpdateID); + if (c.HasPermission(ClientPermissions.ManageSettings) && + NetIdUtils.IdMoreRecentOrMatches(c.LastRecvLobbyUpdate, c.LastSentServerSettingsUpdate)) + { + c.LastRecvServerSettingsUpdate = c.LastSentServerSettingsUpdate; + } c.LastRecvChatMsgID = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvChatMsgID, c.LastChatMsgQueueID); c.LastRecvClientListUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.LastRecvClientListUpdate, LastClientListUpdateID); @@ -1142,6 +1162,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 { @@ -1328,18 +1350,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 @@ -1390,49 +1417,23 @@ namespace Barotrauma.Networking } break; case ClientPermissions.SelectSub: - bool isCampaign = inc.ReadBoolean(); - if (!isCampaign) + bool isShuttle = inc.ReadBoolean(); + inc.ReadPadBits(); + UInt16 subIndex = inc.ReadUInt16(); + var subList = GameMain.NetLobbyScreen.GetSubList(); + if (subIndex >= subList.Count) { - bool isShuttle = inc.ReadBoolean(); - inc.ReadPadBits(); - UInt16 subIndex = inc.ReadUInt16(); - var subList = GameMain.NetLobbyScreen.GetSubList(); - if (subIndex >= subList.Count) - { - DebugConsole.NewMessage("Client \"" + GameServer.ClientLogName(sender) + "\" attempted to select a sub, index out of bounds (" + subIndex + ")", Color.Red); - } - else - { - if (isShuttle) - { - GameMain.NetLobbyScreen.SelectedShuttle = subList[subIndex]; - } - else - { - GameMain.NetLobbyScreen.SelectedSub = subList[subIndex]; - } - } + DebugConsole.NewMessage($"Client \"{ClientLogName(sender)}\" attempted to select a sub, index out of bounds ({subIndex})", Color.Red); } else { - int subEqualityCheckVal = inc.ReadInt32(); - bool add = inc.ReadBoolean(); - SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == subEqualityCheckVal); - - if (sub == null) + if (isShuttle) { - DebugConsole.NewMessage("Client \"" + GameServer.ClientLogName(sender) + "\" attempted to select a sub that does not exist on the server!", Color.Red); + GameMain.NetLobbyScreen.SelectedShuttle = subList[subIndex]; } else { - if (add) - { - GameMain.NetLobbyScreen.AddCampaignSubmarine(sub); - } - else - { - GameMain.NetLobbyScreen.RemoveCampaignSubmarine(sub); - } + GameMain.NetLobbyScreen.SelectedSub = subList[subIndex]; } } break; @@ -1749,7 +1750,7 @@ namespace Barotrauma.Networking " Chat message size: " + chatMessageBytes + " bytes\n" + " Position update size: " + positionUpdateBytes + " bytes\n\n"; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); @@ -1789,7 +1790,7 @@ namespace Barotrauma.Networking } DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); @@ -1876,7 +1877,7 @@ namespace Barotrauma.Networking List campaignSubIndices = new List(); if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - List subList = GameMain.NetLobbyScreen.GetSubList(); + IReadOnlyList subList = GameMain.NetLobbyScreen.GetSubList(); for (int i = 0; i < subList.Count; i++) { if (GameMain.NetLobbyScreen.CampaignSubmarines.Contains(subList[i])) @@ -1914,9 +1915,6 @@ namespace Barotrauma.Networking { outmsg.Write(autoRestartTimerRunning ? serverSettings.AutoRestartTimer : 0.0f); } - - outmsg.Write(serverSettings.RadiationEnabled); - outmsg.Write((byte)serverSettings.MaxMissionCount); } else { @@ -1956,8 +1954,31 @@ namespace Barotrauma.Networking chatMessageBytes = outmsg.LengthBytes - outmsg.LengthBytes; outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); - - if (isInitialUpdate) + + bool messageTooLarge = outmsg.LengthBytes > MsgConstants.MTU; + if (messageTooLarge && !isInitialUpdate) + { + string warningMsg = "Maximum packet size exceeded, will send using reliable mode (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; + warningMsg += + " Client list size: " + clientListBytes + " bytes\n" + + " Chat message size: " + chatMessageBytes + " bytes\n" + + " Campaign size: " + campaignBytes + " bytes\n" + + " Settings size: " + settingsBytes + " bytes\n"; + if (initialUpdateBytes > 0) + { + warningMsg += + " Initial update size: " + settingsBuf.LengthBytes + " bytes\n"; + } + if (settingsBuf != null) + { + warningMsg += + " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; + } + if (GameSettings.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } + + if (isInitialUpdate || messageTooLarge) { //the initial update may be very large if the host has a large number //of submarine files, so the message may have to be fragmented @@ -1973,28 +1994,6 @@ namespace Barotrauma.Networking } else { - if (outmsg.LengthBytes > MsgConstants.MTU) - { - string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU + ")\n"; - errorMsg += - " Client list size: " + clientListBytes + " bytes\n" + - " Chat message size: " + chatMessageBytes + " bytes\n" + - " Campaign size: " + campaignBytes + " bytes\n" + - " Settings size: " + settingsBytes + " bytes\n"; - if (initialUpdateBytes > 0) - { - errorMsg += - " Initial update size: " + settingsBuf.LengthBytes + " bytes\n"; - } - if (settingsBuf != null) - { - errorMsg += - " Settings buffer size: " + settingsBuf.LengthBytes + " bytes\n"; - } - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - } - serverPeer.Send(outmsg, c.Connection, DeliveryMethod.Unreliable); } } @@ -2018,9 +2017,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); @@ -2050,7 +2049,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; @@ -2092,7 +2091,6 @@ namespace Barotrauma.Networking while (fileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) { waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime; - yield return CoroutineStatus.Running; } } @@ -2103,7 +2101,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(); @@ -2120,7 +2118,7 @@ namespace Barotrauma.Networking startGameCoroutine = null; string errorMsg = "Starting the round failed. Campaign was still active, but the map has been disposed. Try selecting another game mode."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); if (OwnerConnection != null) { SendDirectChatMessage(errorMsg, connectedClients.Find(c => c.Connection == OwnerConnection), ChatMessageType.Error); @@ -2162,7 +2160,7 @@ namespace Barotrauma.Networking { string errorMsg = "Failed to start a campaign round (next level not set)."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); if (OwnerConnection != null) { SendDirectChatMessage(errorMsg, connectedClients.Find(c => c.Connection == OwnerConnection), ChatMessageType.Error); @@ -2398,6 +2396,22 @@ namespace Barotrauma.Networking // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway spawnedCharacter.LoadTalents(); } + + spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; + spawnedCharacter.OwnerClientName = teamClients[i].Name; + } + + for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) + { + Character spawnedCharacter = Character.Create(characterInfos[i], spawnWaypoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: false, hasAi: true); + spawnedCharacter.TeamID = teamID; + spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); + spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); + spawnedCharacter.Info.InventoryData = new XElement("inventory"); + spawnedCharacter.Info.StartItemsGiven = true; + spawnedCharacter.SaveInventory(); + // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway + spawnedCharacter.LoadTalents(); } } @@ -2498,8 +2512,9 @@ namespace Barotrauma.Networking msg.Write(serverSettings.AllowFriendlyFire); msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); + msg.Write(serverSettings.AllowLinkingWifiToChat); msg.Write(serverSettings.UseRespawnShuttle); - msg.Write((byte)GameMain.Config.LosMode); + msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); serverSettings.WriteMonsterEnabled(msg); @@ -2523,6 +2538,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); @@ -2574,7 +2590,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) { @@ -2638,6 +2654,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) @@ -2967,7 +2984,7 @@ namespace Barotrauma.Networking { string errorMsg = "Attempted to send a chat message to a null client.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.SendDirectChatMessage:ClientNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.SendDirectChatMessage:ClientNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -3278,7 +3295,7 @@ namespace Barotrauma.Networking BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(serverSettings.AutoBanTime)); } - GameMain.NetLobbyScreen.LastUpdateID++; + //GameMain.NetLobbyScreen.LastUpdateID++; SendVoteStatus(connectedClients); @@ -3286,7 +3303,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); } } @@ -3368,7 +3385,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..d50e6209f 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); @@ -201,7 +186,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError(errorMsg, e); } GameAnalyticsManager.AddErrorEventOnce("ServerEntityEventManager.Read:ReadFailed" + entityName, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace.CleanupStackTrace()); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index c1119f6e1..f373add21 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -129,7 +129,7 @@ namespace Barotrauma.Networking catch (Exception e) { string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("LidgrenServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("LidgrenServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #else @@ -212,15 +212,13 @@ namespace Barotrauma.Networking PendingClient pendingClient = pendingClients.Find(c => (c.Connection is LidgrenConnection l) && l.NetConnection == inc.SenderConnection); - byte incByte = inc.ReadByte(); - bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; - bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; + PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); - if (isConnectionInitializationStep && pendingClient != null) + if (packetHeader.IsConnectionInitializationStep() && pendingClient != null) { ReadConnectionInitializationStep(pendingClient, new ReadWriteMessage(inc.Data, (int)inc.Position, inc.LengthBits, false)); } - else if (!isConnectionInitializationStep) + else if (!packetHeader.IsConnectionInitializationStep()) { LidgrenConnection conn = connectedClients.Find(c => (c is LidgrenConnection l) && l.NetConnection == inc.SenderConnection) as LidgrenConnection; if (conn == null) @@ -246,7 +244,7 @@ namespace Barotrauma.Networking //DebugConsole.NewMessage(isCompressed + " " + isConnectionInitializationStep + " " + (int)incByte + " " + length); - IReadMessage msg = new ReadOnlyMessage(inc.Data, isCompressed, inc.PositionInBytes, length, conn); + IReadMessage msg = new ReadOnlyMessage(inc.Data, packetHeader.IsCompressed(), inc.PositionInBytes, length, conn); OnMessageReceived?.Invoke(conn, msg); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index c7b85305c..6af0c5221 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -102,7 +102,7 @@ namespace Barotrauma.Networking catch (Exception e) { string errorMsg = "Server failed to read an incoming message. {" + e + "}\n" + e.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("SteamP2PServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("SteamP2PServerPeer.Update:ClientReadException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #else @@ -125,14 +125,9 @@ namespace Barotrauma.Networking UInt64 senderSteamId = inc.ReadUInt64(); UInt64 ownerSteamId = inc.ReadUInt64(); - byte incByte = inc.ReadByte(); - bool isCompressed = (incByte & (byte)PacketHeader.IsCompressed) != 0; - bool isConnectionInitializationStep = (incByte & (byte)PacketHeader.IsConnectionInitializationStep) != 0; - bool isDisconnectMessage = (incByte & (byte)PacketHeader.IsDisconnectMessage) != 0; - bool isServerMessage = (incByte & (byte)PacketHeader.IsServerMessage) != 0; - bool isHeartbeatMessage = (incByte & (byte)PacketHeader.IsHeartbeatMessage) != 0; + PacketHeader packetHeader = (PacketHeader)inc.ReadByte(); - if (isServerMessage) + if (packetHeader.IsServerMessage()) { DebugConsole.ThrowError("Got server message from" + senderSteamId.ToString()); return; @@ -160,7 +155,7 @@ namespace Barotrauma.Networking } return; } - else if (isDisconnectMessage) + else if (packetHeader.IsDisconnectMessage()) { if (pendingClient != null) { @@ -174,12 +169,12 @@ namespace Barotrauma.Networking } return; } - else if (isHeartbeatMessage) + else if (packetHeader.IsHeartbeatMessage()) { //message exists solely as a heartbeat, ignore its contents return; } - else if (isConnectionInitializationStep) + else if (packetHeader.IsConnectionInitializationStep()) { if (pendingClient != null) @@ -203,7 +198,7 @@ namespace Barotrauma.Networking { UInt16 length = inc.ReadUInt16(); - IReadMessage msg = new ReadOnlyMessage(inc.Buffer, isCompressed, inc.BytePosition, length, connectedClient); + IReadMessage msg = new ReadOnlyMessage(inc.Buffer, packetHeader.IsCompressed(), inc.BytePosition, length, connectedClient); OnMessageReceived?.Invoke(connectedClient, msg); } } @@ -211,17 +206,17 @@ namespace Barotrauma.Networking { if (OwnerConnection != null) { (OwnerConnection as SteamP2PConnection).Heartbeat(); } - if (isDisconnectMessage) + if (packetHeader.IsDisconnectMessage()) { DebugConsole.ThrowError("Received disconnect message from owner"); return; } - if (isServerMessage) + if (packetHeader.IsServerMessage()) { DebugConsole.ThrowError("Received server message from owner"); return; } - if (isConnectionInitializationStep) + if (packetHeader.IsConnectionInitializationStep()) { if (OwnerConnection == null) { @@ -236,7 +231,7 @@ namespace Barotrauma.Networking } return; } - if (isHeartbeatMessage) + if (packetHeader.IsHeartbeatMessage()) { return; } @@ -244,7 +239,7 @@ namespace Barotrauma.Networking { UInt16 length = inc.ReadUInt16(); - IReadMessage msg = new ReadOnlyMessage(inc.Buffer, isCompressed, inc.BytePosition, length, OwnerConnection); + IReadMessage msg = new ReadOnlyMessage(inc.Buffer, packetHeader.IsCompressed(), inc.BytePosition, length, OwnerConnection); OnMessageReceived?.Invoke(OwnerConnection, msg); } } @@ -267,7 +262,7 @@ namespace Barotrauma.Networking } IWriteMessage msgToSend = new WriteOnlyMessage(); - byte[] msgData = new byte[msg.LengthBytes]; + byte[] msgData = new byte[16]; msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); msgToSend.Write(conn.SteamID); msgToSend.Write((byte)deliveryMethod); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index fb3785123..c349de07d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -52,6 +52,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) @@ -325,7 +348,7 @@ namespace Barotrauma.Networking c.WaitForNextRoundRespawn = null; var matchingData = campaign?.GetClientCharacterData(c); - if (matchingData != null && !matchingData.HasSpawned) + if (matchingData != null) { c.CharacterInfo = matchingData.CharacterInfo; } @@ -396,6 +419,7 @@ namespace Barotrauma.Networking else { ReduceCharacterSkills(characterInfos[i]); + characterInfos[i].RemoveSavedStatValuesOnDeath(); } } } @@ -514,9 +538,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); } } @@ -532,9 +556,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..cb0a2582f 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 { @@ -13,6 +14,21 @@ namespace Barotrauma.Networking public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; public static readonly char SubmarineSeparatorChar = '|'; + public readonly Dictionary LastUpdateIdForFlag = new Dictionary(); + public UInt16 LastPropertyUpdateId { get; private set; } = 1; + + public void UpdateFlag(NetFlags flag) + => LastUpdateIdForFlag[flag] = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); + + private bool IsFlagRequired(Client c, NetFlags flag) + => LastUpdateIdForFlag[flag] > c.LastRecvLobbyUpdate; + + public NetFlags GetRequiredFlags(Client c) + => LastUpdateIdForFlag.Keys + .Where(k => IsFlagRequired(c, k)) + .Concat(NetFlags.None.ToEnumerable()) //prevents InvalidOperationException in Aggregate + .Aggregate((f1, f2) => f1 | f2); + partial void InitProjSpecific() { LoadSettings(); @@ -31,11 +47,7 @@ namespace Barotrauma.Networking public void ServerAdminWrite(IWriteMessage outMsg, Client c) { - //outMsg.Write(isPublic); - //outMsg.Write(EnableUPnP); - //outMsg.WritePadBits(); - //outMsg.Write((UInt16)QueryPort); - + c.LastSentServerSettingsUpdate = LastPropertyUpdateId; WriteNetProperties(outMsg); WriteMonsterEnabled(outMsg); BanList.ServerAdminWrite(outMsg, c); @@ -44,8 +56,18 @@ namespace Barotrauma.Networking public void ServerWrite(IWriteMessage outMsg, Client c) { - outMsg.Write(ServerName); - outMsg.Write(ServerMessageText); + NetFlags requiredFlags = GetRequiredFlags(c); + outMsg.Write((byte)requiredFlags); + if (requiredFlags.HasFlag(NetFlags.Name)) + { + outMsg.Write(ServerName); + } + + if (requiredFlags.HasFlag(NetFlags.Message)) + { + outMsg.Write(ServerMessageText); + } + outMsg.Write((byte)PlayStyle); outMsg.Write((byte)MaxPlayers); outMsg.Write(HasPassword); outMsg.Write(IsPublic); @@ -53,11 +75,15 @@ namespace Barotrauma.Networking outMsg.WritePadBits(); outMsg.WriteRangedInteger(TickRate, 1, 60); - WriteExtraCargo(outMsg); + if (requiredFlags.HasFlag(NetFlags.Properties)) + { + WriteExtraCargo(outMsg); + } + + WriteHiddenSubs(outMsg); - Voting.ServerWrite(outMsg); - - if (c.HasPermission(Networking.ClientPermissions.ManageSettings)) + if (c.HasPermission(Networking.ClientPermissions.ManageSettings) + && !NetIdUtils.IdMoreRecentOrMatches(c.LastRecvServerSettingsUpdate, LastPropertyUpdateId)) { outMsg.Write(true); outMsg.WritePadBits(); @@ -82,20 +108,20 @@ namespace Barotrauma.Networking if (flags.HasFlag(NetFlags.Name)) { string serverName = incMsg.ReadString(); - if (ServerName != serverName) changed = true; + if (ServerName != serverName) { changed = true; } ServerName = serverName; } if (flags.HasFlag(NetFlags.Message)) { string serverMessageText = incMsg.ReadString(); - if (ServerMessageText != serverMessageText) changed = true; + if (ServerMessageText != serverMessageText) { changed = true; } ServerMessageText = serverMessageText; } if (flags.HasFlag(NetFlags.Properties)) { - changed |= ReadExtraCargo(incMsg); + bool propertiesChanged = ReadExtraCargo(incMsg); UInt32 count = incMsg.ReadUInt32(); @@ -111,7 +137,7 @@ namespace Barotrauma.Networking { GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); } - changed = true; + propertiesChanged = true; } else { @@ -121,12 +147,25 @@ namespace Barotrauma.Networking } bool changedMonsterSettings = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - changed |= changedMonsterSettings; - if (changedMonsterSettings) ReadMonsterEnabled(incMsg); - changed |= BanList.ServerAdminRead(incMsg, c); - changed |= Whitelist.ServerAdminRead(incMsg, c); + propertiesChanged |= changedMonsterSettings; + if (changedMonsterSettings) { ReadMonsterEnabled(incMsg); } + propertiesChanged |= BanList.ServerAdminRead(incMsg, c); + propertiesChanged |= Whitelist.ServerAdminRead(incMsg, c); + + if (propertiesChanged) + { + UpdateFlag(NetFlags.Properties); + LastPropertyUpdateId = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); + } + changed |= propertiesChanged; } + 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; @@ -166,12 +205,14 @@ namespace Barotrauma.Networking MaxMissionCount = MathHelper.Clamp(maxMissionCount, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); changed |= true; + UpdateFlag(NetFlags.Misc); } if (flags.HasFlag(NetFlags.LevelSeed)) { GameMain.NetLobbyScreen.LevelSeed = incMsg.ReadString(); changed |= true; + UpdateFlag(NetFlags.LevelSeed); } if (changed) @@ -205,6 +246,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 +286,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 +301,10 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); + HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); + + SelectedSubmarine = SelectNonHiddenSubmarine(SelectedSubmarine); + string[] defaultAllowedClientNameChars = new string[] { "32-33", @@ -327,7 +379,6 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode); GameMain.NetLobbyScreen.SetBotCount(BotCount); - GameMain.NetLobbyScreen.SetMaxMissionCount(MaxMissionCount); List monsterNames = CharacterPrefab.Prefabs.Select(p => p.Identifier).ToList(); MonsterEnabled = new Dictionary(); @@ -337,6 +388,27 @@ namespace Barotrauma.Networking } } + public string SelectNonHiddenSubmarine(string current = null) + { + current ??= GameMain.NetLobbyScreen.SelectedSub.Name; + if (HiddenSubs.Contains(current)) + { + var candidates + = GameMain.NetLobbyScreen.GetSubList().Where(s => !HiddenSubs.Contains(s.Name)).ToArray(); + if (candidates.Any()) + { + GameMain.NetLobbyScreen.SelectedSub = candidates.GetRandom(Rand.RandSync.Unsynced); + return GameMain.NetLobbyScreen.SelectedSub.Name; + } + else + { + HiddenSubs.Remove(current); + return current; + } + } + return current; + } + public void LoadClientPermissions() { ClientPermissions.Clear(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 889f82de4..8d37c948a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -30,10 +30,9 @@ namespace Barotrauma public static SubmarineVote SubVote; - private void StartSubmarineVote(IReadMessage inc, VoteType voteType, Client sender) + private void StartSubmarineVote(SubmarineInfo subInfo, VoteType voteType, Client sender) { - string subName = inc.ReadString(); - SubVote.Sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); + SubVote.Sub = subInfo; SubVote.DeliveryFee = voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0; SubVote.VoteType = voteType; SubVote.State = VoteState.Started; @@ -130,7 +129,12 @@ namespace Barotrauma bool startVote = inc.ReadBoolean(); if (startVote) { - StartSubmarineVote(inc, voteType, sender); + string subName = inc.ReadString(); + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && campaign.CanPurchaseSub(subInfo)) + { + StartSubmarineVote(subInfo, voteType, sender); + } } else { @@ -155,23 +159,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/Networking/WhiteList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs index aaaa0894f..0e6d43c44 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs @@ -69,6 +69,8 @@ namespace Barotrauma.Networking { GameServer.Log("Saving whitelist", ServerLog.MessageType.ServerMessage); + GameMain.Server?.ServerSettings?.UpdateFlag(ServerSettings.NetFlags.Properties); + List lines = new List(); if (Enabled) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index bf83d1bf8..da8cef3db 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -1,13 +1,13 @@ #region Using Statements using Barotrauma.Steam; -using GameAnalyticsSDK.Net; using System; using Barotrauma.IO; using System.Linq; using System.Text; -using System.Threading; +#if LINUX using System.Runtime.InteropServices; +#endif #endregion @@ -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."); } @@ -60,7 +60,7 @@ namespace Barotrauma Game = new GameMain(args); Game.Run(); - if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } + if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } SteamManager.ShutDown(); } @@ -156,11 +156,18 @@ namespace Barotrauma } } + if (GameAnalyticsManager.SendUserStatistics) + { + //send crash report before appending debug console messages (which may contain non-anonymous information) + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, sb.ToString()); + GameAnalyticsManager.ShutDown(); + } + 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(); @@ -171,12 +178,12 @@ namespace Barotrauma } Console.Write(crashReport); - File.WriteAllText(filePath,sb.ToString()); + File.WriteAllText(filePath, sb.ToString()); - if (GameSettings.SendUserStatistics) + if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + + if (GameAnalyticsManager.SendUserStatistics) { - GameAnalytics.AddErrorEvent(EGAErrorSeverity.Critical, crashReport); - GameAnalytics.OnQuit(); Console.Write("A crash report (\"servercrashreport.log\") was saved in the root folder of the game and sent to the developers."); } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 6028b1fc7..e795f0ace 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -32,6 +32,7 @@ namespace Barotrauma set { selectedShuttle = value; lastUpdateID++; } } + [Obsolete("TODO: this list shouldn't exist, the client should just use the visible subs list instead")] public List CampaignSubmarines { get @@ -51,42 +52,6 @@ namespace Barotrauma private List campaignSubmarines; - public void AddCampaignSubmarine(SubmarineInfo sub) - { - if (!campaignSubmarines.Contains(sub)) - { - campaignSubmarines.Add(sub); - } - else - { - return; - } - - lastUpdateID++; - if (GameMain.NetworkMember?.ServerSettings != null) - { - GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true; - } - } - - public void RemoveCampaignSubmarine(SubmarineInfo sub) - { - if (campaignSubmarines.Contains(sub)) - { - campaignSubmarines.Remove(sub); - } - else - { - return; - } - - lastUpdateID++; - if (GameMain.NetworkMember?.ServerSettings != null) - { - GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true; - } - } - public GameModePreset[] GameModes { get; } private int selectedModeIndex; @@ -212,10 +177,7 @@ namespace Barotrauma } private List subs; - public List GetSubList() - { - return subs; - } + public IReadOnlyList GetSubList() => subs; public void AddSub(SubmarineInfo sub) { @@ -276,6 +238,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 9398e9cc8..0d6117ecf 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.21.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -61,6 +61,7 @@ + @@ -69,7 +70,6 @@ - @@ -77,20 +77,12 @@ - - - - - - - - ..\..\Libraries\0Harmony.dll - + diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index e317ddd13..d33456484 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -78,6 +78,7 @@ + @@ -134,6 +135,7 @@ + @@ -204,7 +206,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/DeployGameAnalytics.props b/Barotrauma/BarotraumaShared/DeployGameAnalytics.props new file mode 100644 index 000000000..7c7f3bac4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/DeployGameAnalytics.props @@ -0,0 +1,18 @@ + + + + false + true + + + true + false + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs index 060108b4b..77339f0b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs @@ -1,8 +1,6 @@ using Microsoft.Xna.Framework; -using NLog.Targets; using System; using System.Collections.Generic; -using System.Linq; namespace Barotrauma { @@ -64,7 +62,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/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 35ffe424e..9c942ee08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -1,5 +1,4 @@ -using Barotrauma.Extensions; -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -125,6 +124,8 @@ namespace Barotrauma minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength)); } + public virtual void OnHealed(Character healer, float healAmount) { } + public virtual void OnAttacked(Character attacker, AttackResult attackResult) { } public virtual void SelectTarget(AITarget target) { } @@ -306,14 +307,15 @@ namespace Barotrauma public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea); - public void UnequipContainedItems(Item parentItem, Func predicate = null, bool avoidDroppingInSea = true) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea); + public void UnequipContainedItems(Item parentItem, Func predicate = null, bool avoidDroppingInSea = true, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, unequipMax); public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea); - public static void UnequipContainedItems(Character character, Item parentItem, Func predicate, bool avoidDroppingInSea = true) + public static void UnequipContainedItems(Character character, Item parentItem, Func predicate, bool avoidDroppingInSea = true, int? unequipMax = null) { var inventory = parentItem.OwnInventory; if (inventory == null) { return; } + int removed = 0; if (predicate == null || inventory.AllItems.Any(predicate)) { foreach (Item containedItem in inventory.AllItemsMod) @@ -326,10 +328,12 @@ namespace Barotrauma // If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it. if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) { + if (unequipMax.HasValue && ++removed >= unequipMax) { return; } continue; } } containedItem.Drop(character); + if (unequipMax.HasValue && ++removed >= unequipMax) { return; } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index b105f540e..a5d3ca142 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -76,7 +76,7 @@ namespace Barotrauma { string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } sectorDir = value; @@ -125,7 +125,7 @@ namespace Barotrauma DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } @@ -144,7 +144,7 @@ namespace Barotrauma DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); #endif GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } @@ -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/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 611ba59f0..0500a03c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -339,11 +339,15 @@ namespace Barotrauma { targetingTag = "dead"; } + else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold > Character.GetDamageDoneByAttacker(targetCharacter)) + { + targetingTag = tp.Tag; + } else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner) { - targetingTag = "owner"; + targetingTag = "owner"; } - else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP)) + else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP)) { targetingTag = tP.Tag; } @@ -353,7 +357,7 @@ namespace Barotrauma { targetingTag = "husk"; } - else + else if (!Character.IsFriendly(targetCharacter)) { if (enemy.CombatStrength > CombatStrength) { @@ -386,6 +390,10 @@ namespace Barotrauma { targetingTag = "sonar"; } + if (targetItem.GetComponent() != null) + { + targetingTag = "door"; + } } } else if (aiTarget.Entity is Structure) @@ -511,8 +519,7 @@ namespace Barotrauma } else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0) { - CharacterParams.TargetParams targetingParams = null; - UpdateTargets(Character, out targetingParams); + UpdateTargets(out CharacterParams.TargetParams targetingParams); updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); if (SelectedAiTarget == null) { @@ -1973,7 +1980,7 @@ namespace Barotrauma ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } - else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams)) + else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker, out CharacterParams.TargetParams targetingParams)) { if (targetingParams.State == AIState.Aggressive || targetingParams.State == AIState.PassiveAggressive) { @@ -2362,7 +2369,7 @@ namespace Barotrauma //goes through all the AItargets, evaluates how preferable it is to attack the target, //whether the Character can see/hear the target and chooses the most preferable target within //sight/hearing range - public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams targetingParams) + public AITarget UpdateTargets(out CharacterParams.TargetParams targetingParams) { AITarget newTarget = null; targetValue = 0; @@ -2386,70 +2393,35 @@ namespace Barotrauma } Character targetCharacter = aiTarget.Entity as Character; //ignore the aitarget if it is the Character itself - if (targetCharacter == character) { continue; } + if (targetCharacter == Character) { continue; } float valueModifier = 1; - string targetingTag = null; + string targetingTag = GetTargetingTag(aiTarget); if (targetCharacter != null) { // ignore if target is tagged to be explicitly ignored (Feign Death) if (targetCharacter.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { continue; } - - if (targetCharacter.IsDead) + if (AIParams.Targets.None() && Character.IsFriendly(targetCharacter)) { - targetingTag = "dead"; + continue; } - else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner) + if (targetCharacter.AIController is EnemyAIController enemy) { - targetingTag = "owner"; - } - else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP)) - { - targetingTag = tP.Tag; - } - else - { - if (Character.IsFriendly(targetCharacter)) + if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) { - continue; - } - if (targetCharacter.AIController is EnemyAIController enemy) - { - if (targetCharacter.IsHusk && AIParams.HasTag("husk")) + if (SelectedAiTarget == aiTarget) { - targetingTag = "husk"; + // Freightened -> hold on to the target + valueModifier *= 2; } - else + if (IsBeingChasedBy(targetCharacter)) { - if (enemy.CombatStrength > CombatStrength) - { - targetingTag = "stronger"; - } - else if (enemy.CombatStrength < CombatStrength) - { - targetingTag = "weaker"; - } - else - { - targetingTag = "equal"; - } - if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) - { - if (SelectedAiTarget == aiTarget) - { - // Freightened -> hold on to the target - valueModifier *= 2; - } - if (IsBeingChasedBy(targetCharacter)) - { - valueModifier *= 2; - } - if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) - { - // Inside but in a different room - valueModifier /= 2; - } - } + valueModifier *= 2; + } + if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + // Inside but in a different room + valueModifier /= 2; } } } @@ -2469,7 +2441,7 @@ namespace Barotrauma if (aiTarget.Entity is Hull hull) { // Ignore the target if it's a room and the character is already inside a sub - if (character.CurrentHull != null) { continue; } + if (Character.CurrentHull != null) { continue; } // Ignore ruins if (hull.Submarine == null) { continue; } if (hull.Submarine.Info.IsRuin) { continue; } @@ -2479,7 +2451,7 @@ namespace Barotrauma if (aiTarget.Entity is Item item) { door = item.GetComponent(); - bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null; + bool targetingFromOutsideToInside = item.CurrentHull != null && Character.CurrentHull == null; if (targetingFromOutsideToInside) { if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls) @@ -2488,28 +2460,12 @@ namespace Barotrauma continue; } } - foreach (var prio in AIParams.Targets) + if (door == null && targetingFromOutsideToInside) { - if (item.HasTag(prio.Tag)) + if (item.Submarine?.Info is { IsRuin: true }) { - targetingTag = prio.Tag; - break; - } - } - if (door == null && targetingTag == null) - { - if (item.GetComponent() != null) - { - targetingTag = "sonar"; - } - else if (targetingFromOutsideToInside) - { - targetingTag = "room"; - if (item.Submarine?.Info.IsRuin != null) - { - // Ignore ruin items when the creature is outside. - continue; - } + // Ignore ruin items when the creature is outside. + continue; } } else if (targetingTag == "nasonov") @@ -2521,14 +2477,13 @@ namespace Barotrauma } } // Ignore the target if it's a decoy and the character is already inside a sub - if (character.CurrentHull != null && targetingTag == "decoy") + if (Character.CurrentHull != null && targetingTag == "decoy") { continue; } } else if (aiTarget.Entity is Structure s) { - targetingTag = "wall"; if (!s.HasBody) { // Ignore structures that doesn't have a body (not walls) @@ -2537,7 +2492,7 @@ namespace Barotrauma if (s.IsPlatform) { continue; } if (s.Submarine == null) { continue; } if (s.Submarine.Info.IsRuin) { continue; } - bool isCharacterInside = character.CurrentHull != null; + bool isCharacterInside = Character.CurrentHull != null; bool isInnerWall = s.prefab.Tags.Contains("inner"); if (isInnerWall && !isCharacterInside) { @@ -2624,21 +2579,12 @@ namespace Barotrauma } } } - else - { - targetingTag = "room"; - } if (door != null) { - // If there's not a more specific tag for the door - if (string.IsNullOrEmpty(targetingTag) || targetingTag == "room") - { - targetingTag = "door"; - } if (door.Item.Submarine == null) { continue; } bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; // Ignore inner doors when outside - if (character.CurrentHull == null && !isOutdoor) { continue; } + if (Character.CurrentHull == null && !isOutdoor) { continue; } bool isOpen = door.CanBeTraversed; if (!isOpen) { @@ -2651,7 +2597,7 @@ namespace Barotrauma } if (IsAggressiveBoarder) { - if (character.CurrentHull == null) + if (Character.CurrentHull == null) { // Increase the priority if the character is outside and the door is from outside to inside if (door.CanBeTraversed) @@ -2679,14 +2625,14 @@ namespace Barotrauma if (targetingTag == null) { continue; } var targetParams = GetTargetParams(targetingTag); if (targetParams == null) { continue; } - if (targetParams.IgnoreInside && character.CurrentHull != null) { continue; } - if (targetParams.IgnoreOutside && character.CurrentHull == null) { continue; } + if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; } + if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; } if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; } if (targetParams.IgnoreIfNotInSameSub) { if (aiTarget.Entity.Submarine != Character.Submarine) { continue; } var targetHull = targetCharacter != null ? targetCharacter.CurrentHull : aiTarget.Entity is Item it ? it.CurrentHull : null; - if ((targetHull == null) != (character.CurrentHull == null)) { continue; } + if ((targetHull == null) != (Character.CurrentHull == null)) { continue; } } if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) { @@ -2706,7 +2652,7 @@ namespace Barotrauma { target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin; } - if (character.HealthPercentage > target) + if (Character.HealthPercentage > target) { continue; } @@ -2721,7 +2667,7 @@ namespace Barotrauma // Halve the priority for each swarm mate targeting the same target -> reduces stacking foreach (Character otherCharacter in SwarmBehavior.Members) { - if (otherCharacter == character) { continue; } + if (otherCharacter == Character) { continue; } if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } valueModifier /= 2; } @@ -2731,15 +2677,15 @@ namespace Barotrauma // The same as above, but using all the friendly characters in the level. foreach (Character otherCharacter in Character.CharacterList) { - if (otherCharacter == character) { continue; } + if (otherCharacter == Character) { continue; } if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } - if (!character.IsFriendly(otherCharacter)) { continue; } + if (!Character.IsFriendly(otherCharacter)) { continue; } valueModifier /= 2; } } } if (!aiTarget.IsWithinSector(WorldPosition)) { continue; } - Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition; + Vector2 toTarget = aiTarget.WorldPosition - Character.WorldPosition; float dist = toTarget.Length(); float nonModifiedDist = dist; //if the target has been within range earlier, the character will notice it more easily @@ -2829,7 +2775,7 @@ namespace Barotrauma Character owner = GetOwner(i); // Don't target items that we own. // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) - if (owner == character) { continue; } + if (owner == Character) { continue; } if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget))) { continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 1387566ac..01f4f5d90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -85,6 +85,7 @@ namespace Barotrauma /// List of previous attacks done to this character /// private readonly Dictionary previousAttackResults = new Dictionary(); + private readonly Dictionary previousHealAmounts = new Dictionary(); private readonly SteeringManager outsideSteering, insideSteering; @@ -187,6 +188,15 @@ namespace Barotrauma foreach (var previousAttackResult in previousAttackResults) { RespondToAttack(previousAttackResult.Key, previousAttackResult.Value); + if (previousHealAmounts.ContainsKey(previousAttackResult.Key)) + { + //gradually forget past heals + previousHealAmounts[previousAttackResult.Key] = Math.Min(previousHealAmounts[previousAttackResult.Key] - 5.0f, 100.0f); + if (previousHealAmounts[previousAttackResult.Key] <= 0.0f) + { + previousHealAmounts.Remove(previousAttackResult.Key); + } + } } previousAttackResults.Clear(); respondToAttackTimer = RespondToAttackInterval; @@ -237,39 +247,41 @@ namespace Barotrauma if (Character.Submarine == null) { - // When the character is outside, far enough from the target, and the direct route is blocked, - // use the indoor steering with the main and side path waypoints to help avoid getting stuck in level walls - if (SelectedAiTarget?.Entity != null && !IsCloseEnoughToTarget(2000, useTargetSub: false)) + obstacleRaycastTimer -= deltaTime; + if (obstacleRaycastTimer <= 0) { - obstacleRaycastTimer -= deltaTime; - if (obstacleRaycastTimer <= 0) + obstacleRaycastTimer = obstacleRaycastIntervalLong; + if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false)) { - obstacleRaycastTimer = obstacleRaycastIntervalLong; - Vector2 rayEnd = SelectedAiTarget.Entity.SimPosition; - if (SelectedAiTarget.Entity.Submarine != null) + // If the target is behind a level wall, switch to the pathing to get around the obstacles. + ISpatialEntity spatialTarget = SelectedAiTarget?.Entity; + if (spatialTarget == null) { - rayEnd += SelectedAiTarget.Entity.Submarine.SimPosition; + var gotoObjective = ObjectiveManager.GetActiveObjective(); + spatialTarget = gotoObjective?.Target; } - IEnumerable ignoredBodies = null; - if (SelectedAiTarget.Entity is ISpatialEntity spatialTarget) + if (spatialTarget == null) { + UseIndoorSteeringOutside = false; + } + else + { + IEnumerable ignoredBodies = null; + Vector2 rayEnd = spatialTarget.SimPosition; Submarine targetSub = spatialTarget.Submarine; if (targetSub != null) { + rayEnd += targetSub.SimPosition; ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable(); } + var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); + UseIndoorSteeringOutside = obstacle != null; } - var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); - UseIndoorSteeringOutside = obstacle != null; } - } - else - { - UseIndoorSteeringOutside = false; - if (hasValidPath) + else { - obstacleRaycastTimer -= deltaTime; - if (obstacleRaycastTimer <= 0) + UseIndoorSteeringOutside = false; + if (hasValidPath) { obstacleRaycastTimer = obstacleRaycastIntervalShort; // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). @@ -332,25 +344,10 @@ namespace Barotrauma } } } + + IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave; - // Check whether the character is inside a cave - if (IsInsideCave) - { - // If the character was inside a cave, require them to move a bit further from the area to set the field back to false - // This is to avoid any twitchy behavior with the steering managers - IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => - { - var area = c.Area; - area.Inflate(new Vector2(100)); - return area.Contains(Character.WorldPosition); - }) is Level.Cave; - } - else - { - IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave; - } - - if (UseIndoorSteeringOutside || IsInsideCave || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer)) + if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer)) { if (steeringManager != insideSteering) { @@ -524,11 +521,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 +565,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; @@ -599,7 +598,7 @@ namespace Barotrauma takeMaskOff = false; break; } - else if (gotoObjective.mimic) + else if (gotoObjective.Mimic) { if (!removeSuit) { @@ -625,7 +624,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) { @@ -727,6 +726,7 @@ namespace Barotrauma if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID ) { + if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Bag })) { continue; } findItemState = FindItemState.OtherItem; if (FindSuitableContainer(item, out Item targetContainer)) { @@ -878,7 +878,7 @@ namespace Barotrauma if (target.CurrentHull != hull || !target.Enabled) { continue; } if (AIObjectiveFightIntruders.IsValidTarget(target, Character)) { - if (AddTargets(Character, target) && newOrder == null) + if (!target.IsArrested && AddTargets(Character, target) && newOrder == null) { var orderPrefab = Order.GetPrefab("reportintruders"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); @@ -950,7 +950,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"); @@ -1031,6 +1031,19 @@ namespace Barotrauma } } + public override void OnHealed(Character healer, float healAmount) + { + if (healer == null || healAmount <= 0.0f) { return; } + if (previousHealAmounts.ContainsKey(healer)) + { + previousHealAmounts[healer] += healAmount; + } + else + { + previousHealAmounts.Add(healer, healAmount); + } + } + public override void OnAttacked(Character attacker, AttackResult attackResult) { // The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs @@ -1074,11 +1087,16 @@ namespace Barotrauma } private void RespondToAttack(Character attacker, AttackResult attackResult) - { + { + float healAmount = 0.0f; + if (attacker != null) + { + previousHealAmounts.TryGetValue(attacker, out healAmount); + } // excluding poisons etc - float realDamage = attackResult.Damage; + float realDamage = attackResult.Damage - healAmount; // including poisons etc - float totalDamage = realDamage; + float totalDamage = realDamage - healAmount; if (attackResult.Afflictions != null) { foreach (Affliction affliction in attackResult.Afflictions) @@ -1117,6 +1135,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) @@ -1125,7 +1144,7 @@ namespace Barotrauma // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } - float cumulativeDamage = GetDamageDoneByAttacker(attacker); + float cumulativeDamage = Character.GetDamageDoneByAttacker(attacker); bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; if (isAccidental) { @@ -1136,7 +1155,6 @@ namespace Barotrauma } else { - (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); // Inform other NPCs if (cumulativeDamage > 1 || totalDamage >= 10) { @@ -1184,6 +1202,10 @@ namespace Barotrauma } } } + if (!isAttackerFightingEnemy) + { + (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); + } } } else @@ -1191,7 +1213,7 @@ namespace Barotrauma if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Non-friendly - InformOtherNPCs(GetDamageDoneByAttacker(attacker)); + InformOtherNPCs(Character.GetDamageDoneByAttacker(attacker)); } if (Character.IsBot) { @@ -1211,7 +1233,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 +1274,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 != null && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) { @@ -1295,6 +1327,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 +1464,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) @@ -1564,21 +1617,19 @@ namespace Barotrauma } } - public static void ItemTaken(Item item, Character character) + public static void ItemTaken(Item item, Character thief) { - if (item == null || character == null || item.GetComponent() != null) { return; } - Character thief = character; + if (item == null || thief == null || item.GetComponent() != null) { return; } + bool someoneSpoke = false; + bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false; - bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !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) { - if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsDead || - otherCharacter.Info?.Job == null || - !(otherCharacter.AIController is HumanAIController otherHumanAI) || + if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f || + otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) || !otherHumanAI.VisibleHulls.Contains(thief.CurrentHull)) { continue; @@ -1587,13 +1638,13 @@ namespace Barotrauma if (!otherCharacter.CanSeeCharacter(thief)) { continue; } // Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding // -> allow them to use the emergency items - if (character.Submarine != null) + if (thief.Submarine != null) { - var connectedHulls = character.Submarine.GetHulls(alsoFromConnectedSubs: true); + var connectedHulls = thief.Submarine.GetHulls(alsoFromConnectedSubs: true); if (item.HasTag("fireextinguisher") && connectedHulls.Any(h => h.FireSources.Any())) { continue; } if (item.HasTag("diving") && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } } - if (!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f) + if (!someoneSpoke) { if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { @@ -1624,9 +1675,9 @@ 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); + ItemTaken(foundItem, thief); } bool TriggerSecurity(HumanAIController humanAI) @@ -1698,7 +1749,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); } } @@ -1754,17 +1805,6 @@ namespace Barotrauma humanAI.ObjectiveManager.GetObjective()?.ReportedTargets.Remove(target)); } - public float GetDamageDoneByAttacker(Character otherCharacter) - { - float dmg = 0; - Character.Attacker attacker = Character.LastAttackers.LastOrDefault(a => a.Character == otherCharacter); - if (attacker != null) - { - dmg = attacker.Damage; - } - return dmg; - } - private void StoreHullSafety(Hull hull, HullSafety safety) { if (knownHulls.ContainsKey(hull)) @@ -1786,7 +1826,7 @@ namespace Barotrauma { if (isCurrentHull) { - CurrentHullSafety = 0; + CurrentHullSafety = character.NeedsAir ? 0 : 100; } return CurrentHullSafety; } @@ -1809,8 +1849,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)); @@ -1831,7 +1871,7 @@ namespace Barotrauma float enemyFactor = 1; if (!ignoreEnemies) { - bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e); + bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e) && !e.IsArrested; int enemyCount = visibleHulls == null ? Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) : Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index d2b1edb72..ca242f790 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -26,9 +26,9 @@ namespace Barotrauma private float findPathTimer; - private float buttonPressCooldown; - - const float ButtonPressInterval = 0.25f; + private const float buttonPressCooldown = 3; + private float checkDoorsTimer; + private float buttonPressTimer; public SteeringPath CurrentPath { @@ -97,9 +97,10 @@ namespace Barotrauma public override void Update(float speed) { base.Update(speed); - - buttonPressCooldown -= 1.0f / 60.0f; - findPathTimer -= 1.0f / 60.0f; + float step = 1.0f / 60.0f; + checkDoorsTimer -= step; + buttonPressTimer -= step; + findPathTimer -= step; } public void SetPath(SteeringPath path) @@ -120,10 +121,18 @@ namespace Barotrauma { steering += base.DoSteeringSeek(targetSimPos, weight); } - + public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) { - steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); + if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.state && !lastDoor.door.IsOpen) + { + // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. + Reset(); + } + else + { + steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); + } } /// @@ -204,28 +213,23 @@ 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?.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()) { useNewPath = false; } - else + else if (!character.IsClimbing) { // Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us). float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1); useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t); - if (!useNewPath && character.Submarine != null && !character.IsClimbing) + if (!useNewPath && character.Submarine != null) { // It's possible that the current path was calculated from a start point that is no longer valid. // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. // This is a special case for cases e.g. where the character falls and thus needs a new path. - // Don't do this outside or when climbing ladders, because both cause issues. useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); } } @@ -319,7 +323,8 @@ namespace Barotrauma return currentTarget - pos2; } bool doorsChecked = false; - if (!character.LockHands && buttonPressCooldown <= 0.0f) + checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime()); + if (!character.LockHands && checkDoorsTimer <= 0.0f) { CheckDoorsInPath(); doorsChecked = true; @@ -340,7 +345,7 @@ namespace Barotrauma { if (character.CanInteractWith(ladders.Item)) { - ladders.Item.TryInteract(character, false, true); + ladders.Item.TryInteract(character, forceSelectKey: true); } else { @@ -351,7 +356,7 @@ namespace Barotrauma if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item && character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5) { - previousLadders.Item.TryInteract(character, false, true); + previousLadders.Item.TryInteract(character, forceSelectKey: true); } } } @@ -391,7 +396,7 @@ namespace Barotrauma // Try to change the ladder (hatches between two submarines) if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition)) { - nextLadder.Item.TryInteract(character, false, true); + nextLadder.Item.TryInteract(character, forceSelectKey: true); } } if (isAboveFloor || nextLadderSameAsCurrent) @@ -491,7 +496,14 @@ namespace Barotrauma else { // We'll want this to run each time, because the delegate is used to find a valid button component. - bool canAccessButtons = door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); + bool canAccessButtons = false; + foreach (var button in door.Item.GetConnectedComponents(true)) + { + if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) + { + canAccessButtons = true; + } + } return canAccessButtons || door.IsOpen || ShouldBreakDoor(door); } } @@ -504,8 +516,22 @@ namespace Barotrauma return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y)); } + private (Door door, bool state) lastDoor; + private float GetDoorCheckTime() + { + if (steering.LengthSquared() > 0) + { + return character.AnimController.IsMovingFast ? 0.1f : 0.3f; + } + else + { + return float.PositiveInfinity; + } + } + private void CheckDoorsInPath() { + checkDoorsTimer = GetDoorCheckTime(); if (!canOpenDoors) { return; } for (int i = 0; i < 5; i++) { @@ -522,8 +548,7 @@ namespace Barotrauma } else { - bool closeDoors = character.IsBot && character.IsInFriendlySub || character.Params.AI != null && character.Params.AI.KeepDoorsClosed; - if (i == 0 || !closeDoors) + if (i == 0) { currentWaypoint = currentPath.CurrentNode; nextWaypoint = currentPath.NextNode; @@ -544,7 +569,7 @@ namespace Barotrauma if (currentWaypoint.ConnectedDoor.LinkedGap != null) { // Keep the airlock doors closed, but not in ruins/wrecks - if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom || currentWaypoint.Submarine?.Info.IsRuin != null || currentWaypoint.Submarine?.Info.IsWreck != null) + if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck) { shouldBeOpen = true; door = currentWaypoint.ConnectedDoor; @@ -570,28 +595,48 @@ namespace Barotrauma } if (door == null) { return; } + + if (door.BotsShouldKeepOpen) { shouldBeOpen = true; } - //toggle the door if it's the previous node and open, or if it's current node and closed if ((door.IsOpen || door.IsBroken) != shouldBeOpen) { + if (!shouldBeOpen) + { + if (character.AIController is HumanAIController humanAI) + { + bool keepDoorsClosed = character.IsBot && door.Item.Submarine?.TeamID == character.TeamID || character.Params.AI != null && character.Params.AI.KeepDoorsClosed; + if (!keepDoorsClosed) { return; } + bool isInAirlock = door.Item.CurrentHull is { IsWetRoom: true } || character.CurrentHull is { IsWetRoom: true }; + if (!isInAirlock) + { + // Don't slam the door at anyones face + if (Character.CharacterList.Any(c => c != character && humanAI.IsFriendly(c) && humanAI.VisibleHulls.Contains(c.CurrentHull) && !c.IsUnconscious)) + { + return; + } + } + } + } Controller closestButton = null; float closestDist = 0; bool canAccess = CanAccessDoor(door, button => { - if (currentWaypoint == null) { return true; } // Check that the button is on the right side of the door. - if (door.LinkedGap.IsHorizontal) + if (nextWaypoint != null) { - int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.X - door.Item.WorldPosition.X); - if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; } - } - else - { - int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y); - if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; } + if (door.LinkedGap.IsHorizontal) + { + int dir = Math.Sign((nextWaypoint).WorldPosition.X - door.Item.WorldPosition.X); + if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; } + } + else + { + int dir = Math.Sign((nextWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y); + if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; } + } } float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition); - if (closestButton == null || distance < closestDist) + if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item)) { closestButton = button; closestDist = distance; @@ -600,18 +645,39 @@ namespace Barotrauma }); if (canAccess) { + bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.state != shouldBeOpen; if (door.HasIntegratedButtons) { - door.Item.TryInteract(character, false, true); - buttonPressCooldown = ButtonPressInterval; + if (pressButton && character.CanSeeTarget(door.Item)) + { + if (door.Item.TryInteract(character, forceSelectKey: true)) + { + lastDoor = (door, shouldBeOpen); + buttonPressTimer = buttonPressCooldown; + } + else + { + buttonPressTimer = 0; + } + } break; } else if (closestButton != null) { - if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance + GetColliderLength(), 2)) + if (closestDist < MathUtils.Pow2(closestButton.Item.InteractDistance + GetColliderLength())) { - closestButton.Item.TryInteract(character, false, true); - buttonPressCooldown = ButtonPressInterval; + if (pressButton) + { + if (closestButton.Item.TryInteract(character, forceSelectKey: true)) + { + lastDoor = (door, shouldBeOpen); + buttonPressTimer = buttonPressCooldown; + } + else + { + buttonPressTimer = 0; + } + } break; } else @@ -631,6 +697,7 @@ namespace Barotrauma // The button is on the wrong side of the door or a wall currentPath.Unreachable = true; } + lastDoor = (null, false); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 69270a61e..a98cef44d 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) { @@ -207,7 +207,8 @@ namespace Barotrauma var cells = Level.Loaded.GetCells(character.WorldPosition, 1); if (cells.Count > 0) { - float closestDist = float.PositiveInfinity; + //ignore walls more than 200 meters away + float closestDist = 200.0f * 200.0f; foreach (Voronoi2.VoronoiCell cell in cells) { foreach (Voronoi2.GraphEdge edge in cell.Edges) @@ -267,7 +268,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 +287,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 +299,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 +386,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 +405,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..905cffa20 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) @@ -290,9 +283,9 @@ namespace Barotrauma /// public float CalculatePriority() { + ForceWalk = false; Priority = GetPriority(); ForceHighestPriority = false; - ForceWalk = false; return Priority; } @@ -508,5 +501,34 @@ namespace Barotrauma } } } + + protected static bool CanEquip(Character character, Item item) + { + bool canEquip = item != null; + if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any)) + { + canEquip = false; + var inv = character.Inventory; + foreach (var allowedSlot in item.AllowedSlots) + { + foreach (var slotType in inv.SlotTypes) + { + if (!allowedSlot.HasFlag(slotType)) { continue; } + for (int i = 0; i < inv.Capacity; i++) + { + canEquip = true; + if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null) + { + canEquip = false; + break; + } + } + } + } + } + return canEquip; + } + + protected bool CanEquip(Item item) => CanEquip(character, item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index f0cf653a8..f3197b975 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -68,15 +68,12 @@ namespace Barotrauma protected override void OnObjectiveCompleted(AIObjective objective, Item target) => HumanAIController.RemoveTargets(character, target); - private static bool IsItemInsideValidSubmarine(Item item, Character character) + public static bool IsItemInsideValidSubmarine(Item item, Character character) { if (item.CurrentHull == null) { return false; } if (item.Submarine == null) { return false; } if (item.Submarine.TeamID != character.TeamID) { return false; } - if (character.Submarine != null) - { - if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; } - } + if (character.Submarine != null && !character.Submarine.IsConnectedTo(item.Submarine)) { return false; } return true; } @@ -94,7 +91,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) @@ -129,29 +126,7 @@ namespace Barotrauma { return true; } - bool canEquip = true; - if (!item.AllowedSlots.Contains(InvSlotType.Any)) - { - canEquip = false; - var inv = character.Inventory; - foreach (var allowedSlot in item.AllowedSlots) - { - foreach (var slotType in inv.SlotTypes) - { - if (!allowedSlot.HasFlag(slotType)) { continue; } - for (int i = 0; i < inv.Capacity; i++) - { - canEquip = true; - if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null) - { - canEquip = false; - break; - } - } - } - } - } - return canEquip; + return CanEquip(character, item); } public override void OnDeselected() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 6bdddd863..de085d3c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -111,7 +111,7 @@ namespace Barotrauma public CombatMode Mode { get; private set; } private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; - private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f); + private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator; private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; private float AimSpeed => HumanAIController.AimSpeed; @@ -158,7 +158,7 @@ namespace Barotrauma return Priority; } } - float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, HumanAIController.GetDamageDoneByAttacker(Enemy) / 100.0f); + float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f); Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100); return Priority; } @@ -177,11 +177,12 @@ namespace Barotrauma ignoredWeapons.Clear(); ignoreWeaponTimer = ignoredWeaponsClearTime; } - if (findSafety != null) + bool isCurrentObjective = objectiveManager.IsCurrentObjective(); + if (findSafety != null && isCurrentObjective) { findSafety.Priority = 0; } - if (!AllowCoolDown && !character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective()) + if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective) { distanceTimer -= deltaTime; if (distanceTimer < 0) @@ -204,13 +205,18 @@ namespace Barotrauma protected override void Act(float deltaTime) { + if (IsEnemyDisabled) + { + IsCompleted = true; + return; + } if (AllowCoolDown) { coolDownTimer -= deltaTime; } if (seekAmmunitionObjective == null && seekWeaponObjective == null) { - if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled) + if (Mode != CombatMode.Retreat && TryArm()) { OperateWeapon(deltaTime); } @@ -283,10 +289,12 @@ namespace Barotrauma } else { + AskHelp(); Retreat(deltaTime); } break; case CombatMode.Retreat: + AskHelp(); Retreat(deltaTime); break; default: @@ -294,10 +302,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 +329,7 @@ namespace Barotrauma Weapon = null; continue; } - if (IsLoaded(WeaponComponent, checkContainedItems: true)) + if (WeaponComponent.IsLoaded(character)) { // All good, the weapon is loaded break; @@ -367,6 +371,7 @@ namespace Barotrauma { if (WeaponComponent == null) { + SpeakNoWeapons(); Mode = CombatMode.Retreat; } } @@ -380,6 +385,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; } @@ -412,6 +418,7 @@ namespace Barotrauma onCompleted: () => RemoveSubObjective(ref seekWeaponObjective), onAbandon: () => { + SpeakNoWeapons(); RemoveSubObjective(ref seekWeaponObjective); Mode = CombatMode.Retreat; }); @@ -433,7 +440,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 +482,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 +571,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 +610,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(); @@ -661,6 +668,13 @@ namespace Barotrauma { if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List() { InvSlotType.Any })) { + if (Weapon.AllowedSlots.Contains(InvSlotType.Bag)) + { + if (character.Inventory.TryPutItem(Weapon, character, new List() { InvSlotType.Bag })) + { + return; + } + } Weapon.Drop(character); } } @@ -673,22 +687,25 @@ namespace Barotrauma { return false; } - if (!character.HasEquippedItem(Weapon)) + if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType)) { Weapon.TryInteract(character, forceSelectKey: true); - var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand)); + var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s)); if (character.Inventory.TryPutItem(Weapon, character, slots)) { aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed; } else { + SpeakNoWeapons(); Weapon = null; Mode = CombatMode.Retreat; return false; } } return true; + + bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); } private float findHullTimer; @@ -788,7 +805,6 @@ namespace Barotrauma { UsePathingOutside = false, IgnoreIfTargetDead = true, - DialogueIdentifier = "dialogcannotreachtarget", TargetName = Enemy.DisplayName, AlwaysUseEuclideanDistance = false }, @@ -812,7 +828,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(); @@ -896,8 +912,9 @@ namespace Barotrauma TryAddSubObjective(ref seekAmmunitionObjective, constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent(), objectiveManager) { - targetItemCount = Weapon.GetComponent().Capacity, - checkInventory = false + ItemCount = Weapon.GetComponent().Capacity * Weapon.GetComponent().MaxStackSize, + checkInventory = false, + MoveWholeStack = true }, onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective), onAbandon: () => @@ -1103,7 +1120,7 @@ namespace Barotrauma if (WeaponComponent is RangedWeapon rangedWeapon) { // If the weapon is just equipped, we can't shoot just yet. - if (rangedWeapon.ReloadTimer <= 0) + if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger) { reloadTime = rangedWeapon.Reload; } @@ -1159,6 +1176,21 @@ namespace Barotrauma retreatTarget = null; } + private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30); + private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0, 1), minDuration: 20); + + private void Speak(string textIdentifier, float delay, float minDuration) + { + if (character.IsOnPlayerTeam && !character.IsInFriendlySub) + { + string msg = TextManager.Get(textIdentifier, true); + if (msg != null) + { + character.Speak(msg, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration); + } + } + } + //private float CalculateEnemyStrength() //{ // float enemyStrength = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 5fa7abede..f39b105f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -11,12 +11,11 @@ namespace Barotrauma public Func GetItemPriority; - public int targetItemCount = 1; public string[] ignoredContainerIdentifiers; public bool checkInventory = true; - //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs) - private bool spawnItemIfNotFound = false; + //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs and in some cases also enemy NPCs, like pirates) + private readonly bool spawnItemIfNotFound; //can either be a tag or an identifier public readonly string[] itemIdentifiers; @@ -35,9 +34,24 @@ namespace Barotrauma public bool Equip { get; set; } public bool RemoveEmpty { get; set; } = true; public bool RemoveExisting { get; set; } + /// + /// Only remove existing items when the contain target can't be put in the inventory + /// + public bool RemoveExistingWhenNecessary { get; set; } + public Func RemoveExistingPredicate { get; set; } + public int? RemoveMax { get; set; } public bool MoveWholeStack { get; set; } + private int _itemCount = 1; + public int ItemCount + { + get { return _itemCount; } + set + { + _itemCount = Math.Max(value, 1); + } + } public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -83,7 +97,7 @@ namespace Barotrauma containedItemCount++; } } - return containedItemCount >= targetItemCount; + return containedItemCount >= ItemCount; } } @@ -106,9 +120,9 @@ namespace Barotrauma } if (character.CanInteractWith(container.Item, checkLinked: false)) { - if (RemoveExisting) + if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(item))) { - HumanAIController.UnequipContainedItems(container.Item); + HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax); } else if (RemoveEmpty) { @@ -127,7 +141,6 @@ namespace Barotrauma container.Inventory.TryPutItem(item, null); } } - IsCompleted = true; } } @@ -144,7 +157,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) || @@ -174,7 +186,9 @@ namespace Barotrauma AllowToFindDivingGear = AllowToFindDivingGear, AllowDangerousPressure = AllowDangerousPressure, TargetCondition = ConditionLevel, - ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem) + ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem), + ItemCount = ItemCount, + TakeWholeStack = MoveWholeStack }, onAbandon: () => { Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 559aad746..1c8b16955 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -36,6 +36,11 @@ namespace Barotrauma /// public bool DropIfFails { get; set; } = true; + public bool RemoveExistingWhenNecessary { get; set; } + public Func RemoveExistingPredicate { get; set; } + public int? RemoveExistingMax { get; set; } + public string AbandonGetItemDialogueIdentifier { get; set; } + public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -86,6 +91,7 @@ namespace Barotrauma } if (itemToDecontain.Container != sourceContainer.Item) { + itemToDecontain.Drop(character); IsCompleted = true; return; } @@ -98,7 +104,12 @@ namespace Barotrauma if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character)) { TryAddSubObjective(ref getItemObjective, - constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack }, + constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) + { + CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier, + SpeakIfFails = AbandonGetItemDialogueIdentifier != null, + TakeWholeStack = this.TakeWholeStack + }, onAbandon: () => Abandon = true); return; } @@ -110,6 +121,9 @@ namespace Barotrauma MoveWholeStack = TakeWholeStack, Equip = Equip, RemoveEmpty = false, + RemoveExistingWhenNecessary = RemoveExistingWhenNecessary, + RemoveExistingPredicate = RemoveExistingPredicate, + RemoveMax = RemoveExistingMax, GetItemPriority = GetItemPriority, ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 6e13c261d..4de4c0a43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -31,7 +31,7 @@ namespace Barotrauma protected override AIObjective ObjectiveConstructor(Character target) { - AIObjectiveCombat.CombatMode combatMode = target.IsEscorted && character.TeamID == CharacterTeamType.Team1 ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive; + AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive; var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { @@ -41,7 +41,7 @@ namespace Barotrauma combatObjective.holdFireCondition = () => { //hold fire while the enemy is in the airlock (except if they've attacked us) - if (HumanAIController.GetDamageDoneByAttacker(target) > 0.0f) { return false; } + if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; } return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase)); }; character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f); @@ -65,7 +65,13 @@ namespace Barotrauma if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } + if (target.IsArrested) { return false; } return true; } + + public static bool ShouldArrest(Character target, Character character) + { + return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1; + } } } 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..bb988f3c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -46,10 +46,19 @@ 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.HasOrder(o => o.Priority > 0) || + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasActiveObjective() || + objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + && HumanAIController.HasDivingSuit(character) ? 0 : 100; + } } else { @@ -253,7 +262,7 @@ namespace Barotrauma } foreach (Character enemy in Character.CharacterList) { - if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy)) { continue; } + if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; } if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) { Vector2 dir = character.Position - enemy.Position; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index afc834674..6613d85c0 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; + public 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,21 @@ 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 string CannotFindDialogueIdentifierOverride { get; set; } + + private int _itemCount = 1; + public int ItemCount + { + get { return _itemCount; } + set + { + _itemCount = Math.Max(value, 1); + } + } public InvSlotType? EquipSlotType { get; set; } @@ -67,23 +85,46 @@ 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; + 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() { - if (identifiersOrTags == null) { return false; } + if (IdentifiersOrTags == null) { return false; } var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true); if (item != null) { @@ -93,6 +134,19 @@ namespace Barotrauma return item != null; } + private bool CountItems() + { + int itemCount = 0; + foreach (Item it in character.Inventory.AllItems) + { + if (CheckItem(it)) + { + itemCount++; + } + } + return itemCount >= ItemCount; + } + protected override void Act(float deltaTime) { if (character.LockHands) @@ -105,7 +159,7 @@ namespace Barotrauma Abandon = true; return; } - if (identifiersOrTags != null && !isDoneSeeking) + if (IdentifiersOrTags != null && !isDoneSeeking) { if (checkInventory) { @@ -122,7 +176,7 @@ namespace Barotrauma if (dangerousPressure) { #if DEBUG - string itemName = targetItem != null ? targetItem.Name : identifiersOrTags.FirstOrDefault(); + string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault(); DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow); #endif Abandon = true; @@ -215,10 +269,28 @@ namespace Barotrauma } } } - IsCompleted = true; + if (IdentifiersOrTags == null) + { + IsCompleted = true; + } + else + { + IsCompleted = CountItems(); + if (!IsCompleted) + { + ResetInternal(); + } + } } 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 +315,10 @@ namespace Barotrauma { // Try again ignoredItems.Add(targetItem); + if (targetItem != moveToTarget && moveToTarget is Item item) + { + ignoredItems.Add(item); + } ResetInternal(); } else @@ -256,7 +332,7 @@ namespace Barotrauma private void FindTargetItem() { - if (identifiersOrTags == null) + if (IdentifiersOrTags == null) { if (targetItem == null) { @@ -269,7 +345,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 +364,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 +405,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; } @@ -341,10 +465,10 @@ namespace Barotrauma { if (spawnItemIfNotFound) { - if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab)) + if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && IdentifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab)) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); #endif Abandon = true; } @@ -355,7 +479,7 @@ namespace Barotrauma targetItem = spawnedItem; if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) { - spawnedItem.SpawnedInOutpost = true; + spawnedItem.SpawnedInCurrentOutpost = true; } }); } @@ -363,9 +487,8 @@ namespace Barotrauma else { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow); + 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,7 +498,16 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } - if (targetItem != null) + if (targetItem == null) + { + // Not yet ready + return false; + } + if (IdentifiersOrTags != null && ItemCount > 1) + { + return CountItems(); + } + else { if (Equip && EquipSlotType.HasValue) { @@ -386,23 +518,6 @@ namespace Barotrauma 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); - } - } - return false; - } - return false; } private bool CheckItem(Item item) @@ -410,9 +525,11 @@ 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; } - return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id)); + 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)); } public override void Reset() @@ -437,37 +554,28 @@ 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); + string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true); if (msg != null) { character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); } } } - - // 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..9cb439547 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -0,0 +1,101 @@ +#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 override bool AllowMultipleInstances => 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; } + public bool RequireAllItems { 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) + { + if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; } + int count = gearTags.Count(t => t == tag); + AIObjectiveGetItem? getItem = null; + TryAddSubObjective(ref getItem, () => + new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) + { + AllowVariants = AllowVariants, + Wear = Wear, + TakeWholeStack = TakeWholeStack, + AllowStealing = AllowStealing, + ignoredIdentifiersOrTags = ignoredTags, + CheckPathForEachItem = CheckPathForEachItem, + RequireLoaded = RequireLoaded, + ItemCount = count, + SpeakIfFails = RequireAllItems + }, + 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); + if (RequireAllItems) + { + Abandon = true; + } + }); + } + 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..72dcbaf98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -22,15 +22,23 @@ namespace Barotrauma public Func requiredCondition; public Func endNodeFilter; - public Func priorityGetter; + public Func PriorityGetter; + + public bool IsFollowOrderObjective; + public bool Mimic; - 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; - public float extraDistanceOutsideSub; + public float ExtraDistanceWhileSwimming; + public float ExtraDistanceOutsideSub; + private float _closeEnoughMultiplier = 1; + public float CloseEnoughMultiplier + { + get { return _closeEnoughMultiplier; } + set { _closeEnoughMultiplier = Math.Max(value, 1); } + } private float _closeEnough = 50; private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; @@ -44,14 +52,15 @@ namespace Barotrauma { get { - float dist = _closeEnough; + float dist = _closeEnough * CloseEnoughMultiplier; + float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3); if (character.AnimController.InWater) { - dist += extraDistanceWhileSwimming; + dist += ExtraDistanceWhileSwimming * extraMultiplier; } if (character.CurrentHull == null) { - dist += extraDistanceOutsideSub; + dist += ExtraDistanceOutsideSub * extraMultiplier; } return dist; } @@ -61,6 +70,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 +89,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; } @@ -105,9 +117,9 @@ namespace Barotrauma } else { - if (priorityGetter != null) + if (PriorityGetter != null) { - Priority = priorityGetter(); + Priority = PriorityGetter(); } else if (OverridePriority.HasValue) { @@ -149,7 +161,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) { @@ -176,11 +191,6 @@ namespace Barotrauma character.AIController.SteeringManager.Reset(); return; } - if (cannotFollow) - { - // Wait - character.AIController.SteeringManager.Reset(); - } if (!character.IsClimbing) { character.SelectedConstruction = null; @@ -198,7 +208,7 @@ namespace Barotrauma } } Hull targetHull = GetTargetHull(); - if (!isFollowOrderObjective) + if (!IsFollowOrderObjective) { // Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls. bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls @@ -211,26 +221,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 +255,16 @@ namespace Barotrauma if (getDivingGearIfNeeded && !character.LockHands) { Character followTarget = Target as Character; - bool needsDivingSuit = !isInside || targetIsOutside; - bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); - if (mimic) + 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; } @@ -268,7 +285,7 @@ namespace Barotrauma if (findDivingGear != null && !findDivingGear.CanBeCompleted) { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => Abort(), + onAbandon: () => Abandon = true, onCompleted: () => { cannotFollow = false; @@ -278,7 +295,7 @@ namespace Barotrauma else { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => Abort(), + onAbandon: () => Abandon = true, onCompleted: () => { cannotFollow = false; @@ -311,7 +328,7 @@ namespace Barotrauma if (character.AnimController.InWater) { if (character.CurrentHull == null || - isFollowOrderObjective && + IsFollowOrderObjective && targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) && Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance) { @@ -354,7 +371,7 @@ namespace Barotrauma } if (TargetGap != null) { - if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, isFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime)) + if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime)) { SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); return; @@ -373,7 +390,7 @@ namespace Barotrauma Item scooter = null; float closeEnough = 250; float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition); - bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic || + bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!Mimic || (targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) { @@ -382,24 +399,32 @@ namespace Barotrauma } else if (shouldUseScooter) { - bool hasBattery = false; - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false)) + var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); + var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); + bool handsFull = + (leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) || + (rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1); + if (!handsFull) { - // Non-equipped scooter with a battery - scooter = nonEquippedScooters.FirstOrDefault(); - hasBattery = true; - } - else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable _nonEquippedScooters, requireEquipped: false)) - { - // Non-equipped scooter without a battery - scooter = _nonEquippedScooters.FirstOrDefault(); - // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. - hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); - } - if (scooter != null && hasBattery) - { - // Equip only if we have a battery available - HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false); + bool hasBattery = false; + if (HumanAIController.HasItem(character, scooterTag, out IEnumerable nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false)) + { + // Non-equipped scooter with a battery + scooter = nonEquippedScooters.FirstOrDefault(); + hasBattery = true; + } + else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable _nonEquippedScooters, requireEquipped: false)) + { + // Non-equipped scooter without a battery + scooter = _nonEquippedScooters.FirstOrDefault(); + // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. + hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); + } + if (scooter != null && hasBattery) + { + // Equip only if we have a battery available + HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false); + } } } bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter); @@ -588,7 +613,7 @@ namespace Barotrauma { if (gap.Open < 1) { continue; } if (gap.Submarine == null) { continue; } - if (!isFollowOrderObjective) + if (!IsFollowOrderObjective) { if (gap.FlowTargetHull == null) { continue; } if (gap.Submarine != Target.Submarine) { continue; } @@ -669,18 +694,6 @@ namespace Barotrauma return IsCompleted; } - private void Abort() - { - if (!objectiveManager.IsOrder(this)) - { - Abandon = true; - } - else - { - cannotFollow = true; - } - } - protected override void OnAbandon() { StopMovement(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs new file mode 100644 index 000000000..8a6b8cea7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -0,0 +1,235 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + class AIObjectiveLoadItem : AIObjective + { + public override string Identifier { get; set; } = "load item"; + public override bool IsLoop + { + get => true; + set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); + } + + private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; } + private Item Container { get; } + private ItemContainer ItemContainer { get; } + private ImmutableArray TargetContainerTags { get; } + + private int itemIndex = 0; + private AIObjectiveDecontainItem decontainObjective; + private readonly HashSet ignoredItems = new HashSet(); + private Item targetItem; + private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable"; + + public AIObjectiveLoadItem(Item container, ImmutableArray targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, string option, Character character, AIObjectiveManager objectiveManager, float priorityModifier) + : base(character, objectiveManager, priorityModifier) + { + Container = container; + ItemContainer = container?.GetComponent(); + if (ItemContainer?.Inventory == null) + { + Abandon = true; + return; + } + TargetContainerTags = targetTags; + TargetItemCondition = targetCondition; + if (!string.IsNullOrEmpty(option)) + { + string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}"; + if (TextManager.ContainsTag(optionSpecificDialogueIdentifier)) + { + abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier; + } + } + } + + protected override float GetPriority() + { + if (!IsAllowed) + { + Priority = 0; + Abandon = true; + return Priority; + } + else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition)) + { + // Reduce priority to 0 if the this isn't a valid container right now + Priority = 0; + } + else if (targetItem == null) + { + Priority = 0; + } + else + { + float dist = 0.0f; + if (character.CurrentHull != targetItem.CurrentHull) + { + AddDistance(character.WorldPosition, targetItem.WorldPosition); + } + if (targetItem.CurrentHull != Container.CurrentHull) + { + AddDistance(targetItem.WorldPosition, Container.WorldPosition); + } + void AddDistance(Vector2 startPos, Vector2 targetPos) + { + float yDist = Math.Abs(startPos.Y - targetPos.Y); + // If we're on the same level with the target, we'll disregard the vertical distance + if (yDist > 100) { dist += yDist * 5; } + dist += Math.Abs(character.WorldPosition.X - targetPos.X); + } + float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f; + bool hasContainable = character.HasItem(targetItem); + float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100; + float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2); + Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1)); + if (decontainObjective != null && targetItem.Container != Container) + { + if (!IsValidContainable(targetItem)) + { + // Target is not valid anymore, abandon the objective + decontainObjective.Abandon = true; + } + else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition))) + { + // The container is full and there's no item that should be removed, abandon the objective + decontainObjective.Abandon = true; + } + } + if (ItemContainer.Inventory.IsFull()) + { + // Prioritize containers that still have empty space by lowering the priority of objectives with a full target container + Priority /= 4; + } + } + return Priority; + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + if (targetItem == null) + { + if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority)) + { + if (item == null) + { + // No possible containables found, abandon the objective + Abandon = true; + } + targetItem = item; + } + // Prefer items closer to full condition when target condition is Empty, and vice versa + float GetConditionBasedPriority(Item item) + { + try + { + return TargetItemCondition switch + { + AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage), + AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException) + { +#if DEBUG + DebugConsole.ShowError($"Unexpected target condition \"{TargetItemCondition}\" in local function GetConditionBasedProperty"); +#endif + return 0.0f; + } + } + } + } + + protected override void Act(float deltaTime) + { + if (targetItem != null) + { + if(decontainObjective == null && !IsValidContainable(targetItem)) + { + IgnoreTargetItem(); + Reset(); + return; + } + TryAddSubObjective(ref decontainObjective, + constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier) + { + AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier, + Equip = true, + RemoveExistingWhenNecessary = true, + RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition), + RemoveExistingMax = 1 + }, + onCompleted: () => + { + IsCompleted = true; + RemoveSubObjective(ref decontainObjective); + }, + onAbandon: () => + { + // Try again + IgnoreTargetItem(); + Reset(); + }); + } + else + { + objectiveManager.GetObjective().Wander(deltaTime); + } + } + + private bool IsValidContainable(Item item) + { + if (item == null) { return false; } + if (item.Removed) { return false; } + if (ignoredItems.Contains(item)) { return false; } + if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } + var rootInventoryOwner = item.GetRootInventoryOwner(); + if (rootInventoryOwner is Character owner && owner != character) { return false; } + if (rootInventoryOwner is Item parentItem) + { + if (parentItem.HasTag("donttakeitems")) { return false; } + if (!(parentItem.GetComponent()?.HasAccess(character) ?? true)) { return false; } + } + if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } + if (!character.HasItem(item) && !CanEquip(item)) { return false; } + if (!ItemContainer.HasAccess(character)) { return false; } + if (!ItemContainer.CanBeContained(item)) { return false; } + if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; } + if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full) + { + // Ignore items that have had their condition increase recently + if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full && item.ConditionIncreasedRecently) { return false; } + // Ignore items inside their (condition-restricted) primary containers + if (item.ParentInventory is ItemInventory itemInventory && item.IsContainerPreferred(itemInventory.Container, out bool _, out bool isSecondary, requireConditionRestriction: true) && !isSecondary) { return false; } + } + // Ignore items inside another valid container + if (AIObjectiveLoadItems.IsValidTarget(item.Container, character, TargetContainerTags)) { return false; } + return true; + } + + protected override bool CheckObjectiveSpecific() => IsCompleted; + + public override void Reset() + { + base.Reset(); + // Don't reset the target item when resetting the objective because it affects priority calculations + decontainObjective = null; + itemIndex = 0; + } + + private void IgnoreTargetItem() + { + if(targetItem == null) { return; } + ignoredItems.Add(targetItem); + targetItem = null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs new file mode 100644 index 000000000..5d6eb0753 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -0,0 +1,115 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveLoadItems : AIObjectiveLoop + { + public override string Identifier { get; set; } = "load items"; + protected override float IgnoreListClearInterval => 20.0f; + protected override bool ResetWhenClearingIgnoreList => false; + + private ImmutableArray TargetContainerTags { get; } + private List TargetContainers { get; } = new List(); + private ItemCondition TargetCondition { get; } + + public enum ItemCondition + { + Empty, + Full + } + + public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, string option, ImmutableArray containerTags, Item targetContainer = null, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier, option) + { + if ((containerTags == null || containerTags.None()) && targetContainer == null) + { + Abandon = true; + return; + } + else + { + TargetContainerTags = containerTags.ToImmutableArray(); + } + if (targetContainer != null) + { + TargetContainers.Add(targetContainer); + } + TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full; + } + + protected override bool Filter(Item target) + { + if (!IsValidTarget(target, character, TargetContainerTags, TargetCondition)) { return false; } + if (target.CurrentHull == null || target.CurrentHull.FireSources.Count > 0) { return false; } + if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + return true; + } + + public static bool IsValidTarget(Item item, Character character, ImmutableArray? targetContainerTags = null, ItemCondition? targetCondition = null) + { + if (item == null) { return false; } + if (item.Removed) { return false; } + if (targetContainerTags.HasValue && !Order.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; } + if (!(item.GetComponent() is ItemContainer container)) { return false; } + if (container.Inventory == null) { return false; } + if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } + if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } + if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; } + if (!item.IsInteractable(character)) { return false; } + if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } + if (!container.HasAccess(character)) { return false; } + // Ignore items that require power but don't have it + if (item.GetComponent() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; } + return true; + } + + public static bool ItemMatchesTargetCondition(Item item, ItemCondition targetCondition) + { + if(item == null) { return false; } + try + { + return targetCondition switch + { + ItemCondition.Empty => item.Condition <= 0.1f, + ItemCondition.Full => item.IsFullCondition, + _ => throw new NotImplementedException(), + }; + } + catch (NotImplementedException) + { +#if DEBUG + DebugConsole.ShowError($"Unexpected target condition \"{targetCondition}\" in AIObjectiveLoadItems.ItemMatchesTargetCondition"); +#endif + return false; + } + } + + protected override IEnumerable GetList() => TargetContainers.Any() ? TargetContainers : Item.ItemList; + + protected override AIObjective ObjectiveConstructor(Item target) + => new AIObjectiveLoadItem(target, TargetContainerTags, TargetCondition, Option, character, objectiveManager, PriorityModifier); + + protected override void OnObjectiveCompleted(AIObjective objective, Item target) + => HumanAIController.RemoveTargets(character, target); + + protected override float TargetEvaluation() + { + if (Targets.None()) { return 0; } + if (objectiveManager.IsOrder(this)) + { + float prio = objectiveManager.GetOrderPriority(this); + if (subObjectives.All(so => so.SubObjectives.None() || so.Priority <= 0)) + { + ForceWalk = true; + } + return prio; + } + return AIObjectiveManager.RunPriority - 0.5f; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 338227596..21720599b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -45,6 +45,7 @@ namespace Barotrauma public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowSubObjectiveSorting => true; public virtual bool InverseTargetEvaluation => false; + protected virtual bool ResetWhenClearingIgnoreList => true; public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); } @@ -55,7 +56,15 @@ namespace Barotrauma { if (ignoreListTimer > IgnoreListClearInterval) { - Reset(); + if (ResetWhenClearingIgnoreList) + { + Reset(); + } + else + { + ignoreList.Clear(); + ignoreListTimer = 0; + } } else { @@ -113,7 +122,7 @@ namespace Barotrauma Priority = 0; return Priority; } - if (character.LockHands || character.Submarine == null) + if (character.LockHands) { Priority = 0; } @@ -131,7 +140,7 @@ namespace Barotrauma // If the priority is higher than the target value, let's just use it. // The priority calculation is more precise, but it takes into account things like distances, // so it's better not to use it if it's lower than the rougher targetValue. - targetValue = Priority; + targetValue = currentSubObjective.Priority; } // If the target value is less than 1% of the max value, let's just treat it as zero. if (targetValue < 1) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 72f1b30c1..71871dfff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -137,7 +137,7 @@ namespace Barotrauma Item item = null; if (orderPrefab.MustSetTarget) { - item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandom(); + item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character, orderOption: autonomousObjective.option)?.GetRandom(); } var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } @@ -288,6 +288,7 @@ namespace Barotrauma float highestPriority = 0; for (int i = CurrentOrders.Count - 1; i >= 0; i--) { + if (CurrentOrders.Count <= i) { break; } var orderObjective = CurrentOrders[i].Objective; if (orderObjective == null) { continue; } orderObjective.CalculatePriority(); @@ -332,7 +333,6 @@ namespace Barotrauma SortObjectives(); } - private CoroutineHandle speakRoutine; public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak) { if (character.IsDead) @@ -364,6 +364,7 @@ namespace Barotrauma // Make sure the order priorities reflect those set by the player for (int i = CurrentOrders.Count - 1; i >= 0; i--) { + if (CurrentOrders.Count <= i) { break; } var currentOrder = CurrentOrders[i]; if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option)) { @@ -388,6 +389,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()) @@ -395,53 +397,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); } } } @@ -456,13 +417,14 @@ namespace Barotrauma if (orderGiver == null) { return null; } newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { - CloseEnough = Rand.Range(90, 100) + Rand.Range(50, 70) * Math.Min(HumanAIController.CountCrew(c => c.ObjectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.Target == orderGiver, onlyBots: true), 4), - extraDistanceOutsideSub = 100, - extraDistanceWhileSwimming = 100, + CloseEnough = Rand.Range(80, 100), + CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), + ExtraDistanceOutsideSub = 100, + ExtraDistanceWhileSwimming = 100, AllowGoingOutside = true, IgnoreIfTargetDead = true, - isFollowOrderObjective = true, - mimic = true, + IsFollowOrderObjective = true, + Mimic = character.IsOnPlayerTeam, DialogueIdentifier = "dialogcannotreachplace" }; break; @@ -474,7 +436,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": @@ -500,12 +461,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 { @@ -575,6 +534,39 @@ namespace Barotrauma case "escapehandcuffs": newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier); break; + case "prepareforexpedition": + newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems) + { + KeepActiveWhenReady = true, + CheckInventory = true, + Equip = false, + FindAllItems = true + }; + break; + case "findweapon": + AIObjectivePrepare prepareObjective; + if (order.TargetEntity is Item tItem) + { + prepareObjective = new AIObjectivePrepare(character, this, targetItem: tItem); + } + else + { + prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems) + { + KeepActiveWhenReady = false, + CheckInventory = false, + EvaluateCombatPriority = true, + FindAllItems = false + }; + } + prepareObjective.KeepActiveWhenReady = false; + prepareObjective.Equip = true; + newObjective = prepareObjective; + newObjective.Completed += () => DismissSelf(order, option); + break; + case "loaditems": + newObjective = new AIObjectiveLoadItems(character, this, option, order.GetTargetItems(option), order.TargetEntity as Item, priorityModifier); + break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } @@ -606,17 +598,20 @@ namespace Barotrauma #endif return; } + Order dismissOrder = Order.GetPrefab("dismissed"); + var orderOption = Order.GetDismissOrderOption(currentOrder); + int priority = currentOrder.ManualPriority; #if CLIENT if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) { - GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character); + GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder, orderOption, priority, character); } #else - GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, currentOrder.Order?.TargetSpatialEntity, character, character)); + GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, orderOption, priority, currentOrder.Order.TargetSpatialEntity, character, character)); + SetOrder(dismissOrder, orderOption, priority, character, speak: false); #endif } - private bool IsAllowedToWait() { if (!character.IsOnPlayerTeam) { return false; } @@ -665,10 +660,9 @@ namespace Barotrauma return ForcedOrder != null || CurrentOrders.Any(); } - public bool HasOrder() where T : AIObjective - { - return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T); - } + public bool HasOrder(Func predicate = null) where T : AIObjective => + ForcedOrder is T forcedOrder && (predicate == null || predicate(forcedOrder)) || + CurrentOrders.Any(o => o.Objective is T order && (predicate == null || predicate(order))); public float GetOrderPriority(AIObjective objective) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 5db86b049..5c7055788 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -202,7 +202,7 @@ namespace Barotrauma HumanAIController.FaceTarget(target.Item); if (character.SelectedConstruction != target.Item) { - target.Item.TryInteract(character, false, true); + target.Item.TryInteract(character, forceSelectKey: true); } if (component.AIOperate(deltaTime, character, this)) { @@ -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..c4b2e1d1c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -0,0 +1,200 @@ +using Barotrauma.Extensions; +using System; +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; + public override bool PrioritizeIfSubObjectivesActive => true; + + private AIObjectiveGetItem getSingleItemObjective; + private AIObjectiveGetItems getAllItemsObjective; + private AIObjectiveGetItems getMultipleItemsObjective; + private bool subObjectivesCreated; + private readonly Item targetItem; + private readonly ImmutableArray requiredItems; + private readonly ImmutableArray optionalItems; + 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() + { + if (getSingleItemObjective != null) { return getSingleItemObjective; } + if (getAllItemsObjective == null || getAllItemsObjective.IsCompleted) + { + return getMultipleItemsObjective; + } + return getAllItemsObjective; + } + public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, Item targetItem, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + this.targetItem = targetItem; + } + + public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable optionalItems, IEnumerable requiredItems = null, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + this.optionalItems = optionalItems.ToImmutableArray(); + if (requiredItems != null) + { + this.requiredItems = requiredItems.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 && targetItem == null) + { + getMultipleItemsObjective = CreateObjectives(optionalItems, requireAll: false); + if (requiredItems != null && requiredItems.Any()) + { + getAllItemsObjective = CreateObjectives(requiredItems, requireAll: true); + } + AIObjectiveGetItems CreateObjectives(IEnumerable itemTags, bool requireAll) + { + AIObjectiveGetItems objectiveReference = null; + if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags) + { + CheckInventory = CheckInventory, + Equip = Equip, + EvaluateCombatPriority = EvaluateCombatPriority, + RequireLoaded = true, + RequireAllItems = requireAll + }, + onCompleted: () => + { + if (KeepActiveWhenReady) + { + if (objectiveReference != null) + { + foreach (var item in objectiveReference.achievedItems) + { + if (item?.IsOwnedBy(character) != null) + { + items.Add(item); + } + } + } + } + else + { + IsCompleted = true; + } + }, + onAbandon: () => Abandon = true)) + { + Abandon = true; + } + return objectiveReference; + } + } + else + { + Func getItemConstructor; + if (targetItem != null) + { + getItemConstructor = () => new AIObjectiveGetItem(character, targetItem, objectiveManager, equip: Equip) + { + SpeakIfFails = true + }; + } + else + { + IEnumerable allItems = optionalItems; + if (requiredItems != null && requiredItems.Any()) + { + allItems = requiredItems; + } + getItemConstructor = () => new AIObjectiveGetItem(character, allItems, objectiveManager, equip: Equip, checkInventory: CheckInventory) + { + EvaluateCombatPriority = EvaluateCombatPriority, + SpeakIfFails = true, + RequireLoaded = true + }; + } + if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor, + 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; + getMultipleItemsObjective = null; + getSingleItemObjective = null; + getAllItemsObjective = null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index a6132f8cb..b2115b8ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -20,6 +20,9 @@ namespace Barotrauma private float previousCondition = -1; private RepairTool repairTool; + private const float WaitTimeBeforeRepair = 0.5f; + private float waitTimer; + private bool IsRepairing() => IsRepairing(character, Item); private readonly bool isPriority; @@ -107,7 +110,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 }; @@ -160,6 +163,9 @@ namespace Barotrauma } if (!character.IsClimbing && character.CanInteractWith(Item, out _, checkLinked: false)) { + waitTimer += deltaTime; + if (waitTimer < WaitTimeBeforeRepair) { return; } + HumanAIController.FaceTarget(Item); if (repairTool != null) { @@ -176,8 +182,12 @@ namespace Barotrauma { if (character.SelectedConstruction != Item) { - if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) && - !Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true)) + if (Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) || + Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true)) + { + character.SelectedConstruction = Item; + } + else { Abandon = true; } @@ -209,6 +219,7 @@ namespace Barotrauma } else { + waitTimer = 0.0f; RemoveSubObjective(ref refuelObjective); // If cannot reach the item, approach it. TryAddSubObjective(ref goToObjective, @@ -219,8 +230,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..1c7786d6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -36,9 +36,9 @@ namespace Barotrauma { if (targetCharacter == null) { - string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = $"Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace(); + DebugConsole.ThrowError(character.Name + ": " + errorMsg); + GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); Abandon = true; return; } @@ -59,7 +59,7 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) + if (character.LockHands || targetCharacter == null || targetCharacter.Removed || targetCharacter.IsDead) { Abandon = true; return; @@ -137,66 +137,69 @@ namespace Barotrauma recursive: true); } } - if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + if (character.Submarine != null) { - // Incapacitated target is not in a safe place -> Move to a safe place first - if (character.SelectedCharacter != targetCharacter) + if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) { - if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) + // Incapacitated target is not in a safe place -> Move to a safe place first + if (character.SelectedCharacter != targetCharacter) { - character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); - } - // Go to the target and select it - if (!character.CanInteractWith(targetCharacter)) - { - RemoveSubObjective(ref replaceOxygenObjective); - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) { - CloseEnough = CloseEnoughToTreat, - DialogueIdentifier = "dialogcannotreachpatient", - TargetName = targetCharacter.DisplayName - }, - onCompleted: () => RemoveSubObjective(ref goToObjective), - onAbandon: () => + character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, + new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), + null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); + } + // Go to the target and select it + if (!character.CanInteractWith(targetCharacter)) { + RemoveSubObjective(ref replaceOxygenObjective); RemoveSubObjective(ref goToObjective); - Abandon = true; - }); - } - else - { - character.SelectCharacter(targetCharacter); - } - } - else - { - // Drag the character into safety - if (safeHull == null) - { - if (findHullTimer > 0) - { - findHullTimer -= deltaTime; - } - else - { - safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); - findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); - } - } - if (safeHull != null && character.CurrentHull != safeHull) - { - RemoveSubObjective(ref replaceOxygenObjective); - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager), + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) + { + CloseEnough = CloseEnoughToTreat, + DialogueIdentifier = "dialogcannotreachpatient", + TargetName = targetCharacter.DisplayName + }, onCompleted: () => RemoveSubObjective(ref goToObjective), onAbandon: () => { RemoveSubObjective(ref goToObjective); - safeHull = character.CurrentHull; + Abandon = true; }); + } + else + { + character.SelectCharacter(targetCharacter); + } + } + else + { + // Drag the character into safety + if (safeHull == null) + { + if (findHullTimer > 0) + { + findHullTimer -= deltaTime; + } + else + { + safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); + } + } + if (safeHull != null && character.CurrentHull != safeHull) + { + RemoveSubObjective(ref replaceOxygenObjective); + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager), + onCompleted: () => RemoveSubObjective(ref goToObjective), + onAbandon: () => + { + RemoveSubObjective(ref goToObjective); + safeHull = character.CurrentHull; + }); + } } } } @@ -228,7 +231,7 @@ namespace Barotrauma // We can start applying treatment if (character != targetCharacter && character.SelectedCharacter != targetCharacter) { - if (targetCharacter.CurrentHull.DisplayName != null) + if (targetCharacter.CurrentHull?.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" }, new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), @@ -252,7 +255,7 @@ namespace Barotrauma return; } - SteeringManager?.Reset(); + SteeringManager.Reset(); if (!targetCharacter.IsPlayer) { @@ -269,26 +272,36 @@ namespace Barotrauma float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f; //find which treatments are the most suitable to treat the character's current condition - targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false); + targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false, predictFutureDuration: 10.0f); //check if we already have a suitable treatment for any of the afflictions foreach (Affliction affliction in GetSortedAfflictions(targetCharacter)) { if (affliction == null) { throw new Exception("Affliction was null"); } if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } + float bestSuitability = 0.0f; + Item bestItem = null; foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { - if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f) + if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && + currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) { Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); - if (matchingItem == null) { continue; } - if (targetCharacter != character) { character.SelectCharacter(targetCharacter); } - ApplyTreatment(affliction, matchingItem); - //wait a bit longer after applying a treatment to wait for potential side-effects to manifest - treatmentTimer = TreatmentDelay * 4; - return; + if (matchingItem != null) + { + bestItem = matchingItem; + bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key]; + } } } + if (bestItem != null) + { + if (targetCharacter != character) { character.SelectCharacter(targetCharacter); } + ApplyTreatment(affliction, bestItem); + //wait a bit longer after applying a treatment to wait for potential side-effects to manifest + treatmentTimer = TreatmentDelay * 4; + return; + } } // Find treatments outside of own inventory only if inside the own sub. if (character.Submarine != null && character.Submarine.TeamID == character.TeamID) @@ -319,9 +332,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) { @@ -395,21 +431,7 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { - if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) - { - Abandon = true; - return false; - } - // Don't go into rooms that have enemies - if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) - { - Abandon = true; - return false; - } - bool isCompleted = - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) || - targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Prefab.IsBuff || a.Strength <= a.Prefab.TreatmentThreshold); - + bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), @@ -426,16 +448,36 @@ namespace Barotrauma Abandon = true; return Priority; } - if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) + if (character.CurrentHull == null) + { + if (!objectiveManager.HasOrder()) + { + Priority = 0; + Abandon = true; + return Priority; + } + } + else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) + { + // Don't go into rooms that have enemies + Priority = 0; + Abandon = true; + return Priority; + } + if (targetCharacter == null) { Priority = 0; Abandon = true; } else { - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) - float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f; - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); + float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X); + float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y); + if (character.Submarine?.Info is { IsRuin: false }) + { + verticalDistance *= 2; + } + float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance)); if (targetCharacter.CurrentHull == character.CurrentHull) { distanceFactor = 1; @@ -449,6 +491,16 @@ namespace Barotrauma public static IEnumerable GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions()); + public static IEnumerable GetTreatableAfflictions(Character character) + { + foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + { + if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } + if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; } + yield return affliction; + } + } + public override void Reset() { base.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index eb7504c08..ed1684d2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -64,8 +64,15 @@ namespace Barotrauma public static float GetVitalityFactor(Character character) { - float vitality = character.HealthPercentage - (character.Bleeding * 2) - character.Bloodloss + Math.Min(character.Oxygen, 0); + float vitality = 100; + vitality -= character.Bleeding * 2; + vitality += Math.Min(character.Oxygen, 0); vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis"); + foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character)) + { + float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); + vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; + } return Math.Clamp(vitality, 0, 100); } @@ -79,11 +86,11 @@ 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) { - if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) || - target.CharacterHealth.GetAllAfflictions().All(a => a.Prefab.IsBuff || a.Strength <= a.Prefab.TreatmentThreshold)) + if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; } @@ -105,10 +112,13 @@ namespace Barotrauma { if (GetVitalityFactor(target) >= vitalityThreshold) { return false; } } - if (target.Submarine == null || character.Submarine == null) { return false; } - // Don't allow going into another sub, unless it's connected and of the same team and type. - if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; } - if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) + if (target.Submarine != character.Submarine) { return false; } + if (character.Submarine != null) + { + // Don't allow going into another sub, unless it's connected and of the same team and type. + if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; } + } + if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { // Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters if (targetAI.ObjectiveManager.HasActiveObjective() || @@ -119,9 +129,12 @@ namespace Barotrauma return false; } } - // Don't go into rooms that have enemies - if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; } - return true; + if (target.CurrentHull != null) + { + // Don't go into rooms that have enemies + if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; } + } + return character.GetDamageDoneByAttacker(target) <= 0; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 57b024e16..a460c39c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; using System.Linq; +using System.Collections.Immutable; namespace Barotrauma { @@ -19,11 +20,11 @@ namespace Barotrauma struct OrderInfo { - public Order Order { get; } - public string OrderOption { get; } - public int ManualPriority { get; } - public OrderType Type { get; } - public AIObjective Objective { get; } + public readonly Order Order; + public readonly string OrderOption; + public readonly int ManualPriority; + public readonly OrderType Type; + public readonly AIObjective Objective; public bool IsCurrentOrder => Type == OrderType.Current; public enum OrderType @@ -41,6 +42,8 @@ namespace Barotrauma Objective = objective; } + public OrderInfo(Order order, string orderOption) : this(order, orderOption, CharacterInfo.HighestManualOrderPriority, null) { } + public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { } public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { } @@ -103,7 +106,10 @@ namespace Barotrauma public readonly Type ItemComponentType; public readonly bool CanTypeBeSubclass; - public readonly string[] TargetItems; + public readonly ImmutableArray TargetItems; + public readonly ImmutableArray RequireItems; + private readonly Dictionary> OptionTargetItems; + public bool HasOptionSpecificTargetItems => OptionTargetItems != null && OptionTargetItems.Any(); public readonly string Identifier; @@ -170,6 +176,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; @@ -208,6 +215,12 @@ namespace Barotrauma /// public bool DrawIconWhenContained { get; } + /// + /// Affects how high on the order list the order will be placed (i.e. the manual priority order when it's given) when it's first given. + /// Manually rearranging orders will override this priority. + /// + public int AssignmentPriority { get; } + public static void Init() { Prefabs = new Dictionary(); @@ -305,8 +318,6 @@ namespace Barotrauma } } CanTypeBeSubclass = orderElement.GetAttributeBool("cantypebesubclass", false); - - TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true); color = orderElement.GetAttributeColor("color"); FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f); UseController = orderElement.GetAttributeBool("usecontroller", false); @@ -316,6 +327,36 @@ namespace Barotrauma Options = orderElement.GetAttributeStringArray("options", new string[0]); HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]); AllOptions = Options.Concat(HiddenOptions).ToArray(); + + OptionTargetItems = new Dictionary>(); + if (orderElement.GetAttributeString("targetitems", "") is string targetItems && targetItems.Contains(';')) + { + string[] splitTargetItems = targetItems.Split(';'); +#if DEBUG + if (splitTargetItems.Length != AllOptions.Length) + { + DebugConsole.ThrowError($"Order \"{Identifier}\" has option-specific target items, but the option count doesn't match the target item count"); + } +#endif + var allTargetItems = new List(); + for (int i = 0; i < AllOptions.Length; i++) + { + string[] optionTargetItems = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', ',') : new string[0]; + for (int j = 0; j < optionTargetItems.Length; j++) + { + optionTargetItems[j] = optionTargetItems[j].ToLowerInvariant().Trim(); + allTargetItems.Add(optionTargetItems[j]); + } + OptionTargetItems.Add(AllOptions[i], optionTargetItems.ToImmutableArray()); + } + TargetItems = allTargetItems.ToImmutableArray(); + } + else + { + TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray(); + } + RequireItems = orderElement.GetAttributeStringArray("requireitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray(); + var category = orderElement.GetAttributeString("category", null); if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); @@ -363,6 +404,8 @@ namespace Barotrauma MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); + AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement); + AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100); } /// @@ -378,6 +421,8 @@ namespace Barotrauma ItemComponentType = prefab.ItemComponentType; CanTypeBeSubclass = prefab.CanTypeBeSubclass; TargetItems = prefab.TargetItems; + OptionTargetItems = prefab.OptionTargetItems; + RequireItems = prefab.RequireItems; Options = prefab.Options; SymbolSprite = prefab.SymbolSprite; Color = prefab.Color; @@ -395,6 +440,7 @@ namespace Barotrauma DrawIconWhenContained = prefab.DrawIconWhenContained; Hidden = prefab.Hidden; IgnoreAtOutpost = prefab.IgnoreAtOutpost; + AssignmentPriority = prefab.AssignmentPriority; OrderGiver = orderGiver; TargetEntity = targetEntity; @@ -510,16 +556,17 @@ namespace Barotrauma } /// Only returns items which are interactable for this character - public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null) + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null, string orderOption = null) { List matchingItems = new List(); if (submarine == null) { return matchingItems; } - if (ItemComponentType != null || TargetItems.Length > 0) + if (ItemComponentType != null || TargetItems.Any() || RequireItems.Any()) { foreach (var item in Item.ItemList) { - if (TargetItems.Length > 0 && !TargetItems.Contains(item.Prefab.Identifier) && !item.HasTag(TargetItems)) { continue; } - if (TargetItems.Length == 0 && !TryGetTargetItemComponent(item, out _)) { continue; } + if (RequireItems.Any() && !TargetItemsMatchItem(RequireItems, item)) { continue; } + if (TargetItems.Any() && !TargetItemsMatchItem(item, orderOption)) { continue; } + if (RequireItems.None() && TargetItems.None() && !TryGetTargetItemComponent(item, out _)) { continue; } if (mustBelongToPlayerSub && item.Submarine?.Info != null && item.Submarine.Info.Type != SubmarineType.Player) { continue; } if (item.Submarine != submarine && !submarine.DockedTo.Contains(item.Submarine)) { continue; } if (requiredTeam.HasValue && (item.Submarine == null || item.Submarine.TeamID != requiredTeam.Value)) { continue; } @@ -534,14 +581,13 @@ namespace Barotrauma return matchingItems; } - /// Only returns items which are interactable for this character - public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null) + public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null, string orderOption = null) { Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ? Submarine.MainSubs[1] : Submarine.MainSub; - return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor); + return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor, orderOption: orderOption); } public string GetOptionName(string id) @@ -580,5 +626,34 @@ namespace Barotrauma } return ""; } + + public override string ToString() + { + return $"Order ({Name})"; + } + + public ImmutableArray GetTargetItems(string option = null) + { + if (string.IsNullOrEmpty(option) || !OptionTargetItems.TryGetValue(option, out ImmutableArray optionTargetItems)) + { + return TargetItems; + } + else + { + return optionTargetItems; + } + } + + public bool TargetItemsMatchItem(Item item, string option = null) + { + if (item == null) { return false; } + ImmutableArray targetItems = GetTargetItems(option); + return TargetItemsMatchItem(targetItems, item); + } + + public static bool TargetItemsMatchItem(ImmutableArray targetItems, Item item) + { + return item != null && targetItems != null && targetItems.Length > 0 && (targetItems.Contains(item.Prefab.Identifier) || item.HasTag(targetItems)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index e46839e24..540e4135d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -84,18 +84,17 @@ namespace Barotrauma public bool IsBlocked() { if (blocked.HasValue) { return blocked.Value; } - blocked = false; - if (Waypoint.Submarine != null) { return blocked.Value; } if (Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { return blocked.Value; } foreach (var w in Level.Loaded.ExtraWalls) { - if (!(w is DestructibleLevelWall d)) { return blocked.Value; } - if (d.Destroyed) { return blocked.Value; } - if (!d.IsPointInside(Waypoint.Position)) { return blocked.Value; } - blocked = true; - break; + if (!w.IsPointInside(Waypoint.Position)) { continue; } + if (w is DestructibleLevelWall d) + { + blocked = !d.Destroyed; + } + if (blocked.Value) { break; } } return blocked.Value; } @@ -125,6 +124,7 @@ namespace Barotrauma { wp.OnLinksChanged += WaypointLinksChanged; } + sortedNodes = new List(nodes.Count); this.isCharacter = isCharacter; } @@ -166,7 +166,7 @@ namespace Barotrauma } } - private readonly List sortedNodes = new List(); + private readonly List sortedNodes; public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { @@ -175,8 +175,7 @@ namespace Barotrauma node.ResetBlocked(); } - //sort nodes roughly according to distance - sortedNodes.Clear(); + // First calculate the temp positions for all nodes. foreach (PathNode node in nodes) { node.TempPosition = node.Position; @@ -194,8 +193,15 @@ namespace Barotrauma else if (hostSub == null && wpSub != null) { // Outside and targeting inside - node.TempPosition += wpSub.SimPosition; + node.TempPosition += wpSub.SimPosition; } + } + + //sort nodes roughly according to distance + sortedNodes.Clear(); + PathNode startNode = null; + foreach (PathNode node in nodes) + { float xDiff = Math.Abs(start.X - node.TempPosition.X); float yDiff = Math.Abs(start.Y - node.TempPosition.Y); if (InsideSubmarine && !(node.Waypoint.Submarine?.Info?.IsRuin ?? false)) @@ -215,13 +221,20 @@ namespace Barotrauma //much higher cost to waypoints that are outside if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; } - //optimization: - //node extremely far, don't try to use it as a start node - if (node.TempDistance > 800.0f) + //optimization: node extremely far, don't try to use it as a start node + if (node.TempDistance > (InsideSubmarine ? 100.0f : 800.0f)) { continue; } - + //optimization: node extremely close (< 1 m). If it's valid, choose it as the start node and skip the more exhaustive search for the closest one + if (node.TempDistance < 1.0f) + { + if (IsValidStartNode(node)) + { + startNode = node; + break; + } + } //prefer nodes that are closer to the end position node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f; @@ -233,6 +246,88 @@ namespace Barotrauma sortedNodes.Insert(i, node); } + //find the most suitable start node, starting from the ones that are the closest + if (startNode == null) + { + foreach (PathNode node in sortedNodes) + { + if (IsValidStartNode(node)) + { + startNode = node; + break; + } + } + } + + if (startNode == null) + { +#if DEBUG + DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed); +#endif + return new SteeringPath(true); + } + + //sort nodes again, now based on distance from the end position + sortedNodes.Clear(); + PathNode endNode = null; + foreach (PathNode node in nodes) + { + node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition); + if (InsideSubmarine) + { + if (ApplyPenaltyToOutsideNodes) + { + //much higher cost to waypoints that are outside + if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; } + } + //avoid stopping at a doorway + if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; } + //avoid stopping at a ladder + if (node.Waypoint.Ladders != null) { node.TempDistance *= 10.0f; } + } + //optimization: node extremely far (> 100m / 800 m) from the end position, don't try to use it as an end node + if (node.TempDistance > (InsideSubmarine ? 100.0f * 100.0f : 800.0f * 800.0f)) + { + continue; + } + //optimization: node extremely close (< 1 m). If it's valid, choose it as the end node and skip the more exhaustive search for the closest one + if (node.TempDistance < 1.0f) + { + if (IsValidEndNode(node)) + { + endNode = node; + break; + } + } + int i = 0; + while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance) + { + i++; + } + sortedNodes.Insert(i, node); + } + if (endNode == null) + { + //find the most suitable end node, starting from the ones closest to the end position + foreach (PathNode node in sortedNodes) + { + if (IsValidEndNode(node)) + { + endNode = node; + break; + } + } + } + if (endNode == null) + { +#if DEBUG + DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. " + errorMsgStr, Color.DarkRed); +#endif + return new SteeringPath(true); + } + var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize); + return path; + bool IsWaypointVisible(PathNode node, Vector2 rayStart, bool checkVisibility = true) { //if searching for a path inside the sub, make sure the waypoint is visible @@ -251,85 +346,33 @@ namespace Barotrauma return true; } - //find the most suitable start node, starting from the ones that are the closest - PathNode startNode = null; - foreach (PathNode node in sortedNodes) + bool IsValidStartNode(PathNode node) { - if (nodeFilter != null && !nodeFilter(node)) { continue; } - if (startNodeFilter != null && !startNodeFilter(node)) { continue; } + if (nodeFilter != null && !nodeFilter(node)) { return false; } + if (startNodeFilter != null && !startNodeFilter(node)) { return false; } // Always check the visibility for the start node - if (!IsWaypointVisible(node, start)) { continue; } - if (node.IsBlocked()) { continue; } + if (!IsWaypointVisible(node, start)) { return false; } + if (node.IsBlocked()) { return false; } if (node.Waypoint.ConnectedGap != null) { - if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; } } - startNode = node; - break; + return true; } - if (startNode == null) + bool IsValidEndNode(PathNode node) { -#if DEBUG - DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed); -#endif - return new SteeringPath(true); - } - - //sort nodes again, now based on distance from the end position - sortedNodes.Clear(); - foreach (PathNode node in nodes) - { - node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition); - if (InsideSubmarine) - { - if (ApplyPenaltyToOutsideNodes) - { - //much higher cost to waypoints that are outside - if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; } - } - //avoid stopping at a doorway - if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; } - //avoid stopping at a ladder - if (node.Waypoint.Ladders != null) { node.TempDistance *= 10.0f; } - } - - int i = 0; - while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance) - { - i++; - } - sortedNodes.Insert(i, node); - } - - //find the most suitable end node, starting from the ones closest to the end position - PathNode endNode = null; - foreach (PathNode node in sortedNodes) - { - if (nodeFilter != null && !nodeFilter(node)) { continue; } - if (endNodeFilter != null && !endNodeFilter(node)) { continue; } + if (nodeFilter != null && !nodeFilter(node)) { return false; } + if (endNodeFilter != null && !endNodeFilter(node)) { return false; } // Only check the visibility for the end node when allowed (fix leaks) - if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; } - if (node.IsBlocked()) { continue; } + if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { return false; } + if (node.IsBlocked()) { return false; } if (node.Waypoint.ConnectedGap != null) { - if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; } } - endNode = node; - break; + return true; } - - if (endNode == null) - { -#if DEBUG - DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. " + errorMsgStr, Color.DarkRed); -#endif - return new SteeringPath(true); - } - - var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize); - - return path; } private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "", float minGapSize = 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 8e4a6d4e4..cb99e69b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -1,4 +1,5 @@ using Barotrauma.Items.Components; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma @@ -47,14 +48,16 @@ namespace Barotrauma public void SetOrder(Character orderedCharacter) { OrderedCharacter = orderedCharacter; - if (orderedCharacter != CommandingCharacter) + if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrderPrefab, Option))) { - CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false)); + if (orderedCharacter != CommandingCharacter) + { + CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); + } + CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter); + OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter); + OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5); } - - // not sure if new orders are supposed to be created each time. TODO m61: check later - CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter); - OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter); TimeSinceLastAttempt = 0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index 1d4e55f2e..fb2aa21b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -31,10 +31,10 @@ namespace Barotrauma } // there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing - if (targetingImportances.Any()) + if (targetingImportances.Any(i => i > 0)) { targetingImportances.Sort(); - Importance = targetingImportances.TakeLast(3).Average(); + Importance = targetingImportances.TakeLast(3).Sum(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 23856db00..9ffb51694 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -199,7 +199,7 @@ namespace Barotrauma foreach (Character potentialCharacter in Character.CharacterList) { - if (!HumanAIController.IsActive(character)) { continue; } + if (!HumanAIController.IsActive(potentialCharacter)) { continue; } if (HumanAIController.IsFriendly(character, potentialCharacter, true) && potentialCharacter.AIController is HumanAIController) { @@ -313,7 +313,7 @@ namespace Barotrauma ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter); #endif Order orderPrefab = Order.GetPrefab("dismissed"); - character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false)); + //character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false)); shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character); shipIssueWorker.RemoveOrder(); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index bbaba7ba8..f9a0b6c2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -462,7 +462,7 @@ namespace Barotrauma DebugConsole.Log(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index c37d0d8b1..e829475c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -951,7 +951,7 @@ namespace Barotrauma { string errorMsg = "Creature death animation error: invalid limb mass on character \"" + character.SpeciesName + "\" (type: " + limb.type + ", mass: " + limb.Mass + ")"; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidMass" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidMass" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); deathAnimTimer = deathAnimDuration; return; } @@ -961,7 +961,7 @@ namespace Barotrauma { string errorMsg = "Creature death animation error: invalid diff (center of mass: " + centerOfMass + ", limb position: " + limb.SimPosition + ")"; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidDiff" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("FishAnimController.UpdateDying:InvalidDiff" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); deathAnimTimer = deathAnimDuration; return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 37a5a7029..109cec337 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -811,35 +811,41 @@ namespace Barotrauma if (currentHull != null) { float surfacePos = currentHull.Surface; + float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); //if the hull is almost full of water, check if there's a water-filled hull above it //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) { - foreach (Gap gap in currentHull.ConnectedGaps) + GetSurfacePos(CurrentHull, ref surfacePos); + void GetSurfacePos(Hull hull, ref float prevSurfacePos) { - if (gap.IsHorizontal || gap.Open <= 0.0f) { continue; } - if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; } - - //if the gap is above us and leads outside, there's no surface to limit the movement - if (!gap.IsRoomToRoom && gap.Position.Y > currentHull.Position.Y) + if (prevSurfacePos > surfaceThreshold) { return; } + foreach (Gap gap in hull.ConnectedGaps) { - surfacePos += 100000.0f; - continue; - } + if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; } + if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; } - foreach (var linkedTo in gap.linkedTo) - { - if (linkedTo is Hull hull && hull != currentHull) + //if the gap is above us and leads outside, there's no surface to limit the movement + if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) { - surfacePos = Math.Max(surfacePos, hull.Surface); - break; + prevSurfacePos += 100000.0f; + return; + } + + foreach (var linkedTo in gap.linkedTo) + { + if (linkedTo is Hull otherHull && otherHull != hull) + { + prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); + GetSurfacePos(otherHull, ref prevSurfacePos); + break; + } } } } } - surfaceLimiter = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f) - surfacePos; - surfaceLimiter = Math.Max(1.0f, surfaceLimiter); + surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos); if (surfaceLimiter > 50.0f) { return; } } @@ -1048,7 +1054,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; @@ -1057,10 +1064,12 @@ namespace Barotrauma onGround = false; IgnorePlatforms = true; - Vector2 tempTargetMovement = TargetMovement; - tempTargetMovement.Y = Math.Min(tempTargetMovement.Y, 1.0f); - + bool climbFast = targetMovement.Y > 3.0f; bool slide = targetMovement.Y < -1.1f; + Vector2 tempTargetMovement = TargetMovement; + tempTargetMovement.Y = climbFast ? + Math.Min(tempTargetMovement.Y, 2.0f) : + Math.Min(tempTargetMovement.Y, 1.0f); movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f); @@ -1075,79 +1084,98 @@ 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 (climbFast) { stepHeight *= 2; } - 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; } float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f; - float headPos = HeadPosition ?? 0; float torsoPos = TorsoPosition ?? 0; - MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f); MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f); + float headPos = HeadPosition ?? 0; + MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f); Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f); Vector2 handPos = new Vector2( ladderSimPos.X, bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y); + if (climbFast) { handPos.Y -= stepHeight; } + bool aiming = this.aiming || aimingMelee; //prevent the hands from going above the top of the ladders handPos.Y = Math.Min(-0.5f, handPos.Y); - - if (!character.IsKeyDown(InputType.Aim) || Math.Abs(movement.Y) > 0.01f) + if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) + { + MoveLimb(rightHand, + new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X, + (slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y), + 5.2f); + rightHand.body.ApplyTorque(Dir * 2.0f); + } + if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) { MoveLimb(leftHand, new Vector2(handPos.X - ladderSimSize.X * 0.5f, (slide ? handPos.Y : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y), 5.2f); ; - - MoveLimb(rightHand, - new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X, - (slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y), - 5.2f); - leftHand.body.ApplyTorque(Dir * 2.0f); - rightHand.body.ApplyTorque(Dir * 2.0f); } Vector2 footPos = new Vector2( handPos.X - Dir * 0.05f, bottomPos + ColliderHeightFromFloor - stepHeight * 2.7f - ladderSimPos.Y); + if (climbFast) { footPos.Y += stepHeight; } + + //apply torque to the legs to make the knees bend + Limb leftLeg = GetLimb(LimbType.LeftLeg); + Limb rightLeg = GetLimb(LimbType.RightLeg); //only move the feet if they're above the bottom of the ladders //(if not, they'll just dangle in air, and the character holds itself up with it's arms) - if (footPos.Y > -ladderSimSize.Y && leftFoot != null && rightFoot != null) + if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null) { + Limb refLimb = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); + bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi; + bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi; + if (slide) { - MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true); - MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); + if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true); } + if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); } } else { float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight; float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight; - MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); + if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); } float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f); float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f); - MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); + if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); } #if CLIENT if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) { @@ -1163,36 +1191,48 @@ namespace Barotrauma prevFootPos = footPos.Y; } - //apply torque to the legs to make the knees bend - Limb leftLeg = GetLimb(LimbType.LeftLeg); - Limb rightLeg = GetLimb(LimbType.RightLeg); - - leftLeg.body.ApplyTorque(Dir * -8.0f); - rightLeg.body.ApplyTorque(Dir * -8.0f); + if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); } + if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); } } 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; + + //reached the top of the ladders -> can't go further up + Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor; + + if (!InWater) { climbForce.Y += 0.3f * movementFactor; } - Vector2 climbForce = new Vector2(0.0f, movement.Y + 0.3f) * movementFactor; 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 (!aiming) + { + 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 +1261,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 +1629,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; @@ -1656,7 +1710,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError(errorMsg); #endif - GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidPos", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidPos", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -1690,7 +1744,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError(errorMsg); #endif - GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidAngle", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("FootIK:InvalidAngle", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 5184eaad5..b36776f97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -65,8 +65,8 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.Limbs:AccessRemoved", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.ErrorSeverity.Error, + "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } return new Limb[0]; @@ -122,6 +122,7 @@ namespace Barotrauma protected Vector2 overrideTargetMovement; protected float floorY, standOnFloorY; + protected Fixture floorFixture; protected Vector2 floorNormal = Vector2.UnitY; protected float surfaceY; @@ -489,6 +490,7 @@ namespace Barotrauma limbDictionary = new Dictionary(); limbs = new Limb[RagdollParams.Limbs.Count]; RagdollParams.Limbs.ForEach(l => AddLimb(l)); + if (limbs.Contains(null)) { return; } SetupDrawOrder(); } @@ -549,19 +551,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(); } @@ -887,7 +893,7 @@ namespace Barotrauma string errorMsg = "Ragdoll.GetCenterOfMass returned an invalid value (" + centerOfMass + "). Limb positions: {" + string.Join(", ", limbs.Select(l => l.SimPosition)) + "}, total mass: " + totalMass + "."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return Collider.SimPosition; } @@ -925,14 +931,14 @@ namespace Barotrauma { GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.FindHull:InvalidPosition", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace.CleanupStackTrace()); return; } Hull newHull = Hull.FindHull(findPos, currentHull); - - if (newHull == currentHull) return; + + if (newHull == currentHull) { return; } if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine)) { @@ -965,16 +971,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) @@ -982,13 +988,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() @@ -1021,7 +1027,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) { @@ -1044,7 +1050,7 @@ namespace Barotrauma character.DisableImpactDamageTimer = 0.25f; - SetPosition(Collider.SimPosition + moveAmount); + SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles); character.CursorPosition += moveAmount; Collider?.UpdateDrawPosition(); @@ -1356,19 +1362,19 @@ namespace Barotrauma string errorMsg = null; if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f) { - errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: " + character.Name + ")."; + errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: [name])."; } else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f) { - errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: " + character.Name + ")."; + errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: [name])."; } else if (!MathUtils.IsValid(body.Rotation)) { - errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: " + character.Name + ")."; + errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: [name])."; } else if (!MathUtils.IsValid(body.AngularVelocity) || Math.Abs(body.AngularVelocity) > 1000f) { - errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: " + character.Name + ")."; + errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: [name])."; } if (errorMsg != null) { @@ -1386,11 +1392,11 @@ namespace Barotrauma } #if DEBUG - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg.Replace("[name]", Character.Name)); #else - DebugConsole.NewMessage(errorMsg, Color.Red); + DebugConsole.NewMessage(errorMsg.Replace("[name]", Character.Name), Color.Red); #endif - GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName)); if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f) { @@ -1508,6 +1514,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); } @@ -1580,6 +1587,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) { @@ -1620,26 +1628,41 @@ 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)) { DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.SetPosition:InvalidPosition", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.ErrorSeverity.Error, + "Attempted to move a ragdoll (" + character.SpeciesName + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace()); return; } 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) { @@ -1720,7 +1743,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) { @@ -1740,7 +1763,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/CauseOfDeath.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CauseOfDeath.cs index 987bf64af..e1cd01576 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CauseOfDeath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CauseOfDeath.cs @@ -20,7 +20,7 @@ namespace Barotrauma { string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); type = CauseOfDeathType.Unknown; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 96c4838ec..79c0fd003 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; @@ -611,8 +612,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - if (IsDead) { return true; } - return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); } } @@ -621,6 +621,11 @@ namespace Barotrauma get { return CharacterHealth.IsUnconscious; } } + public bool IsArrested + { + get { return IsHuman && HasEquippedItem("handlocker"); } + } + public bool IsPet { get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; } @@ -642,6 +647,11 @@ namespace Barotrauma set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); } } + public float HullOxygenPercentage + { + get { return CurrentHull?.OxygenPercentage ?? 0.0f; } + } + public bool UseHullOxygen { get; set; } = true; public float Stun @@ -690,12 +700,12 @@ namespace Barotrauma { get { - if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) return 100.0f; + if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) { return 100.0f; } return speechImpediment; } set { - if (value < speechImpediment) return; + if (value < speechImpediment) { return; } speechImpedimentSet = true; speechImpediment = MathHelper.Clamp(value, 0.0f, 100.0f); } @@ -807,7 +817,7 @@ namespace Barotrauma { if (!canBeDragged) { return false; } if (Removed || !AnimController.Draggable) { return false; } - return IsKnockedDown || LockHands || IsPet; + return IsKnockedDown || LockHands || IsPet || CanInventoryBeAccessed; } set { canBeDragged = value; } } @@ -825,7 +835,7 @@ namespace Barotrauma } else { - return IsKnockedDown || LockHands; + return IsKnockedDown || LockHands || IsBot && TeamID != CharacterTeamType.FriendlyNPC; } } set { canInventoryBeAccessed = value; } @@ -854,7 +864,7 @@ namespace Barotrauma { if (!accessRemovedCharacterErrorShown) { - string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + "."; + string errorMsg = "Attempted to access a potentially removed character. Character: [name], id: " + ID + ", removed: " + Removed + "."; if (AnimController == null) { errorMsg += " AnimController == null"; @@ -864,11 +874,11 @@ namespace Barotrauma errorMsg += " AnimController.Collider == null"; } errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.NewMessage(errorMsg, Color.Red); + DebugConsole.NewMessage(errorMsg.Replace("[name]", Name), Color.Red); GameAnalyticsManager.AddErrorEventOnce( "Character.SimPosition:AccessRemoved", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.ErrorSeverity.Error, + errorMsg.Replace("[name]", SpeciesName) + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } return Vector2.Zero; @@ -1362,7 +1372,11 @@ namespace Barotrauma public override string ToString() { +#if DEBUG return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName; +#else + return SpeciesName; +#endif } public void GiveJobItems(WayPoint spawnPoint = null) @@ -1988,7 +2002,7 @@ namespace Barotrauma public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null) { - seeingEntity ??= AnimController.SimplePhysicsEnabled ? this as ISpatialEntity : GetSeeingLimb() as ISpatialEntity; + seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity; if (seeingEntity == null) { return false; } ISpatialEntity sourceEntity = seeingEntity ; // TODO: Could we just use the method below? If not, let's refactor it so that we can. @@ -2023,16 +2037,11 @@ namespace Barotrauma { return wall != target; } - else if (body.UserData is Item item && item != target) + else if (body.UserData is Item item) { - // TODO: The door collider should be disabled, so this check is probably unnecessary. - var door = item.GetComponent(); - if (door != null) - { - return !door.CanBeTraversed; - } + return item != target; } - return false; + return true; } } @@ -2041,18 +2050,23 @@ namespace Barotrauma /// public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X; - public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this); + public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item, slotType) : item.IsOwnedBy(this); - public bool HasEquippedItem(Item item, InvSlotType? slotType = null) + public bool HasEquippedItem(Item item, InvSlotType? slotType = null, Func predicate = null) { if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) { + InvSlotType slot = Inventory.SlotTypes[i]; + if (predicate != null) + { + if (!predicate(slot)) { continue; } + } if (slotType.HasValue) { - if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; } + if (!slotType.Value.HasFlag(slot)) { continue; } } - else if (Inventory.SlotTypes[i] == InvSlotType.Any) + else if (slot == InvSlotType.Any) { continue; } @@ -2239,6 +2253,10 @@ namespace Barotrauma { return wire.Connections[0] == null; } + if (SelectedConstruction?.GetComponent()?.DisconnectedWires.Contains(wire) ?? false) + { + return wire.Connections[0] == null && wire.Connections[1] == null; + } } if (checkLinked && item.DisplaySideBySideWhenLinked) @@ -2472,7 +2490,10 @@ namespace Barotrauma { minDist = dist; nearbyLadder = ladder; - if (isControlled) ladder.Item.IsHighlighted = true; + if (isControlled) + { + ladder.Item.IsHighlighted = true; + } break; } } @@ -2480,7 +2501,10 @@ namespace Barotrauma if (nearbyLadder != null && climbInput) { - if (nearbyLadder.Select(this)) SelectedConstruction = nearbyLadder.Item; + if (nearbyLadder.Select(this)) + { + SelectedConstruction = nearbyLadder.Item; + } } } @@ -2502,14 +2526,20 @@ namespace Barotrauma { DeselectCharacter(); #if CLIENT - if (Controlled == this) CharacterHealth.OpenHealthWindow = null; + if (Controlled == this) + { + CharacterHealth.OpenHealthWindow = null; + } #endif } else { SelectCharacter(FocusedCharacter); #if CLIENT - if (Controlled == this) CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth; + if (Controlled == this) + { + CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth; + } #endif } } @@ -2644,7 +2674,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); @@ -2714,7 +2744,7 @@ namespace Barotrauma if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { Implode(); - if (IsDead) { return; } + if (IsDead) { return; } } } } @@ -2723,7 +2753,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) @@ -2874,6 +2906,18 @@ namespace Barotrauma } } + public float GetDamageDoneByAttacker(Character otherCharacter) + { + if (otherCharacter == null) { return 0; } + float dmg = 0; + Attacker attacker = LastAttackers.LastOrDefault(a => a.Character == otherCharacter); + if (attacker != null) + { + dmg = attacker.Damage; + } + return dmg; + } + private void UpdateAttackers(float deltaTime) { //slowly forget about damage done by attackers @@ -3128,47 +3172,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 @@ -3324,6 +3374,8 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (string.IsNullOrEmpty(message)) { return; } + if (SpeechImpediment >= 100.0f) { return; } + if (prevAiChatMessages.ContainsKey(identifier) && prevAiChatMessages[identifier] < Timing.TotalTime - minDurationBetweenSimilar) { @@ -3341,13 +3393,13 @@ namespace Barotrauma private void UpdateAIChatMessages(float deltaTime) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } List sentMessages = new List(); foreach (AIChatMessage message in aiChatMessageQueue) { message.SendDelay -= deltaTime; - if (message.SendDelay > 0.0f) continue; + if (message.SendDelay > 0.0f) { continue; } if (message.MessageType == null) { @@ -3424,9 +3476,9 @@ namespace Barotrauma { if (Removed) { - string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = "Tried to apply an attack to a removed character ([name]).\n" + Environment.StackTrace.CleanupStackTrace(); + DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); + GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName)); return new AttackResult(); } @@ -3615,7 +3667,7 @@ namespace Barotrauma // { // string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})"; // GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name, -// GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, +// GameAnalyticsManager.ErrorSeverity.Warning, // errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); //#if DEBUG // DebugConsole.ThrowError(errorMsg); @@ -3628,7 +3680,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(); } } } @@ -3845,7 +3897,7 @@ namespace Barotrauma { string errorMsg = "Attempted to apply an invalid impulse to a limb in Character.BreakJoints (" + diff + "). Limb position: " + limb.SimPosition + ", center of mass: " + centerOfMass + "."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -3890,7 +3942,7 @@ namespace Barotrauma AnimController.Frozen = false; - if (GameSettings.SendUserStatistics) + if (GameAnalyticsManager.SendUserStatistics) { string characterType = "Unknown"; @@ -3940,6 +3992,7 @@ namespace Barotrauma } SelectedConstruction = null; + SelectedCharacter = null; AnimController.ResetPullJoints(); @@ -4068,10 +4121,15 @@ 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); + if (AIController is HumanAIController humanAI) + { + humanAI.PathSteering?.ResetPath(); + } } public static void SaveInventory(Inventory inventory, XElement parentElement) @@ -4442,6 +4500,10 @@ namespace Barotrauma public static IEnumerable GetFriendlyCrew(Character character) { + if (character is null) + { + return Enumerable.Empty(); + } return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index c193a3918..e2772ba1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -365,6 +365,30 @@ namespace Barotrauma public const int MaxCurrentOrders = 3; public static int HighestManualOrderPriority => MaxCurrentOrders; + public int GetManualOrderPriority(Order order) + { + if (order != null && order.AssignmentPriority < 100 && CurrentOrders.Any()) + { + int orderPriority = HighestManualOrderPriority; + for (int i = 0; i < CurrentOrders.Count; i++) + { + if (CurrentOrders[i].Order is Order currentOrder && order.AssignmentPriority >= currentOrder.AssignmentPriority) + { + break; + } + else + { + orderPriority--; + } + } + return Math.Max(orderPriority, 1); + } + else + { + return HighestManualOrderPriority; + } + } + public List CurrentOrders { get; } = new List(); //unique ID given to character infos in MP @@ -1176,13 +1200,13 @@ 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); } - public void IncreaseSkillLevel(string skillIdentifier, float increase, bool gainedFromApprenticeship = false) + public void IncreaseSkillLevel(string skillIdentifier, float increase, bool gainedFromAbility = false) { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } @@ -1202,7 +1226,7 @@ namespace Barotrauma { // assume we are getting at least 1 point in skill, since this logic only runs in such cases float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f); - var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromApprenticeship); + var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility); Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain); foreach (Character character in Character.GetFriendlyCrew(Character)) { @@ -1276,17 +1300,16 @@ namespace Barotrauma public float GetProgressTowardsNextLevel() { - float progress = (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel()); - return progress; + return (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (float)(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); @@ -1388,7 +1411,6 @@ namespace Barotrauma foreach (var savedStat in statValuePair.Value) { if (savedStat.StatValue == 0f) { continue; } - if (savedStat.RemoveAfterRound) { continue; } savedStatElement.Add(new XElement("savedstatvalue", new XAttribute("stattype", statValuePair.Key.ToString()), @@ -1746,6 +1768,20 @@ namespace Barotrauma OnPermanentStatChanged(statType); } + public void RemoveSavedStatValuesOnDeath() + { + foreach (StatTypes statType in SavedStatValues.Keys) + { + foreach (SavedStatValue savedStatValue in SavedStatValues[statType]) + { + if (!savedStatValue.RemoveOnDeath) { continue; } + if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; } + savedStatValue.StatValue = 0.0f; + // no need to make a network update, as this is only done after the character has died + } + } + } + public void ResetSavedStatValue(string statIdentifier) { foreach (StatTypes statType in SavedStatValues.Keys) @@ -1785,7 +1821,7 @@ namespace Barotrauma } } - public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, bool removeAfterRound = false, float maxValue = float.MaxValue, bool setValue = false) + public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) { if (!SavedStatValues.ContainsKey(statType)) { @@ -1801,7 +1837,7 @@ namespace Barotrauma } else { - SavedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath, removeAfterRound)); + SavedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath)); changed = true; } if (changed) { OnPermanentStatChanged(statType); } @@ -1813,29 +1849,27 @@ namespace Barotrauma public string StatIdentifier { get; set; } public float StatValue { get; set; } public bool RemoveOnDeath { get; set; } - public bool RemoveAfterRound { get; set; } - public SavedStatValue(string statIdentifier, float value, bool removeOnDeath, bool retainAfterRound) + public SavedStatValue(string statIdentifier, float value, bool removeOnDeath) { StatValue = value; RemoveOnDeath = removeOnDeath; StatIdentifier = statIdentifier; - RemoveAfterRound = retainAfterRound; } } class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter { - public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromApprenticeship) + public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromAbility) { Value = value; String = abilityString; Character = character; - GainedFromApprenticeship = gainedFromApprenticeship; + GainedFromAbility = gainedFromAbility; } public Character Character { get; set; } public float Value { get; set; } public string String { get; set; } - public bool GainedFromApprenticeship { get; set; } + public bool GainedFromAbility { get; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index ab825d986..cb0d4507a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -99,15 +99,21 @@ namespace Barotrauma public float GetVitalityDecrease(CharacterHealth characterHealth) { - if (Strength < Prefab.ActivationThreshold) { return 0.0f; } - AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + return GetVitalityDecrease(characterHealth, Strength); + } + + public float GetVitalityDecrease(CharacterHealth characterHealth, float strength) + { + if (strength < Prefab.ActivationThreshold) { return 0.0f; } + strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength); + AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; } float currVitalityDecrease = MathHelper.Lerp( - currentEffect.MinVitalityDecrease, - currentEffect.MaxVitalityDecrease, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinVitalityDecrease, + currentEffect.MaxVitalityDecrease, + (strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); if (currentEffect.MultiplyByMaxVitality) { @@ -116,7 +122,8 @@ namespace Barotrauma return currVitalityDecrease; } - + + public float GetScreenGrainStrength() { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index b902c04cb..d1d728d12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -59,17 +59,25 @@ namespace Barotrauma } } - private float DormantThreshold => (Prefab as AfflictionPrefabHusk)?.DormantThreshold ?? Prefab.MaxStrength * 0.5f; - private float ActiveThreshold => (Prefab as AfflictionPrefabHusk)?.ActiveThreshold ?? Prefab.MaxStrength * 0.75f; + private readonly AfflictionPrefabHusk HuskPrefab; - private float TransitionThreshold => (Prefab as AfflictionPrefabHusk)?.TransitionThreshold ?? Prefab.MaxStrength * 0.75f; + private float DormantThreshold => HuskPrefab.DormantThreshold; + private float ActiveThreshold => HuskPrefab.ActiveThreshold; + private float TransitionThreshold => HuskPrefab.TransitionThreshold; + private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath; - private float TransformThresholdOnDeath => (Prefab as AfflictionPrefabHusk)?.TransformThresholdOnDeath ?? ActiveThreshold; - - public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { } + public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) + { + HuskPrefab = prefab as AfflictionPrefabHusk; + if (HuskPrefab == null) + { + DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!"); + } + } public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { + if (HuskPrefab == null) { return; } base.Update(characterHealth, targetLimb, deltaTime); character = characterHealth.Character; if (character == null) { return; } @@ -174,7 +182,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 +202,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 @@ -272,11 +281,13 @@ namespace Barotrauma if ((Prefab as AfflictionPrefabHusk)?.TransferBuffs ?? false) { - foreach (Affliction affliction in character.CharacterHealth.Afflictions) + foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) { if (affliction.Prefab.IsBuff) { - husk.CharacterHealth.ApplyAffliction(null, affliction.Prefab.Instantiate(affliction.Strength)); + husk.CharacterHealth.ApplyAffliction( + character.CharacterHealth.GetAfflictionLimb(affliction), + affliction.Prefab.Instantiate(affliction.Strength)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 2b87f5573..970570afb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -222,6 +222,9 @@ namespace Barotrauma [Serialize("", false)] public string DialogFlag { get; private set; } + [Serialize("", false)] + public string Tag { get; private set; } + [Serialize("0,0,0,0", false)] public Color MinFaceTint { get; private set; } @@ -234,6 +237,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 +253,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()) { @@ -266,6 +275,9 @@ namespace Barotrauma var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); AfflictionAbilityFlags.Add(flagType); break; + case "affliction": + DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects."); + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index cea7a1bf5..7dc1cba09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -22,18 +22,11 @@ namespace Barotrauma public readonly string Name; - public readonly List Afflictions = new List(); + //public readonly List Afflictions = new List(); public readonly Dictionary VitalityMultipliers = new Dictionary(); public readonly Dictionary VitalityTypeMultipliers = new Dictionary(); - private readonly CharacterHealth characterHealth; - - public float TotalDamage - { - get { return Afflictions.Sum(a => a.GetVitalityDecrease(characterHealth)); } - } - public LimbHealth() { } public LimbHealth(XElement element, CharacterHealth characterHealth) @@ -43,7 +36,6 @@ namespace Barotrauma { Name = TextManager.Get("HealthLimbName." + limbName); } - this.characterHealth = characterHealth; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -77,22 +69,16 @@ namespace Barotrauma } } } - - public List GetActiveAfflictions(AfflictionPrefab prefab) - { - return Afflictions.FindAll(a => a.Prefab == prefab); - } - public List GetActiveAfflictions(string afflictionType) - { - return Afflictions.FindAll(a => a.Prefab.AfflictionType == afflictionType); - } } public const float InsufficientOxygenThreshold = 30.0f; public const float LowOxygenThreshold = 50.0f; protected float minVitality; - protected float maxVitality + /// + /// Maximum vitality without talent- or job-based modifiers + /// + protected float UnmodifiedMaxVitality { get => Character.Params.Health.Vitality; set => Character.Params.Health.Vitality = value; @@ -119,13 +105,8 @@ namespace Barotrauma } private readonly List limbHealths = new List(); - //non-limb-specific afflictions - private readonly List afflictions = new List(); - /// - /// Note: returns only the non-limb-secific afflictions. Use GetAllAfflictions or some other method for getting also the limb-specific afflictions. - /// - public IEnumerable Afflictions => afflictions; + private readonly Dictionary afflictions = new Dictionary(); private readonly HashSet irremovableAfflictions = new HashSet(); private Affliction bloodlossAffliction; private Affliction oxygenLowAffliction; @@ -147,7 +128,7 @@ namespace Barotrauma { get { - float max = maxVitality; + float max = UnmodifiedMaxVitality; if (Character?.Info?.Job?.Prefab != null) { max += Character.Info.Job.Prefab.VitalityModifier; @@ -243,7 +224,7 @@ namespace Barotrauma this.Character = character; InitIrremovableAfflictions(); - Vitality = maxVitality; + Vitality = UnmodifiedMaxVitality; minVitality = character.IsHuman ? -100.0f : 0.0f; @@ -270,57 +251,37 @@ namespace Barotrauma irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f)); foreach (Affliction affliction in irremovableAfflictions) { - afflictions.Add(affliction); + afflictions.Add(affliction, null); } } partial void InitProjSpecific(XElement element, Character character); - public IEnumerable GetAllAfflictions(Func limbHealthFilter = null) + public IReadOnlyCollection GetAllAfflictions() { - return limbHealthFilter == null - ? afflictions.Union(limbHealths.SelectMany(lh => lh.Afflictions)) - : afflictions.Where(limbHealthFilter).Union(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter))); + return afflictions.Keys; + } + + public IEnumerable GetAllAfflictions(Func limbHealthFilter) + { + return afflictions.Keys.Where(limbHealthFilter); + } + + private float GetTotalDamage(LimbHealth limbHealth) + { + float totalDamage = 0.0f; + foreach (KeyValuePair kvp in afflictions) + { + if (kvp.Value != limbHealth) { continue; } + var affliction = kvp.Key; + totalDamage += affliction.GetVitalityDecrease(this); + } + return totalDamage; } private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex]; private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false)); - /// - /// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb. - /// - private IEnumerable GetMatchingAfflictions(LimbHealth limb) - => limb.Afflictions.Union(afflictions.Where(a => GetMatchingLimbHealth(a) == limb)); - - /// - /// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb. - /// - private IEnumerable GetMatchingAfflictions(LimbHealth limb, Func predicate) - => limb.Afflictions.Where(predicate).Union(afflictions.Where(a => predicate(a) && GetMatchingLimbHealth(a) == limb)); - - public IEnumerable GetAfflictionsByType(string afflictionType, bool allowLimbAfflictions = true) - { - if (allowLimbAfflictions) - { - return GetAllAfflictions(a => a.Prefab.AfflictionType == afflictionType); - } - else - { - return afflictions.Where(a => a.Prefab.AfflictionType == afflictionType); - } - } - - public IEnumerable GetAfflictionsByType(string afflictionType, Limb limb) - { - if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) - { - DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name + - "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex); - return null; - } - return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType); - } - public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) => GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions); @@ -329,20 +290,10 @@ namespace Barotrauma private Affliction GetAffliction(Func predicate, bool allowLimbAfflictions = true) { - foreach (Affliction affliction in afflictions) + foreach (KeyValuePair kvp in afflictions) { - if (predicate(affliction)) { return affliction; } - } - if (!allowLimbAfflictions) - { - return null; - } - foreach (LimbHealth limbHealth in limbHealths) - { - foreach (Affliction affliction in limbHealth.Afflictions) - { - if (predicate(affliction)) { return affliction; } - } + if (!allowLimbAfflictions && kvp.Value != null) { continue; } + if (predicate(kvp.Key)) { return kvp.Key; } } return null; } @@ -360,19 +311,22 @@ namespace Barotrauma "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex); return null; } - foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions) + foreach (KeyValuePair kvp in afflictions) { - if (affliction.Prefab.Identifier == identifier) return affliction; + if (limbHealths[limb.HealthIndex] == kvp.Value && kvp.Key.Prefab.Identifier == identifier) { return kvp.Key; } } return null; } public Limb GetAfflictionLimb(Affliction affliction) { - for (int i = 0; i < limbHealths.Count; i++) + foreach (KeyValuePair kvp in afflictions) { - if (!limbHealths[i].Afflictions.Contains(affliction)) continue; - return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i); + if (kvp.Key == affliction) + { + int limbHealthIndex = limbHealths.IndexOf(kvp.Value); + return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealthIndex); + } } return null; } @@ -389,12 +343,17 @@ namespace Barotrauma if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; } float strength = 0.0f; - foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions) + LimbHealth limbHealth = limbHealths[limb.HealthIndex]; + foreach (KeyValuePair kvp in afflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } - if (affliction.Prefab.AfflictionType == afflictionType) + if (kvp.Value == limbHealth) { - strength += affliction.Strength; + Affliction affliction = kvp.Key; + if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } + if (affliction.Prefab.AfflictionType == afflictionType) + { + strength += affliction.Strength; + } } } return strength; @@ -403,28 +362,16 @@ namespace Barotrauma public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true) { float strength = 0.0f; - foreach (Affliction affliction in afflictions) + foreach (KeyValuePair kvp in afflictions) { + if (!allowLimbAfflictions && kvp.Value != null) { continue; } + var affliction = kvp.Key; if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } if (affliction.Prefab.AfflictionType == afflictionType) { strength += affliction.Strength; } } - if (!allowLimbAfflictions) { return strength; } - - foreach (LimbHealth limbHealth in limbHealths) - { - foreach (Affliction affliction in limbHealth.Afflictions) - { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } - if (affliction.Prefab.AfflictionType == afflictionType) - { - strength += affliction.Strength; - } - } - } - return strength; } @@ -469,73 +416,77 @@ namespace Barotrauma } } - public float GetResistance(AfflictionPrefab affliction) + public float GetResistance(AfflictionPrefab afflictionPrefab) { float resistance = 0.0f; - for (int i = 0; i < afflictions.Count; i++) + foreach (KeyValuePair kvp in afflictions) { - resistance += afflictions[i].GetResistance(affliction); + var affliction = kvp.Key; + resistance += affliction.GetResistance(afflictionPrefab); } - return 1 - ((1 - resistance) * Character.GetAbilityResistance(affliction)); + return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); } public float GetStatValue(StatTypes statType) { float value = 0f; - for (int i = 0; i < afflictions.Count; i++) + foreach (KeyValuePair kvp in afflictions) { - value += afflictions[i].GetStatValue(statType); + var affliction = kvp.Key; + value += affliction.GetStatValue(statType); } return value; } public bool HasFlag(AbilityFlags flagType) { - for (int i = 0; i < afflictions.Count; i++) + foreach (KeyValuePair kvp in afflictions) { - if (afflictions[i].HasFlag(flagType)) { return true; } + var affliction = kvp.Key; + if (affliction.HasFlag(flagType)) { return true; } } return false; } private readonly List matchingAfflictions = new List(); - public void ReduceAffliction(Limb targetLimb, string affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAffliction(Limb targetLimb, string afflictionIdentifier, float amount, ActionType? treatmentAction = null) { matchingAfflictions.Clear(); - matchingAfflictions.AddRange(afflictions); - if (targetLimb != null) + + if (targetLimb == null) { - matchingAfflictions.AddRange(limbHealths[targetLimb.HealthIndex].Afflictions); + matchingAfflictions.AddRange(afflictions.Keys); } else { - foreach (LimbHealth limbHealth in limbHealths) + foreach (KeyValuePair kvp in afflictions) { - matchingAfflictions.AddRange(limbHealth.Afflictions); + var affliction = kvp.Key; + if (kvp.Value == null) + { + matchingAfflictions.Add(affliction); + } + else if (limbHealths[targetLimb.HealthIndex] == kvp.Value) + { + matchingAfflictions.Add(affliction); + } } } - if (!string.IsNullOrEmpty(affliction)) + if (!string.IsNullOrEmpty(afflictionIdentifier)) { matchingAfflictions.RemoveAll(a => - !a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) && - !a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase)); + !a.Prefab.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase) && + !a.Prefab.AfflictionType.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase)); } - if (matchingAfflictions.Count == 0) return; + if (matchingAfflictions.Count == 0) { return; } float reduceAmount = amount / matchingAfflictions.Count; for (int i = matchingAfflictions.Count - 1; i >= 0; i--) { var matchingAffliction = matchingAfflictions[i]; - // this logic runs very often, so culling unnecessary object creation and talent checking with this method - if (Character.HasTalents()) - { - var afflictionReduction = new AbilityValueAffliction(reduceAmount, matchingAffliction); - Character.CheckTalents(AbilityEffectType.OnReduceAffliction, afflictionReduction); - } - if (matchingAffliction.Strength < reduceAmount) { float surplus = reduceAmount - matchingAffliction.Strength; @@ -593,20 +544,28 @@ namespace Barotrauma } } } - + + private readonly static List afflictionsToRemove = new List(); + private readonly static List> afflictionsToUpdate = new List>(); public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) { if (Unkillable || Character.GodMode) { return; } - foreach (LimbHealth limbHealth in limbHealths) - { - limbHealth.Afflictions.RemoveAll(a => + + afflictionsToRemove.Clear(); + afflictionsToRemove.AddRange(afflictions.Keys.Where(a => a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType || a.Prefab.AfflictionType == AfflictionPrefab.Burn.AfflictionType || - a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType); + a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType)); + foreach (var affliction in afflictionsToRemove) + { + afflictions.Remove(affliction); + } - if (damageAmount > 0.0f) limbHealth.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount)); - if (bleedingDamageAmount > 0.0f && DoesBleed) limbHealth.Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount)); - if (burnDamageAmount > 0.0f) limbHealth.Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount)); + foreach (LimbHealth limbHealth in limbHealths) + { + if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); } + if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); } + if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); } } CalculateVitality(); @@ -643,12 +602,12 @@ namespace Barotrauma public void RemoveAllAfflictions() { - foreach (LimbHealth limbHealth in limbHealths) + afflictionsToRemove.Clear(); + afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a))); + foreach (var affliction in afflictionsToRemove) { - limbHealth.Afflictions.Clear(); + afflictions.Remove(affliction); } - - afflictions.RemoveAll(a => !irremovableAfflictions.Contains(a)); foreach (Affliction affliction in irremovableAfflictions) { affliction.Strength = 0.0f; @@ -658,13 +617,16 @@ namespace Barotrauma public void RemoveNegativeAfflictions() { - // also don't remove genetic effects, even if they're negative - foreach (LimbHealth limbHealth in limbHealths) + afflictionsToRemove.Clear(); + afflictionsToRemove.AddRange(afflictions.Keys.Where(a => + !irremovableAfflictions.Contains(a) && + !a.Prefab.IsBuff && + a.Prefab.AfflictionType != "geneticmaterialbuff" && + a.Prefab.AfflictionType != "geneticmaterialdebuff")); + foreach (var affliction in afflictionsToRemove) { - limbHealth.Afflictions.RemoveAll(a => !a.Prefab.IsBuff && a.Prefab.AfflictionType != "geneticmaterialbuff" && a.Prefab.AfflictionType != "geneticmaterialdebuff"); + afflictions.Remove(affliction); } - - afflictions.RemoveAll(a => !irremovableAfflictions.Contains(a) && !a.Prefab.IsBuff && a.Prefab.AfflictionType != "geneticmaterialbuff" && a.Prefab.AfflictionType != "geneticmaterialdebuff"); foreach (Affliction affliction in irremovableAfflictions) { affliction.Strength = 0.0f; @@ -688,36 +650,52 @@ namespace Barotrauma { if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - - foreach (Affliction affliction in limbHealth.Afflictions) + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } + if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { - if (newAffliction.Prefab == affliction.Prefab) + if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase))) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab)); - if (allowStacking) - { - // Add the existing strength - newStrength += affliction.Strength; - } - newStrength = Math.Min(affliction.Prefab.MaxStrength, newStrength); - if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); } - affliction.Strength = newStrength; - affliction.Source = newAffliction.Source; - CalculateVitality(); - if (Vitality <= MinVitality) - { - Kill(); - } return; } } + Affliction existingAffliction = null; + foreach (KeyValuePair kvp in afflictions) + { + var affliction = kvp.Key; + if (kvp.Value == limbHealth && kvp.Key.Prefab == newAffliction.Prefab) + { + existingAffliction = kvp.Key; + break; + } + } + + if (existingAffliction != null) + { + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab)); + if (allowStacking) + { + // Add the existing strength + newStrength += existingAffliction.Strength; + } + newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength); + if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); } + existingAffliction.Strength = newStrength; + existingAffliction.Source = newAffliction.Source; + CalculateVitality(); + if (Vitality <= MinVitality) + { + Kill(); + } + return; + } + //create a new instance of the affliction to make sure we don't use the same instance for multiple characters //or modify the affliction instance of an Attack or a StatusEffect var copyAffliction = newAffliction.Prefab.Instantiate( Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); - limbHealth.Afflictions.Add(copyAffliction); + afflictions.Add(copyAffliction, limbHealth); Character.HealthUpdateInterval = 0.0f; @@ -727,7 +705,7 @@ namespace Barotrauma Kill(); } #if CLIENT - if (CharacterHealth.OpenHealthWindow != this) + if (OpenHealthWindow != this && limbHealth != null) { selectedLimbIndex = -1; } @@ -736,52 +714,7 @@ namespace Barotrauma private void AddAffliction(Affliction newAffliction, bool allowStacking = true) { - if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } - if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } - if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) - { - if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase))) - { - return; - } - } - foreach (Affliction affliction in afflictions) - { - if (newAffliction.Prefab == affliction.Prefab) - { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab)); - if (allowStacking) - { - // Add the existing strength - newStrength += affliction.Strength; - } - newStrength = Math.Min(affliction.Prefab.MaxStrength, newStrength); - if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); } - affliction.Strength = newStrength; - affliction.Source = newAffliction.Source; - CalculateVitality(); - if (Vitality <= MinVitality) - { - Kill(); - } - return; - } - } - - //create a new instance of the affliction to make sure we don't use the same instance for multiple characters - //or modify the affliction instance of an Attack or a StatusEffect - afflictions.Add(newAffliction.Prefab.Instantiate( - Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), - source: newAffliction.Source)); - - Character.HealthUpdateInterval = 0.0f; - - CalculateVitality(); - if (Vitality <= MinVitality) - { - Kill(); - } + AddLimbAffliction(limbHealth: null, newAffliction, allowStacking); } partial void UpdateLimbAfflictionOverlays(); @@ -794,52 +727,44 @@ namespace Barotrauma if (Character.GodMode) { return; } - for (int i = 0; i < limbHealths.Count; i++) + afflictionsToRemove.Clear(); + afflictionsToUpdate.Clear(); + foreach (KeyValuePair kvp in afflictions) { - for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) - { - if (limbHealths[i].Afflictions[j].Strength <= 0.0f) - { - SteamAchievementManager.OnAfflictionRemoved(limbHealths[i].Afflictions[j], Character); - limbHealths[i].Afflictions.RemoveAt(j); - } - } - for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) - { - var affliction = limbHealths[i].Afflictions[j]; - Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i); - if (targetLimb == null) - { - targetLimb = Character.AnimController.MainLimb; - } - affliction.Update(this, targetLimb, deltaTime); - affliction.DamagePerSecondTimer += deltaTime; - if (affliction is AfflictionBleeding bleeding) - { - UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); - } - Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); - } - } - - for (int i = afflictions.Count - 1; i >= 0; i--) - { - var affliction = afflictions[i]; - if (irremovableAfflictions.Contains(affliction)) { continue; } + var affliction = kvp.Key; if (affliction.Strength <= 0.0f) { SteamAchievementManager.OnAfflictionRemoved(affliction, Character); - afflictions.RemoveAt(i); + if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } + continue; } + afflictionsToUpdate.Add(kvp); } - for (int i = 0; i < afflictions.Count; i++) + foreach (KeyValuePair kvp in afflictionsToUpdate) { - var affliction = afflictions[i]; - affliction.Update(this, null, deltaTime); + var affliction = kvp.Key; + Limb targetLimb = null; + if (kvp.Value != null) + { + int healthIndex = limbHealths.IndexOf(kvp.Value); + targetLimb = + Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ?? + Character.AnimController.MainLimb; + } + affliction.Update(this, targetLimb, deltaTime); affliction.DamagePerSecondTimer += deltaTime; + if (affliction is AfflictionBleeding bleeding) + { + UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); + } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } + foreach (var affliction in afflictionsToRemove) + { + afflictions.Remove(affliction); + } + Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); // maybe a bit of a hacky way to do this. should inquire if there is a better way. M61T @@ -869,20 +794,9 @@ namespace Barotrauma if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; } - for (int i = 0; i < limbHealths.Count; i++) + foreach (KeyValuePair kvp in afflictions) { - for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) - { - var affliction = limbHealths[i].Afflictions[j]; - Color faceTint = affliction.GetFaceTint(); - if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } - Color bodyTint = affliction.GetBodyTint(); - if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } - } - } - for (int i = 0; i < afflictions.Count; i++) - { - var affliction = afflictions[i]; + var affliction = kvp.Key; Color faceTint = affliction.GetFaceTint(); if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } Color bodyTint = affliction.GetBodyTint(); @@ -919,7 +833,7 @@ namespace Barotrauma public void SetVitality(float newVitality) { - maxVitality = newVitality; + UnmodifiedMaxVitality = newVitality; CalculateVitality(); } @@ -928,29 +842,22 @@ namespace Barotrauma Vitality = MaxVitality; if (Unkillable || Character.GodMode) { return; } - foreach (LimbHealth limbHealth in limbHealths) - { - foreach (Affliction affliction in limbHealth.Afflictions) - { - float vitalityDecrease = affliction.GetVitalityDecrease(this); - string identifier = affliction.Prefab.Identifier.ToLowerInvariant(); - string type = affliction.Prefab.AfflictionType.ToLowerInvariant(); - if (limbHealth.VitalityMultipliers.ContainsKey(identifier)) - { - vitalityDecrease *= limbHealth.VitalityMultipliers[identifier]; - } - if (limbHealth.VitalityTypeMultipliers.ContainsKey(type)) - { - vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type]; - } - Vitality -= vitalityDecrease; - affliction.CalculateDamagePerSecond(vitalityDecrease); - } - } - - foreach (Affliction affliction in afflictions) + foreach (KeyValuePair kvp in afflictions) { + var affliction = kvp.Key; + var limbHealth = kvp.Value; float vitalityDecrease = affliction.GetVitalityDecrease(this); + if (limbHealth != null) + { + if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier)) + { + vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier]; + } + if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType)) + { + vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType]; + } + } Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); } @@ -978,27 +885,28 @@ namespace Barotrauma // We need to use another list of the afflictions when we call the status effects triggered by afflictions, // because those status effects may add or remove other afflictions while iterating the collection. - private readonly List afflictionsCopy = new List(); + private readonly List<(Affliction affliction, Limb limb)> afflictionsCopy = new List<(Affliction affliction, Limb limb)>(); public void ApplyAfflictionStatusEffects(ActionType type) { - for (int i = 0; i < limbHealths.Count; i++) - { - for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) - { - var affliction = limbHealths[i].Afflictions[j]; - Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i); - if (targetLimb == null) - { - targetLimb = Character.AnimController.MainLimb; - } - affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb); - } - } afflictionsCopy.Clear(); - afflictionsCopy.AddRange(afflictions); - for (int i = afflictionsCopy.Count - 1; i >= 0; i--) + foreach (KeyValuePair kvp in afflictions) { - afflictionsCopy[i].ApplyStatusEffects(type, 1.0f, this, targetLimb: null); + var affliction = kvp.Key; + var limbHealth = kvp.Value; + Limb targetLimb = null; + if (limbHealth != null) + { + int healthIndex = limbHealths.IndexOf(limbHealth); + targetLimb = + Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ?? + Character.AnimController.MainLimb; + } + afflictionsCopy.Add((affliction, GetAfflictionLimb(affliction))); + } + + foreach ((Affliction affliction, Limb limb) in afflictionsCopy) + { + affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: limb); } } @@ -1026,28 +934,26 @@ namespace Barotrauma return (causeOfDeath, strongestAffliction); } - // TODO: this method is called a lot (every half second) -> optimize, don't create new class instances and lists every time! + private readonly List allAfflictions = new List(); private List GetAllAfflictions(bool mergeSameAfflictions) { - List allAfflictions = new List(afflictions); - foreach (LimbHealth limbHealth in limbHealths) + allAfflictions.Clear(); + if (!mergeSameAfflictions) { - allAfflictions.AddRange(limbHealth.Afflictions); + allAfflictions.AddRange(afflictions.Keys); } - - if (mergeSameAfflictions) + else { - List mergedAfflictions = new List(); - foreach (Affliction affliction in allAfflictions) + foreach (Affliction affliction in afflictions.Keys) { - var existingAffliction = mergedAfflictions.Find(a => a.Prefab == affliction.Prefab); + var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab); if (existingAffliction == null) { var newAffliction = affliction.Prefab.Instantiate(affliction.Strength); if (affliction.Source != null) { newAffliction.Source = affliction.Source; } newAffliction.DamagePerSecond = affliction.DamagePerSecond; newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer; - mergedAfflictions.Add(newAffliction); + allAfflictions.Add(newAffliction); } else { @@ -1055,10 +961,7 @@ namespace Barotrauma existingAffliction.Strength += affliction.Strength; } } - - return mergedAfflictions; } - return allAfflictions; } @@ -1067,27 +970,43 @@ namespace Barotrauma /// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating) /// /// A dictionary where the key is the identifier of the item and the value the suitability - /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. - /// Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random) - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float randomization = 0.0f) + /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. + /// If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds. + public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) { //key = item identifier //float = suitability treatmentSuitability.Clear(); float minSuitability = -10, maxSuitability = 10; - foreach (Affliction affliction in getAfflictions(limb)) + foreach (KeyValuePair kvp in afflictions) { - if (affliction.Strength <= affliction.Prefab.TreatmentThreshold) { continue; } - if (ignoreHiddenAfflictions && affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; } + var affliction = kvp.Key; + var limbHealth = kvp.Value; + if (limb != null && affliction.Prefab.IndicatorLimb != limb.type) + { + if (limbHealth == null) { continue; } + int healthIndex = limbHealths.IndexOf(limbHealth); + if (limb.HealthIndex != healthIndex) { continue; } + } + + float strength = affliction.Strength; + if (predictFutureDuration > 0.0f) + { + strength = GetPredictedStrength(affliction, predictFutureDuration, limb); + } + + if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } + if (ignoreHiddenAfflictions && strength < affliction.Prefab.ShowIconThreshold) { continue; } + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { if (!treatmentSuitability.ContainsKey(treatment.Key)) { - treatmentSuitability[treatment.Key] = treatment.Value * affliction.Strength; + treatmentSuitability[treatment.Key] = treatment.Value * strength; } else { - treatmentSuitability[treatment.Key] += treatment.Value * affliction.Strength; + treatmentSuitability[treatment.Key] += treatment.Value * strength; } minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability); maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability); @@ -1099,28 +1018,51 @@ namespace Barotrauma foreach (string treatment in treatmentSuitability.Keys.ToList()) { treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); - treatmentSuitability[treatment] = MathHelper.Lerp(treatmentSuitability[treatment], Rand.Range(0.0f, 1.0f), randomization); - } - } - else - { - foreach (string treatment in treatmentSuitability.Keys.ToList()) - { - treatmentSuitability[treatment] += Rand.Range(-100.0f, 100.0f) * randomization; } } + } - IEnumerable getAfflictions(Limb limb) + public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); + + private readonly HashSet afflictionTags = new HashSet(); + public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) + { + afflictionTags.Clear(); + foreach (Affliction affliction in afflictions) { - if (limb == null) + var currentEffect = affliction.GetActiveEffect(); + if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.Tag)) { - return GetAllAfflictions(); - } - else - { - return GetMatchingAfflictions(GetMatchingLimbHealth(limb)); + afflictionTags.Add(currentEffect.Tag); } } + return afflictionTags; + } + + public float GetPredictedStrength(Affliction affliction, float predictFutureDuration, Limb limb = null) + { + float strength = affliction.Strength; + foreach (var statusEffect in StatusEffect.DurationList) + { + if (!statusEffect.Targets.Any(t => t == Character || (limb != null && Character.AnimController.Limbs.Contains(t)))) { continue; } + float statusEffectDuration = Math.Min(statusEffect.Timer, predictFutureDuration); + foreach (var statusEffectAffliction in statusEffect.Parent.Afflictions) + { + if (statusEffectAffliction.Prefab == affliction.Prefab) + { + strength += statusEffectAffliction.Strength * statusEffectDuration; + } + } + foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction) + { + if (statusEffectAffliction.affliction.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) || + statusEffectAffliction.affliction.Equals(affliction.Prefab.AfflictionType, StringComparison.OrdinalIgnoreCase)) + { + strength -= statusEffectAffliction.amount * statusEffectDuration; + } + } + } + return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength); } private readonly List activeAfflictions = new List(); @@ -1128,8 +1070,11 @@ namespace Barotrauma public void ServerWrite(IWriteMessage msg) { activeAfflictions.Clear(); - foreach (var affliction in afflictions) + foreach (KeyValuePair kvp in afflictions) { + var affliction = kvp.Key; + var limbHealth = kvp.Value; + if (limbHealth != null) { continue; } if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold) { activeAfflictions.Add(affliction); @@ -1150,13 +1095,13 @@ namespace Barotrauma } limbAfflictions.Clear(); - foreach (LimbHealth limbHealth in limbHealths) + foreach (KeyValuePair kvp in afflictions) { - foreach (Affliction limbAffliction in limbHealth.Afflictions) - { - if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) continue; - limbAfflictions.Add((limbHealth, limbAffliction)); - } + var limbAffliction = kvp.Key; + var limbHealth = kvp.Value; + if (limbHealth == null) { continue; } + if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) { continue; } + limbAfflictions.Add((limbHealth, limbAffliction)); } msg.Write((byte)limbAfflictions.Count); @@ -1190,19 +1135,24 @@ namespace Barotrauma public void Save(XElement healthElement) { - foreach (Affliction affliction in afflictions) + foreach (KeyValuePair kvp in afflictions) { - if (affliction.Strength <= 0.0f) { continue; } + var affliction = kvp.Key; + var limbHealth = kvp.Value; + if (affliction.Strength <= 0.0f || limbHealth != null) { continue; } healthElement.Add(new XElement("Affliction", new XAttribute("identifier", affliction.Identifier), new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture)))); } + for (int i = 0; i < limbHealths.Count; i++) { var limbHealthElement = new XElement("LimbHealth", new XAttribute("i", i)); healthElement.Add(limbHealthElement); - foreach (Affliction affliction in limbHealths[i].Afflictions) + foreach (KeyValuePair kvp in afflictions.Where(a => a.Value == limbHealths[i])) { + var affliction = kvp.Key; + var limbHealth = kvp.Value; if (affliction.Strength <= 0.0f) { continue; } limbHealthElement.Add(new XElement("Affliction", new XAttribute("identifier", affliction.Identifier), @@ -1250,13 +1200,9 @@ namespace Barotrauma { irremovableAffliction.Strength = strength; } - else if (limbHealth != null) - { - limbHealth.Afflictions.Add(afflictionPrefab.Instantiate(strength)); - } else { - afflictions.Add(afflictionPrefab.Instantiate(strength)); + afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index e1dfcebc8..47a66ff4f 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 + }); } } } @@ -191,7 +194,7 @@ namespace Barotrauma { string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 2841d305e..7e11a575b 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 { @@ -147,7 +147,7 @@ namespace Barotrauma { string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); } 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..b0dc3c7be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -9,7 +9,7 @@ namespace Barotrauma public string Identifier { get; } public const float MaximumSkill = 100.0f; - + public float Level { get { return level; } @@ -18,7 +18,7 @@ namespace Barotrauma public void IncreaseSkill(float value, bool increasePastMax) { - level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? float.MaxValue : MaximumSkill); + level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumOlympianSkill : MaximumSkill); } private Sprite icon; @@ -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..6d7a8f929 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -364,7 +364,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif - GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsManager.ErrorSeverity.Error, "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } @@ -381,7 +381,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif - GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsManager.ErrorSeverity.Error, "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return 0.0f; } @@ -401,7 +401,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif - GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error, "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return 1.0f; } @@ -420,7 +420,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); #endif - GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error, "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); return Vector2.Zero; } @@ -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; @@ -464,7 +471,7 @@ namespace Barotrauma if (!MathUtils.IsValid(value)) { string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif @@ -478,7 +485,7 @@ namespace Barotrauma ", limb enabled: " + body.Enabled + ", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif @@ -497,7 +504,7 @@ namespace Barotrauma if (!MathUtils.IsValid(value)) { string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif @@ -511,7 +518,7 @@ namespace Barotrauma ", limb enabled: " + body.Enabled + ", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index be1df08e5..4d03cd6fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -656,6 +656,34 @@ namespace Barotrauma return target != null; } + public bool TryGetTarget(Character targetCharacter, out TargetParams target) + { + if (!TryGetTarget(targetCharacter.SpeciesName, out target)) + { + target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetCharacter.Params.Group.ToString(), StringComparison.OrdinalIgnoreCase)); + } + return target != null; + } + + public bool TryGetTarget(IEnumerable tags, out TargetParams target) + { + target = null; + if (tags == null || tags.None()) { return false; } + float priority = -1; + foreach (var potentialTarget in targets) + { + if (potentialTarget.Priority > priority) + { + if (tags.Any(t => string.Equals(t, potentialTarget.Tag, StringComparison.OrdinalIgnoreCase))) + { + target = potentialTarget; + priority = target.Priority; + } + } + } + return target != null; + } + public TargetParams GetTarget(string targetTag, bool throwError = true) { if (!TryGetTarget(targetTag, out TargetParams target)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index 433da2d10..43085c608 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -96,6 +96,13 @@ namespace Barotrauma set; } + [Serialize(500.0f, true)] + public float MaximumOlympianSkill + { + get; + set; + } + private SkillSettings(XElement element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); 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..93b267cd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -1,4 +1,5 @@ using Barotrauma.Items.Components; +using System; using System.Linq; using System.Xml.Linq; @@ -10,24 +11,25 @@ namespace Barotrauma.Abilities { Any = 0, Melee = 1, - Ranged = 2 + Ranged = 2, + HandheldRanged = 3, + Turret = 4 }; private readonly string itemIdentifier; private readonly string[] tags; private readonly WeaponType weapontype; + private readonly bool ignoreNonHarmfulAttacks; public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { - itemIdentifier = conditionElement.GetAttributeString("itemidentifier", ""); + itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty); tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); - switch (conditionElement.GetAttributeString("weapontype", "")) + ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false); + + string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); + if (!Enum.TryParse(weaponTypeStr, ignoreCase: true, out weapontype)) { - case "melee": - weapontype = WeaponType.Melee; - break; - case "ranged": - weapontype = WeaponType.Ranged; - break; + DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type."); } } @@ -35,17 +37,19 @@ namespace Barotrauma.Abilities { if (abilityObject is AbilityAttackData attackData) { - Item item = attackData?.SourceAttack?.SourceItem; - - if (item == null) + if (ignoreNonHarmfulAttacks && attackData.SourceAttack != null) { - DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!"); - return false; + if (attackData.SourceAttack.Stun <= 0.0f && (attackData.SourceAttack.Afflictions?.All(a => a.Key.Prefab.IsBuff) ?? true)) + { + return false; + } } + Item item = attackData?.SourceAttack?.SourceItem; + if (!string.IsNullOrEmpty(itemIdentifier)) { - if (item.prefab.Identifier != itemIdentifier) + if (item?.prefab.Identifier != itemIdentifier) { return false; } @@ -53,18 +57,34 @@ namespace Barotrauma.Abilities if (tags.Any()) { - if (!tags.All(t => item.HasTag(t))) + if (!tags.All(t => item?.HasTag(t) ?? false)) { return false; } } - switch (weapontype) + if (weapontype != WeaponType.Any) { - case WeaponType.Melee: - return item.GetComponent() != null; - case WeaponType.Ranged: - return item.GetComponent() != null; + 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; + case WeaponType.HandheldRanged: + { + var projectile = item?.GetComponent(); + return projectile?.Launcher?.GetComponent() != null; + } + case WeaponType.Turret: + { + var projectile = item?.GetComponent(); + return projectile?.Launcher?.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/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs new file mode 100644 index 000000000..6be5969f8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs @@ -0,0 +1,28 @@ +using System.Xml.Linq; +using static Barotrauma.StatusEffect; + +namespace Barotrauma.Abilities +{ + class AbilityConditionStatusEffectIdentifier : AbilityConditionData + { + private string effectIdentifier; + + public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + effectIdentifier = conditionElement.GetAttributeString("effectidentifier", "").ToLowerInvariant(); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is AbilityStatusEffectIdentifier abilityStatusEffectIdentifier) + { + return abilityStatusEffectIdentifier.EffectIdentifier == effectIdentifier; + } + else + { + LogAbilityConditionError(abilityObject, typeof(AbilityStatusEffectIdentifier)); + return false; + } + } + } +} 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/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 43fef2a11..10d80a1a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -5,18 +5,21 @@ namespace Barotrauma.Abilities { class CharacterAbilityGainSimultaneousSkill : CharacterAbility { - private string skillIdentifier; + private readonly string skillIdentifier; + private readonly bool ignoreAbilitySkillGain; public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityValue)?.Value is float skillIncrease) + if (abilityObject is AbilitySkillGain abilitySkillGain) { - Character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease); + if (ignoreAbilitySkillGain && !abilitySkillGain.GainedFromAbility) { return; } + Character.Info?.IncreaseSkillLevel(skillIdentifier, abilitySkillGain.Value, gainedFromAbility: true); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index 6fb4887b3..0998bf475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Abilities value = abilityElement.GetAttributeFloat("value", 0f); maxValue = abilityElement.GetAttributeFloat("maxvalue", float.MaxValue); targetAllies = abilityElement.GetAttributeBool("targetallies", false); - removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true); + removeOnDeath = abilityElement.GetAttributeBool("removeondeath", false); giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); setValue = abilityElement.GetAttributeBool("setvalue", false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs index d93519de0..7a53e6d91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -49,11 +49,11 @@ namespace Barotrauma.Abilities { var skill = character.Info?.Job?.Skills?.GetRandom(); if (skill == null) { return; } - character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease); + character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease, gainedFromAbility: true); } else { - character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease); + character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease, gainedFromAbility: true); } } } 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/CharacterAbilityModifyReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs deleted file mode 100644 index cc36bce31..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class CharacterAbilityModifyReduceAffliction : CharacterAbility - { - float addedAmountMultiplier; - public override bool AllowClientSimulation => true; - - public CharacterAbilityModifyReduceAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) - { - addedAmountMultiplier = abilityElement.GetAttributeFloat("addedamountmultiplier", 0f); - } - - protected override void ApplyEffect(AbilityObject abilityObject) - { - if (abilityObject is AbilityValueAffliction afflictionReduceAmount) - { - afflictionReduceAmount.Affliction.Strength -= addedAmountMultiplier * afflictionReduceAmount.Value; - } - else - { - LogabilityObjectMismatch(); - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs index e80bf63db..3d280aeeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -1,6 +1,4 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -13,6 +11,10 @@ namespace Barotrauma.Abilities { itemIdentifier = abilityElement.GetAttributeString("itemidentifier", ""); amount = abilityElement.GetAttributeInt("amount", 1); + if (string.IsNullOrEmpty(itemIdentifier)) + { + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined."); + } } protected override void ApplyEffect() @@ -34,7 +36,13 @@ namespace Barotrauma.Abilities if (GameMain.GameSession?.RoundEnding ?? true) { Item item = new Item(itemPrefab, Character.WorldPosition, Character.Submarine); - Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any }); + if (!Character.Inventory.TryPutItem(item, Character, item.AllowedSlots)) + { + foreach (Item containedItem in Character.Inventory.AllItemsMod) + { + if (containedItem.OwnInventory?.TryPutItem(item, Character) ?? false) { break; } + } + } } else { 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/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index 95466f08d..03f68259f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -6,15 +6,19 @@ namespace Barotrauma.Abilities { class CharacterAbilityApprenticeship : CharacterAbility { + private readonly bool ignoreAbilitySkillGain; + public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { + ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); } protected override void ApplyEffect(AbilityObject abilityObject) { - if (abilityObject is AbilitySkillGain abilitySkillGain && !abilitySkillGain.GainedFromApprenticeship && abilitySkillGain.Character != Character) + if (abilityObject is AbilitySkillGain abilitySkillGain && abilitySkillGain.Character != Character) { - Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromApprenticeship: true); + if (ignoreAbilitySkillGain && !abilitySkillGain.GainedFromAbility) { return; } + Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromAbility: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs index 339b5c47f..5cb3857fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities if (skillIdentifier != lastSkillIdentifier) { lastSkillIdentifier = skillIdentifier; - Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f); + Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, gainedFromAbility: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index 75668b0ca..8d6c64ac5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Abilities } } - if (closestCharacter.SelectedConstruction == null || !closestCharacter.SelectedConstruction.HasTag(tag)) { return; } + if (closestCharacter?.SelectedConstruction == null || !closestCharacter.SelectedConstruction.HasTag(tag)) { return; } if (closestDistance < squaredMaxDistance) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 277af65b0..492b857d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -31,7 +31,6 @@ namespace Barotrauma ConfigElement = element; Identifier = element.GetAttributeString("identifier", "noidentifier"); DisplayName = TextManager.Get("talentname." + Identifier, returnNull: true) ?? Identifier; - this.CalculatePrefabUIntIdentifier(TalentPrefabs); foreach (XElement subElement in element.Elements()) { @@ -101,37 +100,42 @@ 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; + var newPrefab = new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage }; + TalentPrefabs.Add(newPrefab, isOverride); + newPrefab.CalculatePrefabUIntIdentifier(TalentPrefabs); } + + 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 12be8b7e9..6e1353fd2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -110,7 +110,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"; public static void AssignOnExecute(string names, Action onExecute) @@ -219,7 +219,7 @@ namespace Barotrauma try { #if CLIENT - SpawnItem(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), Character.Controlled, out string errorMsg); + SpawnItem(args, Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? PlayerInput.MousePosition, Character.Controlled, out string errorMsg); #elif SERVER SpawnItem(args, Vector2.Zero, null, out string errorMsg); #endif @@ -232,7 +232,7 @@ namespace Barotrauma { string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\"."; ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); } }, () => @@ -240,7 +240,10 @@ namespace Barotrauma List itemNames = new List(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - itemNames.Add(itemPrefab.Name); + if (!itemNames.Contains(itemPrefab.Name)) + { + itemNames.Add(itemPrefab.Name); + } } List spawnPosParams = new List() { "cursor", "inventory" }; @@ -251,8 +254,8 @@ namespace Barotrauma return new string[][] { - itemNames.ToArray(), - spawnPosParams.ToArray() + itemNames.ToArray(), + spawnPosParams.ToArray() }; }, isCheat: true)); @@ -879,7 +882,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 { @@ -1063,7 +1066,7 @@ namespace Barotrauma }, null)); - IEnumerable TestLevels() + IEnumerable TestLevels() { SubmarineInfo selectedSub = null; string subName = GameMain.Config.QuickStartSubmarineName; @@ -1206,7 +1209,7 @@ namespace Barotrauma catch (InvalidOperationException e) { string errorMsg = "Error while executing the fixhulls command.\n" + e.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("DebugConsole.FixHulls", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.FixHulls", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } } }, null, true)); @@ -1411,6 +1414,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, () => { @@ -1506,6 +1532,11 @@ namespace Barotrauma } }, isCheat: true)); + commands.Add(new Command("skipeventcooldown", "skipeventcooldown: Skips the currently active event cooldown and triggers pending monster spawns immediately.", args => + { + GameMain.GameSession?.EventManager?.SkipEventCooldown(); + }, isCheat: true)); + commands.Add(new Command("ballastflora", "infectballast [options]: Infect ballasts and control its growth.", args => { if (args.Length == 0) @@ -1833,7 +1864,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; @@ -1849,7 +1880,7 @@ namespace Barotrauma ThrowError("Failed to execute command \"" + command + "\"!"); GameAnalyticsManager.AddErrorEventOnce( "DebugConsole.ExecuteCommand:LengthZero", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Failed to execute command \"" + command + "\"!"); return; } @@ -1858,7 +1889,7 @@ namespace Barotrauma if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase)) { - NewMessage(command, Color.White, true); + NewCommand(command); } #if CLIENT @@ -2138,7 +2169,7 @@ namespace Barotrauma { if (spawnPos != null) { - if (Entity.Spawner == null) + if (Entity.Spawner == null || Entity.Spawner.Removed) { new Item(itemPrefab, spawnPos.Value, null); } @@ -2174,15 +2205,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; } @@ -2272,7 +2325,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) @@ -2310,7 +2366,7 @@ namespace Barotrauma } #endif - NewMessage(error, Color.Red, isError: true); + ShowError(error); } public static void AddWarning(string warning) @@ -2320,7 +2376,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/Decals/DecalManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs index 277efee1c..2d7792808 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs @@ -74,24 +74,12 @@ namespace Barotrauma continue; } } - Prefabs.Add(new DecalPrefab(element, configFile), allowOverriding || sourceElement.IsOverride()); + var newPrefab = new DecalPrefab(element, configFile); + Prefabs.Add(newPrefab, allowOverriding || sourceElement.IsOverride()); + newPrefab.CalculatePrefabUIntIdentifier(Prefabs); break; } } - - using MD5 md5 = MD5.Create(); - foreach (DecalPrefab prefab in Prefabs) - { - prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); - - //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small - var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier); - if (collision != null) - { - DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Decals: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")"); - collision.UIntIdentifier++; - } - } } public void RemoveByFile(string filePath) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs index 3f80bbecd..bc0551501 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs @@ -5,7 +5,7 @@ using System.Xml.Linq; namespace Barotrauma { - class DecalPrefab : IPrefab, IDisposable + class DecalPrefab : IPrefab, IHasUintIdentifier, IDisposable { public readonly string Name; @@ -21,7 +21,7 @@ namespace Barotrauma /// Unique identifier that's generated by hashing the prefab's string identifier. /// Used to reduce the amount of bytes needed to write decal data into network messages in multiplayer. /// - public uint UIntIdentifier; + public uint UIntIdentifier { get; set; } public string FilePath { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 3ac2e2dbf..6bba12392 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -60,6 +60,7 @@ OnGainMissionMoney, OnLocationDiscovered, OnItemDeconstructed, + OnItemDeconstructedByAlly, OnItemDeconstructedMaterial, OnItemDeconstructedInventory, OnStopTinkering, @@ -68,6 +69,7 @@ OnCrewGeneticMaterialCombinedOrRefined, AfterSubmarineAttacked, OnApplyTreatment, + OnStatusEffectIdentifier, } public enum StatTypes @@ -111,6 +113,7 @@ GeneticMaterialRefineBonus, GeneticMaterialTaintedProbabilityReductionOnCombine, SkillGainSpeed, + MedicalItemApplyingMultiplier, // Tinker TinkeringDuration, TinkeringStrength, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs index f4b6d0f0a..73263a685 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs @@ -1,5 +1,4 @@ using System.Xml.Linq; -using NLog.Targets; namespace Barotrauma { 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/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index c96723441..a1a054a1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -92,6 +92,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); } + private void TagHullsByName(string name) + { + ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name, StringComparison.OrdinalIgnoreCase)); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } @@ -144,6 +149,9 @@ namespace Barotrauma case "itemtag": if (kvp.Length > 1) { TagItemsByTag(kvp[1].Trim()); } break; + case "hullname": + if (kvp.Length > 1) { TagHullsByName(kvp[1].Trim()); } + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index fea12aa2a..c75d1ea35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -34,6 +35,9 @@ namespace Barotrauma [Serialize(false, true, description: "If true, one target must interact with the other to trigger the action.")] public bool WaitForInteraction { get; set; } + [Serialize(false, true, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] + public bool AllowMultipleTargets { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) @@ -55,7 +59,7 @@ namespace Barotrauma public bool isRunning = false; - private Either npcOrItem = null; + private readonly List> npcsOrItems = new List>(); public override void Update(float deltaTime) { @@ -93,12 +97,16 @@ namespace Barotrauma Character player = null; Character npc = null; Item item = null; - npcOrItem?.TryGet(out npc); - npcOrItem?.TryGet(out item); if (e1 is Character char1) { - if (char1.IsBot) { npc ??= char1; } - else { player = char1; } + if (char1.IsBot) + { + npc ??= char1; + } + else + { + player = char1; + } } else { @@ -106,8 +114,14 @@ namespace Barotrauma } if (e2 is Character char2) { - if (char2.IsBot) { npc ??= char2; } - else { player = char2; } + if (char2.IsBot) + { + npc ??= char2; + } + else + { + player = char2; + } } else { @@ -120,7 +134,10 @@ namespace Barotrauma { if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine) { - npcOrItem = npc; + if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc)) + { + npcsOrItems.Add(npc); + } npc.CampaignInteractionType = CampaignMode.InteractionType.Examine; npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated; #if CLIENT @@ -134,12 +151,14 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); #endif } - - return; + if (!AllowMultipleTargets) { return; } } else if (item != null) { - npcOrItem = item; + if (!npcsOrItems.Any(n => n.TryGet(out Item item2) && item2 == item)) + { + npcsOrItems.Add(item); + } item.CampaignInteractionType = CampaignMode.InteractionType.Examine; if (player.SelectedConstruction == item || player.Inventory != null && player.Inventory.Contains(item) || @@ -170,19 +189,21 @@ namespace Barotrauma private void ResetTargetIcons() { - if (npcOrItem == null) { return; } - if (npcOrItem.TryGet(out Character npc)) + foreach (var npcOrItem in npcsOrItems) { - npc.CampaignInteractionType = CampaignMode.InteractionType.None; - npc.SetCustomInteract(null, null); - npc.RequireConsciousnessForCustomInteract = true; -#if SERVER - GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); -#endif - } - else if (npcOrItem.TryGet(out Item item)) - { - item.CampaignInteractionType = CampaignMode.InteractionType.None; + if (npcOrItem.TryGet(out Character npc)) + { + npc.CampaignInteractionType = CampaignMode.InteractionType.None; + npc.SetCustomInteract(null, null); + npc.RequireConsciousnessForCustomInteract = true; + #if SERVER + GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + #endif + } + else if (npcOrItem.TryGet(out Item item)) + { + item.CampaignInteractionType = CampaignMode.InteractionType.None; + } } } @@ -269,7 +290,7 @@ namespace Barotrauma return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" + (WaitForInteraction ? - $"Selected non-player target: {(npcOrItem?.ToString() ?? "").ColorizeObject()}, " : + $"Selected non-player target: {(npcsOrItems?.ToString() ?? "").ColorizeObject()}, " : $"Distance: {((int)distance).ColorizeObject()}, ") + $"Radius: {Radius.ColorizeObject()}, " + $"TargetTags: {Target1Tag.ColorizeObject()}, " + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 0e0e9a77c..f956de233 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -1,10 +1,9 @@ -using FarseerPhysics; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using NLog; namespace Barotrauma { @@ -33,6 +32,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 +51,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 +83,10 @@ namespace Barotrauma { get { return currentIntensity; } } + public float MusicIntensity + { + get { return musicIntensity; } + } public List ActiveEvents { @@ -85,7 +94,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 +123,7 @@ namespace Barotrauma if (isClient) { return; } + timeStamps.Clear(); pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); @@ -124,12 +149,12 @@ namespace Barotrauma } MTRandom rand = new MTRandom(seed); - EventSet initialEventSet = SelectRandomEvents(EventSet.List, rand); + EventSet initialEventSet = SelectRandomEvents(EventSet.List, requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; - initialEventSet = SelectRandomEvents(EventSet.List.FindAll(e => !e.Additive), rand); + initialEventSet = SelectRandomEvents(EventSet.List.FindAll(e => !e.Additive), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); } if (initialEventSet != null) { @@ -202,8 +227,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() @@ -389,6 +418,11 @@ namespace Barotrauma pathFinder = null; } + public void SkipEventCooldown() + { + eventCoolDown = 0.0f; + } + private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) { if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; } @@ -401,11 +435,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 +443,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 +451,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 +460,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 +493,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()); @@ -479,7 +505,7 @@ namespace Barotrauma } if (eventSet.ChildSets.Count > 0) { - var newEventSet = SelectRandomEvents(eventSet.ChildSets, rand); + var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); if (newEventSet != null) { CreateEvents(newEventSet, rand); @@ -498,11 +524,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()); @@ -518,7 +540,7 @@ namespace Barotrauma } } - private EventSet SelectRandomEvents(List eventSets, Random random = null) + private EventSet SelectRandomEvents(List eventSets, bool? requireCampaignSet = null, Random random = null) { if (level == null) { return null; } Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); @@ -529,6 +551,27 @@ namespace Barotrauma level.LevelData.Type == es.LevelType && (string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase))); + if (requireCampaignSet.HasValue) + { + if (requireCampaignSet.Value) + { + if (allowedEventSets.Any(es => es.IsCampaignSet)) + { + allowedEventSets = + allowedEventSets.Where(es => es.IsCampaignSet); + } + else + { + DebugConsole.AddWarning("No campaign event sets available. Using a non-campaign-specific set instead."); + } + } + else + { + allowedEventSets = + allowedEventSets.Where(es => !es.IsCampaignSet); + } + } + Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; LocationType locationType = location?.GetLocationType(); @@ -647,6 +690,7 @@ namespace Barotrauma isCrewAway = false; crewAwayDuration = 0.0f; eventThreshold += settings.EventThresholdIncrease * deltaTime; + eventThreshold = Math.Min(eventThreshold, 1.0f); eventCoolDown -= deltaTime; } @@ -739,7 +783,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 +793,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 +817,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 +893,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..60455b983 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 || (parentSet?.IsCampaignSet ?? false)); 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..53bbb3732 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; 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..21742b295 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; @@ -42,15 +42,15 @@ namespace Barotrauma { if (maxAmount <= 1) { - return "MonsterEvent (" + speciesName + ")"; + return $"MonsterEvent ({speciesName}, {SpawnPosType})"; } else if (minAmount < maxAmount) { - return "MonsterEvent (" + speciesName + " x" + minAmount + "-" + maxAmount + ")"; + return $"MonsterEvent ({speciesName} x{minAmount}-{maxAmount}, {SpawnPosType})"; } else { - return "MonsterEvent (" + speciesName + " x" + maxAmount + ")"; + return $"MonsterEvent ({speciesName} x{maxAmount}, {SpawnPosType})"; } } @@ -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/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 823a3149f..bc72b77cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -187,5 +187,14 @@ namespace Barotrauma.Extensions } return retVal; } + + public static int FindIndex(this IReadOnlyList list, Predicate predicate) + { + for (int i=0; i + /// No attempt to contact the consent server has been made + /// + Unknown, + + /// + /// An error occurred while attempting to retrieve consent status + /// + Error, + + /// + /// The consent status was not saved on the remote database + /// + Ask, + + /// + /// The user explicitly denied consent + /// + No, + + /// + /// The user explicitly granted consent + /// + Yes + } + + public static Consent UserConsented { get; private set; } = Consent.Unknown; + + public static bool SendUserStatistics => UserConsented == Consent.Yes && loadedImplementation != null; + + private static bool consentTextAvailable + => TextManager.ContainsTag("statisticsconsentheader") + && TextManager.ContainsTag("statisticsconsenttext"); + + private readonly static string consentServerUrl = "https://barotraumagame.com/baromaster/"; + private readonly static string consentServerFile = "consentserver.php"; + + private static string GetAuthTicket() + { + Steamworks.AuthTicket authTicket = SteamManager.GetAuthSessionTicket(); + //convert byte array to hex + return BitConverter.ToString(authTicket.Data).Replace("-", ""); + } + + /// + /// Sets the consent status. This method cannot be called to + /// set the status to Consent.Yes; only a positive response from + /// the database or the user accepting via the privacy policy + /// prompt should enable it. + /// + public static void SetConsent(Consent consent) + { + if (consent == Consent.Yes) + { + throw new Exception( + "Cannot call SetConsent with value Consent.Yes, must only be set to this value via consent prompt"); + } + SetConsentInternal(consent); + } + + /// + /// Implementation of the bulk of SetConsent. + /// DO NOT CALL THIS UNLESS NEEDED. + /// + private static void SetConsentInternal(Consent consent) + { + if (UserConsented == consent) { return; } + + if (consent == Consent.Ask) + { + CreateConsentPrompt(); + } + + if (consent != Consent.No && consent != Consent.Yes) + { + UserConsented = consent; + ShutDown(); + return; + } + if (consent == Consent.No) + { + UserConsented = consent; + ShutDown(); + } + + string authTicketStr; + try + { + authTicketStr = GetAuthTicket(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in GameAnalyticsManager.SetConsent. Could not get a Steam authentication ticket.", e); + return; + } + + RestClient client = null; + try + { + client = new RestClient(consentServerUrl); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while connecting to consent server", e); + } + if (client == null) { return; } + + var request = new RestRequest(consentServerFile, Method.GET); + request.AddParameter("authticket", authTicketStr); + request.AddParameter("action", "setconsent"); + request.AddParameter("consent", consent == Consent.Yes ? 1 : 0); + + var response = client.Execute(request, Method.GET); + if (CheckResponse(response)) + { + UserConsented = consent; + if (consent == Consent.Yes) + { + Init(); + } + } + } + + static partial void CreateConsentPrompt(); + + public static void InitIfConsented() + { + if (!consentTextAvailable) + { + SetConsent(Consent.Unknown); + return; + } + + static void error(string reason, Exception exception) + { + DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception); + SetConsent(Consent.Error); + } + + string authTicketStr; + try + { + authTicketStr = GetAuthTicket(); + } + catch (Exception e) + { + error("Could not get a Steam authentication ticket.", e); + return; + } + + RestClient client; + try + { + client = new RestClient(consentServerUrl); + } + catch (Exception e) + { + error("Error while connecting to consent server.", e); + return; + } + + var request = new RestRequest(consentServerFile, Method.GET); + request.AddParameter("authticket", authTicketStr); + request.AddParameter("action", "getconsent"); + + TaskPool.Add($"{nameof(GameAnalyticsManager)}.{nameof(InitIfConsented)}", client.ExecuteAsync(request), (t) => + { + if (t.Exception != null) + { + error("Error executing the request to the consent server.", t.Exception.InnerException); + return; + } + + var response = ((Task)t).Result; + if (!CheckResponse(response)) + { + SetConsent(Consent.Error); + } + else if (string.IsNullOrEmpty(response.Content)) + { + SetConsent(Consent.Ask); + } + else + { + SetConsentInternal(response.Content[0] == '1' + ? Consent.Yes + : Consent.No); + } + }); + } + + private static bool CheckResponse(IRestResponse response) + { + if (response.ErrorException != null) + { + DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerErrorException", "[error]", response.ErrorException.ToString())); + return false; + } + else if (response.StatusCode != HttpStatusCode.OK) + { + switch (response.StatusCode) + { + case HttpStatusCode.NotFound: + DebugConsole.ThrowError(TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", consentServerUrl)); + break; + case HttpStatusCode.ServiceUnavailable: + DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); + break; + default: + DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", new string[2] { "[statuscode]", "[statusdescription]" }, + new string[2] { response.StatusCode.ToString(), response.StatusDescription })); + break; + } + } + return response.StatusCode == HttpStatusCode.OK; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs new file mode 100644 index 000000000..77efeba8d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -0,0 +1,390 @@ +#nullable enable +using Barotrauma.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace Barotrauma +{ + public static partial class GameAnalyticsManager + { + public enum ErrorSeverity + { + Undefined = 0, + Debug = 1, + Info = 2, + Warning = 3, + Error = 4, + Critical = 5 + } + + public enum ProgressionStatus + { + Undefined = 0, + Start = 1, + Complete = 2, + Fail = 3 + } + + private readonly static HashSet sentEventIdentifiers = new HashSet(); + + private class Implementation : IDisposable + { + #region GameAnalytics methods + private readonly Action initialize; + internal void Initialize(string gameKey, string secretKey) + => initialize(gameKey, secretKey); + + private readonly Action configureBuild; + internal void ConfigureBuild(string config) => configureBuild(config); + + private readonly Action addErrorEvent; + internal void AddErrorEvent(ErrorSeverity severity, string message) + => addErrorEvent(severity, message); + + private readonly Action?> addDesignEvent0; + internal void AddDesignEvent(string message, IDictionary? fields = null) + => addDesignEvent0(message, fields); + + private readonly Action addDesignEvent1; + internal void AddDesignEvent(string message, double value) + => addDesignEvent1(message, value); + + private readonly Action addProgressionEvent01; + internal void AddProgressionEvent(ProgressionStatus status, string progression01) + => addProgressionEvent01(status, progression01); + + private readonly Action addProgressionEvent01Score; + internal void AddProgressionEvent(ProgressionStatus status, string progression01, double score) + => addProgressionEvent01Score(status, progression01, score); + + private readonly Action addProgressionEvent02; + internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02) + => addProgressionEvent02(status, progression01, progression02); + + private readonly Action addProgressionEvent03; + internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02, string progression03) + => addProgressionEvent03(status, progression01, progression02, progression03); + + private readonly Action setCustomDimension01; + internal void SetCustomDimension01(string dimension01) + => setCustomDimension01(dimension01); + + private readonly Action configureAvailableCustomDimensions01; + internal void ConfigureAvailableCustomDimensions01(params string[] customDimensions) + => configureAvailableCustomDimensions01(customDimensions); + + private readonly Action setEnabledInfoLog; + internal void SetEnabledInfoLog(bool enabled) + => setEnabledInfoLog(enabled); + #endregion + + #region Data required to fetch methods via reflection + private const string AssemblyName = "GameAnalytics.NetStandard"; + private const string Namespace = "GameAnalyticsSDK.Net"; + private const string MainClass = "GameAnalytics"; + private const string EnumPrefix = "EGA"; + #endregion + + #region Call implementations + private readonly object?[] args1 = new object?[1]; + private readonly object?[] args2 = new object?[2]; + private readonly object?[] args3 = new object?[3]; + private readonly object?[] args4 = new object?[4]; + + private Action Call(MethodInfo methodInfo) + => () => methodInfo?.Invoke(null, null); + + private Action Call(MethodInfo methodInfo) + => (T arg1) => + { + args1[0] = arg1; + methodInfo.Invoke(null, args1); + }; + + private Action Call(MethodInfo methodInfo) + => (T1 arg1, T2 arg2) => + { + args2[0] = arg1; + args2[1] = arg2; + methodInfo.Invoke(null, args2); + }; + + private Action Call(MethodInfo methodInfo) + => (T1 arg1, T2 arg2, T3 arg3) => + { + args3[0] = arg1; + args3[1] = arg2; + args3[2] = arg3; + methodInfo.Invoke(null, args3); + }; + + private Action Call(MethodInfo methodInfo) + => (T1 arg1, T2 arg2, T3 arg3, T4 arg4) => + { + args4[0] = arg1; + args4[1] = arg2; + args4[2] = arg3; + args4[3] = arg4; + methodInfo.Invoke(null, args4); + }; + #endregion + + private AssemblyLoadContext? loadContext; + private Assembly? assembly; + + private string GetAssemblyPath(string assemblyName) + => Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + $"{assemblyName}.dll"); + + private bool resolvingDependency; + private Assembly? ResolveDependency(AssemblyLoadContext context, AssemblyName dependencyName) + { + if (resolvingDependency) { return null; } + resolvingDependency = true; + Assembly dep = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null"))); + resolvingDependency = false; + return dep; + } + + internal Implementation() + { + loadContext = new AssemblyLoadContext(AssemblyName, isCollectible: true); + loadContext.Resolving += ResolveDependency; + assembly = loadContext.LoadFromAssemblyPath( + GetAssemblyPath(AssemblyName)); + + Type getType(string name) + => assembly.GetType($"{Namespace}.{name}") + ?? throw new Exception($"Could not find type\"{Namespace}.{name}\""); + + var mainClass = getType(MainClass); + var errorSeverityEnumType = getType($"{EnumPrefix}{nameof(ErrorSeverity)}"); + var progressionStatusEnumType = getType($"{EnumPrefix}{nameof(ProgressionStatus)}"); + + MethodInfo getMethod(string name, Type[] types) + { + return mainClass?.GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: types, modifiers: null) + ?? throw new Exception($"Could not find method \"{name}\" with types {string.Join(',', types.Select(t => t.Name))}"); + } + + initialize = Call(getMethod(nameof(Initialize), + new Type[] { typeof(string), typeof(string) })); + configureBuild = Call(getMethod(nameof(ConfigureBuild), + new Type[] { typeof(string) })); + addErrorEvent = Call(getMethod(nameof(AddErrorEvent), + new Type[] { errorSeverityEnumType, typeof(string) })); + addDesignEvent0 = Call?>(getMethod(nameof(AddDesignEvent), + new Type[] { typeof(string), typeof(IDictionary) })); + addDesignEvent1 = Call(getMethod(nameof(AddDesignEvent), + new Type[] { typeof(string), typeof(double) })); + addProgressionEvent01 = Call(getMethod(nameof(AddProgressionEvent), + new Type[] { progressionStatusEnumType, typeof(string) })); + addProgressionEvent01Score = Call(getMethod(nameof(AddProgressionEvent), + new Type[] { progressionStatusEnumType, typeof(string), typeof(double) })); + addProgressionEvent02 = Call(getMethod(nameof(AddProgressionEvent), + new Type[] { progressionStatusEnumType, typeof(string), typeof(string) })); + addProgressionEvent03 = Call(getMethod(nameof(AddProgressionEvent), + new Type[] { progressionStatusEnumType, typeof(string), typeof(string), typeof(string) })); + setCustomDimension01 = Call(getMethod(nameof(SetCustomDimension01), + new Type[] { typeof(string) })); + configureAvailableCustomDimensions01 = Call(getMethod(nameof(ConfigureAvailableCustomDimensions01), + new Type[] { typeof(string[]) })); + setEnabledInfoLog = Call(getMethod(nameof(SetEnabledInfoLog), + new Type[] { typeof(bool) })); + + onQuit = Call(getMethod("OnQuit", Array.Empty())); + } + + private readonly Action? onQuit; + private void OnQuit() + { + if (assembly != null) { onQuit?.Invoke(); } + } + + public void Dispose() + { + if (loadContext is null) { return; } + OnQuit(); + loadContext?.Unload(); + loadContext = null; + assembly = null; + } + + ~Implementation() + { + OnQuit(); + } + } + private static Implementation? loadedImplementation; + + public static void AddErrorEvent(ErrorSeverity errorSeverity, string message) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddErrorEvent(errorSeverity, message); + } + + /// + /// Adds an error event to GameAnalytics if an event with the same identifier has not been added yet. + /// + public static void AddErrorEventOnce(string identifier, ErrorSeverity errorSeverity, string message) + { + if (!SendUserStatistics) { return; } + if (sentEventIdentifiers.Contains(identifier)) { return; } + + if (GameMain.Config.AllEnabledPackages != null) + { + if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) + { + message = "[MODDED] " + message; + } + } + + loadedImplementation?.AddErrorEvent(errorSeverity, message); + sentEventIdentifiers.Add(identifier); + } + + public static void AddDesignEvent(string eventID) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddDesignEvent(eventID); + } + + public static void AddDesignEvent(string eventID, double value) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddDesignEvent(eventID, value); + } + + public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddProgressionEvent(progressionStatus, progression01); + } + + public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, double score) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, score); + } + + public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02); + } + + public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02, string progression03) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03); + } + + public static void SetCustomDimension01(string dimension) + { + if (!SendUserStatistics) { return; } + loadedImplementation?.SetCustomDimension01(dimension); + } + + private static void Init() + { + ShutDown(); + try + { + loadedImplementation = new Implementation(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e); + SetConsent(Consent.Error); + return; + } +#if DEBUG + try + { + loadedImplementation?.SetEnabledInfoLog(true); + } + catch (Exception e) + { + DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e); + SetConsent(Consent.Error); + return; + } +#endif + + string exePath = Assembly.GetEntryAssembly()!.Location; + string? exeName = string.Empty; +#if SERVER + exeName = "s"; +#endif + Md5Hash? exeHash = null; + try + { + using (var stream = File.OpenRead(exePath)) + { + exeHash = new Md5Hash(stream); + } + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while calculating MD5 hash for the executable \"" + exePath + "\"", e); + } + try + { + string buildConfiguration = "Release"; +#if DEBUG + buildConfiguration = "Debug"; +#elif UNSTABLE + buildConfiguration = "Unstable"; +#endif + loadedImplementation?.ConfigureBuild(GameMain.Version.ToString() + + exeName + ":" + + AssemblyInfo.GitRevision + ":" + + buildConfiguration); + loadedImplementation?.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor"); + + InitKeys(); + + loadedImplementation?.AddDesignEvent("Executable:" + + GameMain.Version.ToString() + + exeName + ":" + + ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":" + + AssemblyInfo.GitBranch + ":" + + AssemblyInfo.GitRevision + ":" + + buildConfiguration); + } + catch (Exception e) + { + DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e); + SetConsent(Consent.Error); + return; + } + + var allPackages = GameMain.Config?.AllEnabledPackages.ToList(); + if (allPackages?.Count > 0) + { + StringBuilder sb = new StringBuilder("ContentPackage: "); + int i = 0; + foreach (ContentPackage cp in allPackages) + { + string trimmedName = cp.Name.Replace(":", "").Replace(" ", ""); + sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length))); + if (i < allPackages.Count - 1) { sb.Append(" "); } + } + loadedImplementation?.AddDesignEvent(sb.ToString()); + } + } + + static partial void InitKeys(); + + public static void ShutDown() + { + loadedImplementation?.Dispose(); + loadedImplementation = null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs deleted file mode 100644 index 05096b9f7..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs +++ /dev/null @@ -1,132 +0,0 @@ -using GameAnalyticsSDK.Net; -using System; -using System.Text; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Reflection; -using System.Security.Cryptography; - -namespace Barotrauma -{ - public static class GameAnalyticsManager - { - private static HashSet sentEventIdentifiers = new HashSet(); - - public static void Init() - { -#if DEBUG - try - { - GameAnalytics.SetEnabledInfoLog(true); - } - catch (Exception e) - { - DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e); - GameSettings.SendUserStatistics = false; - return; - } -#endif - - string exePath = Assembly.GetEntryAssembly().Location; - string exeName = null; - Md5Hash exeHash = null; - exeName = Path.GetFileNameWithoutExtension(exePath).Replace(":", ""); - var md5 = MD5.Create(); - try - { - using (var stream = File.OpenRead(exePath)) - { - exeHash = new Md5Hash(stream); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Error while calculating MD5 hash for the executable \"" + exePath + "\"", e); - } - try - { - GameAnalytics.ConfigureBuild(GameMain.Version.ToString() - + (string.IsNullOrEmpty(exeName) ? "Unknown" : exeName) + ":" - + ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash)); - GameAnalytics.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor"); - - GameAnalytics.Initialize("a3a073c20982de7c15d21e840e149122", "9010ad9a671233b8d9610d76cec8c897d9ff3ba7"); - - GameAnalytics.AddDesignEvent("Executable:" - + (string.IsNullOrEmpty(exeName) ? "Unknown" : exeName) + ":" - + ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash)); - } - catch (Exception e) - { - DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e); - GameSettings.SendUserStatistics = false; - return; - } - - var allPackages = GameMain.Config?.AllEnabledPackages.ToList(); - if (allPackages?.Count > 0) - { - StringBuilder sb = new StringBuilder("ContentPackage: "); - int i = 0; - foreach (ContentPackage cp in allPackages) - { - string trimmedName = cp.Name.Replace(":", "").Replace(" ", ""); - sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length))); - if (i < allPackages.Count - 1) { sb.Append(" "); } - } - GameAnalytics.AddDesignEvent(sb.ToString()); - } - } - - /// - /// Adds an error event to GameAnalytics if an event with the same identifier has not been added yet. - /// - public static void AddErrorEventOnce(string identifier, EGAErrorSeverity errorSeverity, string message) - { - if (!GameSettings.SendUserStatistics) { return; } - if (sentEventIdentifiers.Contains(identifier)) { return; } - - if (GameMain.Config.AllEnabledPackages != null) - { - if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) - { - message = "[MODDED] " + message; - } - } - - GameAnalytics.AddErrorEvent(errorSeverity, message); - sentEventIdentifiers.Add(identifier); - } - - public static void AddDesignEvent(string eventID) - { - if (!GameSettings.SendUserStatistics) return; - GameAnalytics.AddDesignEvent(eventID); - } - - public static void AddDesignEvent(string eventID, double value) - { - if (!GameSettings.SendUserStatistics) return; - GameAnalytics.AddDesignEvent(eventID, value); - } - - public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01) - { - if (!GameSettings.SendUserStatistics) return; - GameAnalytics.AddProgressionEvent(progressionStatus, progression01); - } - - public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02) - { - if (!GameSettings.SendUserStatistics) return; - GameAnalytics.AddProgressionEvent(progressionStatus, progression01, progression02); - } - - public static void SetCustomDimension01(string dimension) - { - if (!GameSettings.SendUserStatistics) return; - GameAnalytics.SetCustomDimension01(dimension); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 8f0c37464..5d20749f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -168,9 +168,9 @@ namespace Barotrauma { if (itemPrefab == null) { - string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n"+Environment.StackTrace.CleanupStackTrace(); + string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } bool success = false; @@ -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/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index d55314017..1d7049542 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -246,7 +246,8 @@ namespace Barotrauma continue; } - Item containerItem = new Item(containerPrefab, position, wp.Submarine); + Vector2 containerPosition = GetCargoPos(cargoRoom, containerPrefab); + Item containerItem = new Item(containerPrefab, containerPosition, wp.Submarine); itemContainer = containerItem.GetComponent(); if (itemContainer == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index a1dfda365..ff3462958 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -11,10 +11,10 @@ namespace Barotrauma { internal struct CampaignSettings { - public static CampaignSettings Empty = new CampaignSettings(); + public static CampaignSettings Empty => new CampaignSettings(); // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down - public static CampaignSettings Unsure = Empty; + public static CampaignSettings Unsure => Empty; public bool RadiationEnabled { get; set; } public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); @@ -34,7 +34,7 @@ namespace Barotrauma { maxMissionCount = DefaultMaxMissionCount; RadiationEnabled = inc.ReadBoolean(); - MaxMissionCount = inc.ReadInt32(); + MaxMissionCount = inc.ReadRangedInteger(MinMissionCountLimit, MaxMissionCountLimit); } public CampaignSettings(XElement element) @@ -47,7 +47,7 @@ namespace Barotrauma public void Serialize(IWriteMessage msg) { msg.Write(RadiationEnabled); - msg.Write(MaxMissionCount); + msg.WriteRangedInteger(MaxMissionCount, MinMissionCountLimit, MaxMissionCountLimit); } public int GetAddedMissionCount() @@ -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) @@ -484,6 +484,10 @@ namespace Barotrauma /// private Submarine GetLeavingSub() { + if (Level.IsLoadedOutpost) + { + return Submarine.MainSub; + } //in single player, only the sub the controlled character is inside can transition between levels //in multiplayer, if there's subs at both ends of the level, only the one with more players inside can transition //TODO: ignore players who don't have the permission to trigger a transition between levels? @@ -493,11 +497,6 @@ namespace Barotrauma Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers); Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers); - if (Level.IsLoadedOutpost) - { - leavingSubAtStart ??= Submarine.MainSub; - leavingSubAtEnd ??= Submarine.MainSub; - } int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 : leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost)); int playersInSubAtEnd = leavingSubAtEnd == null || !leavingSubAtEnd.AtEndExit ? 0 : @@ -572,7 +571,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) { @@ -686,6 +685,14 @@ namespace Barotrauma int loops = CampaignMetadata.GetInt("campaign.endings", 0); CampaignMetadata.SetValue("campaign.endings", loops + 1); } + + GameAnalyticsManager.AddProgressionEvent( + GameAnalyticsManager.ProgressionStatus.Complete, + Name ?? "none"); + string eventId = "FinishCampaign:"; + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); + GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money); } protected virtual void EndCampaignProjSpecific() { } @@ -712,7 +719,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 +914,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/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 739f6f112..1191f30c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -119,12 +120,6 @@ namespace Barotrauma #endif } -#if SERVER - List availableSubs = new List(); - List sourceList = new List(); - sourceList.AddRange(SubmarineInfo.SavedSubmarines); -#endif - foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -165,14 +160,6 @@ namespace Barotrauma petsElement = subElement; break; #if SERVER - case "availablesubs": - foreach (XElement availableSub in subElement.Elements()) - { - string subName = availableSub.GetAttributeString("name", ""); - SubmarineInfo matchingSub = sourceList.Find(s => s.Name == subName); - if (matchingSub != null) { availableSubs.Add(matchingSub); } - } - break; case "savedexperiencepoints": foreach (XElement savedExp in subElement.Elements()) { @@ -188,14 +175,6 @@ namespace Barotrauma InitCampaignData(); #if SERVER - // Fallback if using a save with no available subs assigned, use vanilla submarines - if (availableSubs.Count == 0) - { - GameMain.NetLobbyScreen.CampaignSubmarines.AddRange(sourceList.FindAll(s => s.IsCampaignCompatible && s.IsVanillaSubmarine())); - } - - GameMain.NetLobbyScreen.CampaignSubmarines = availableSubs; - characterData.Clear(); string characterDataPath = GetCharacterDataSavePath(); if (!File.Exists(characterDataPath)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 9523e5299..f1c9f4762 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -283,7 +283,8 @@ namespace Barotrauma public void PurchaseSubmarine(SubmarineInfo newSubmarine) { - if (Campaign == null) return; + if (Campaign is null) { return; } + if (Campaign.Money < newSubmarine.Price) { return; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { Campaign.Money -= newSubmarine.Price; @@ -312,7 +313,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 +325,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 +352,8 @@ namespace Barotrauma return; } + Submarine.LockX = Submarine.LockY = false; + LevelData = levelData; Submarine.Unload(); @@ -404,6 +407,21 @@ namespace Barotrauma InitializeLevel(level); + GameAnalyticsManager.AddProgressionEvent( + GameAnalyticsManager.ProgressionStatus.Start, + GameMode?.Name ?? "none"); + + string eventId = "StartRound:GameMode:" + (GameMode?.Name ?? "none") + ":"; + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); + foreach (Mission mission in missions) + { + GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier); + } + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none")); + #if CLIENT if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } @@ -511,10 +529,6 @@ namespace Barotrauma mpCampaign.UpgradeManager.ApplyUpgrades(); mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine); } - if (GameMode is CampaignMode) - { - Submarine.WarmStartPower(); - } } GameMain.Config.RecentlyEncounteredCreatures.Clear(); @@ -676,6 +690,8 @@ namespace Barotrauma { IEnumerable crewCharacters = GetSessionCrewCharacters(); + int prevMoney = (GameMode as CampaignMode)?.Money ?? 0; + foreach (Mission mission in missions) { mission.End(); @@ -733,6 +749,32 @@ namespace Barotrauma missions.Clear(); IsRunning = false; + + bool success = false; +#if CLIENT + success = CrewManager.GetCharacters().Any(c => !c.IsDead); +#else + success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); +#endif + double roundDuration = Timing.TotalTime - RoundStartTime; + GameAnalyticsManager.AddProgressionEvent( + success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, + GameMode?.Name ?? "none", + roundDuration); + string eventId = "EndRound:GameMode:" + (GameMode?.Name ?? "none") + ":"; + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); + foreach (Mission mission in missions) + { + GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration); + } + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration); + if (GameMode is CampaignMode campaignMode) + { + GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney); + } #if CLIENT HintManager.OnRoundEnded(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index b8b37bcc3..28a6d9571 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -757,25 +757,6 @@ namespace Barotrauma public bool CampaignDisclaimerShown, EditorDisclaimerShown; - private static bool sendUserStatistics = true; - public static bool SendUserStatistics - { - get - { - return false; -/*#if DEBUG - return false; -#endif - return sendUserStatistics;*/ - } - set - { - sendUserStatistics = value; - GameMain.Config.SaveNewPlayerConfig(); - } - } - public static bool ShowUserStatisticsPrompt { get; set; } - public bool ShowLanguageSelectionPrompt { get; set; } public static bool ShowOffensiveServerPrompt { get; set; } @@ -858,7 +839,6 @@ namespace Barotrauma if (!fileFound) { ShowLanguageSelectionPrompt = true; - ShowUserStatisticsPrompt = true; SaveNewPlayerConfig(); } } @@ -870,9 +850,8 @@ namespace Barotrauma private bool LoadPlayerConfigInternal() { XDocument doc = XMLExtensions.LoadXml(PlayerSavePath); - if (doc == null || doc.Root == null) + if (doc?.Root == null) { - ShowUserStatisticsPrompt = true; ShowTutorialSkipWarning = true; return false; } @@ -989,12 +968,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(overrideMultiplayerSaveFolder)) { doc.Root.Add(new XAttribute("overridemultiplayersavefolder", overrideMultiplayerSaveFolder)); - } - - if (!ShowUserStatisticsPrompt) - { - doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics)); - } + } XElement gMode = doc.Root.Element("graphicsmode"); if (gMode == null) @@ -1194,7 +1168,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Saving game settings failed.", e); - GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); return false; } @@ -1211,7 +1185,6 @@ namespace Barotrauma Language = doc.Root.GetAttributeString("language", Language); } AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates); - sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics); QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true); MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 0aa9fc4fc..2aeb8ca59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -430,7 +430,7 @@ namespace Barotrauma if (index < 0 || index >= slots.Length) { string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index e2d433d8c..0258a3eee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -207,8 +207,14 @@ namespace Barotrauma.Items.Components if (!item.linkedTo.Contains(target.item)) { item.linkedTo.Add(target.item); } if (!target.item.linkedTo.Contains(item)) { target.item.linkedTo.Add(item); } - if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target); - if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this); + if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) + { + target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target); + } + if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) + { + item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this); + } DockingTarget = target; DockingTarget.DockingTarget = this; @@ -484,7 +490,7 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "DockingPort.CreateDoorBody:InvalidPosition", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); position = Vector2.Zero; } @@ -779,29 +785,25 @@ namespace Barotrauma.Items.Components if (IsHorizontal) { - if (hulls[0].WorldRect.X < hulls[1].WorldRect.X) + if (hulls[0].WorldRect.X > hulls[1].WorldRect.X) { - gap.linkedTo.Add(hulls[0]); - gap.linkedTo.Add(hulls[1]); - } - else - { - gap.linkedTo.Add(hulls[1]); - gap.linkedTo.Add(hulls[0]); + var temp = hulls[0]; + hulls[0] = hulls[1]; + hulls[1] = temp; } + gap.linkedTo.Add(hulls[0]); + gap.linkedTo.Add(hulls[1]); } else { - if (hulls[0].WorldRect.Y > hulls[1].WorldRect.Y) + if (hulls[0].WorldRect.Y < hulls[1].WorldRect.Y) { - gap.linkedTo.Add(hulls[0]); - gap.linkedTo.Add(hulls[1]); - } - else - { - gap.linkedTo.Add(hulls[1]); - gap.linkedTo.Add(hulls[0]); + var temp = hulls[0]; + hulls[0] = hulls[1]; + hulls[1] = temp; } + gap.linkedTo.Add(hulls[0]); + gap.linkedTo.Add(hulls[1]); } for (int i = 0; i < 2; i++) @@ -813,7 +815,7 @@ namespace Barotrauma.Items.Components if (IsHorizontal) { - if (item.WorldPosition.X < DockingTarget.item.WorldPosition.X) + if (doorGap.WorldPosition.X < gap.WorldPosition.X) { if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); } } @@ -831,7 +833,7 @@ namespace Barotrauma.Items.Components } else { - if (item.WorldPosition.Y > DockingTarget.item.WorldPosition.Y) + if (doorGap.WorldPosition.Y > gap.WorldPosition.Y) { if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); } } @@ -873,11 +875,17 @@ namespace Barotrauma.Items.Components if (myWayPoint != null && targetWayPoint != null) { myWayPoint.FindHull(); - myWayPoint.linkedTo.Remove(targetWayPoint); - myWayPoint.OnLinksChanged?.Invoke(myWayPoint); + if (myWayPoint.linkedTo.Contains(targetWayPoint)) + { + myWayPoint.linkedTo.Remove(targetWayPoint); + myWayPoint.OnLinksChanged?.Invoke(myWayPoint); + } targetWayPoint.FindHull(); - targetWayPoint.linkedTo.Remove(myWayPoint); - targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint); + if (targetWayPoint.linkedTo.Contains(myWayPoint)) + { + targetWayPoint.linkedTo.Remove(myWayPoint); + targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index bf6875202..2ff2e7d77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -204,9 +204,12 @@ namespace Barotrauma.Items.Components set; } - [Serialize(true, true, description: ""), Editable] + [Editable, Serialize(true, true, description: "", alwaysUseInstanceValues: true)] public bool UseBetweenOutpostModules { get; private set; } + [Editable, Serialize(false, false, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)] + public bool BotsShouldKeepOpen { get; private set; } + public Door(Item item, XElement element) : base(item, element) { @@ -291,7 +294,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { - if (item.Condition < RepairThreshold) { return true; } + if (item.Condition < RepairThreshold && item.GetComponent().HasRequiredItems(picker, addMessage: false)) { return true; } if (requiredItems.None()) { return false; } if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; } return base.Pick(picker); @@ -299,7 +302,7 @@ namespace Barotrauma.Items.Components public override bool OnPicked(Character picker) { - if (item.Condition < RepairThreshold) { return true; } + if (item.Condition < RepairThreshold && item.GetComponent().HasRequiredItems(picker, addMessage: false)) { return true; } if (!HasAccess(picker)) { ToggleState(ActionType.OnPicked, picker); @@ -339,6 +342,7 @@ namespace Barotrauma.Items.Components ToggleState(ActionType.OnUse, character); PickingTime = originalPickingTime; StopPicking(picker); + return true; } #if CLIENT else if (hasRequiredItems && character != null && character == Character.Controlled) @@ -545,7 +549,7 @@ namespace Barotrauma.Items.Components if (!itemPosErrorShown) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")"); - GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsManager.ErrorSeverity.Error, "Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")."); itemPosErrorShown = true; } @@ -568,8 +572,8 @@ namespace Barotrauma.Items.Components if (!characterPosErrorShown.Contains(c)) { if (GameSettings.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); } - GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")." + + GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsManager.ErrorSeverity.Error, + "Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + c.SimPosition + ")." + " Removed: " + c.Removed + " Remoteplayer: " + c.IsRemotePlayer); characterPosErrorShown.Add(c); @@ -598,8 +602,8 @@ namespace Barotrauma.Items.Components if (!MathUtils.IsValid(body.SimPosition)) { DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")"); - GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + body.SimPosition + ")." + + GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsManager.ErrorSeverity.Error, + "Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + body.SimPosition + ")." + " Removed: " + c.Removed + " Remoteplayer: " + c.IsRemotePlayer); return false; 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/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 27c566b83..bb67dea57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -310,12 +310,6 @@ namespace Barotrauma.Items.Components internal static class GrowthSideExtension { - // Enum.HasFlag() sucks - public static bool IsBitSet(this TileSide side, TileSide bit) - { - return ((int) side & (int) bit) != 0; - } - // K&R algorithm for counting how many bits are set in a bit field public static int Count(this TileSide side) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index e88c44e43..c03c26b72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components CollidesWith = Physics.CollisionCharacter, CollisionCategories = Physics.CollisionItemBlocking, Enabled = false, - UserData = "Holdable.Pusher" + UserData = this }; Pusher.FarseerBody.OnCollision += OnPusherCollision; Pusher.FarseerBody.FixedRotation = false; 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..f2561695e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -69,14 +69,28 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { //return if someone is already trying to pick the item - if (pickTimer > 0.0f) return false; - if (picker == null || picker.Inventory == null) return false; + if (pickTimer > 0.0f) { return false; } + if (picker == null || picker.Inventory == null) { return false; } if (PickingTime > 0.0f) { var abilityPickingTime = new AbilityValueItem(PickingTime, item.Prefab); picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime); + if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) + { + foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped]) + { + foreach (var heldItem in picker.HeldItems) + { + if (ri.MatchesItem(heldItem)) + { + abilityPickingTime.Value /= 1 + heldItem.Prefab.AddedPickingSpeedMultiplier; + } + } + } + } + if ((picker.PickingItem == null || picker.PickingItem == item) && PickingTime <= float.MaxValue) { #if SERVER @@ -142,7 +156,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..bee423e3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -32,6 +32,13 @@ namespace Barotrauma.Items.Components set { reload = Math.Max(value, 0.0f); } } + [Serialize(false, false, description: "Tells the AI to hold the trigger down when it uses this weapon")] + public bool HoldTrigger + { + get; + set; + } + [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] public int ProjectileCount { @@ -110,9 +117,7 @@ namespace Barotrauma.Items.Components if (ReloadTimer < 0.0f) { ReloadTimer = 0.0f; - // was this an optimization or related to something else? it cannot occur for charge-type weapons - //IsActive = false; - if (MaxChargeTime == 0.0f) + if (MaxChargeTime <= 0f) { IsActive = false; return; @@ -147,9 +152,10 @@ namespace Barotrauma.Items.Components private float GetSpread(Character user) { - float degreeOfFailure = 1.0f - DegreeOfSuccess(user); + float degreeOfFailure = MathHelper.Clamp(1.0f - DegreeOfSuccess(user), 0.0f, 1.0f); 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 +209,8 @@ namespace Barotrauma.Items.Components { lastProjectile?.Item.GetComponent()?.Snap(); } - float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier); + float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier); + projectile.Launcher = item; 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 ad9965f0b..85f129123 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -627,7 +627,7 @@ namespace Barotrauma.Items.Components { string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return 0.0f; } @@ -646,6 +646,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; } @@ -899,7 +903,7 @@ namespace Barotrauma.Items.Components { DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException); GameAnalyticsManager.AddErrorEventOnce("ItemComponent.Load:TargetInvocationException" + item.Name + element.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace.CleanupStackTrace()); } @@ -1004,7 +1008,7 @@ namespace Barotrauma.Items.Components { containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) { - targetItemCount = itemCount, + ItemCount = itemCount, Equip = equip, RemoveEmpty = removeEmpty, GetItemPriority = i => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 27b8542ff..a7559b690 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 (this.Item.GetComponent() != null) { return false; } + if (Inventory.TryPutItem(item, user)) { IsActive = true; @@ -575,7 +577,7 @@ namespace Barotrauma.Items.Components { DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); } contained.body.Submarine = item.Submarine; 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/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index c15a5a5b4..77323447c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -61,14 +61,14 @@ namespace Barotrauma.Items.Components public IEnumerable LimbPositions { get { return limbPositions; } } - [Editable, Serialize(false, false, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.")] + [Editable, Serialize(false, false, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)] public bool IsToggle { get; set; } - [Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.")] + [Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] public bool State { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index f8eb62c33..13ca3271a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components private float userDeconstructorSpeedMultiplier = 1.0f; - private const float TinkeringSpeedIncrease = 1.5f; + private const float TinkeringSpeedIncrease = 2.5f; private ItemContainer inputContainer, outputContainer; @@ -158,10 +158,21 @@ namespace Barotrauma.Items.Components // In multiplayer, the server handles the deconstruction into new items if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + float amountMultiplier = 1f; + if (user != null && !user.Removed) { - var abilityTargetItem = new AbilityItem(targetItem); + var abilityTargetItem = new AbilityDeconstructedItem(targetItem, user); user.CheckTalents(AbilityEffectType.OnItemDeconstructed, abilityTargetItem); + + foreach (Character character in Character.GetFriendlyCrew(user)) + { + character.CheckTalents(AbilityEffectType.OnItemDeconstructedByAlly, abilityTargetItem); + } + + var itemCreationMultiplier = new AbilityValueItem(amountMultiplier, targetItem.Prefab); + user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemCreationMultiplier); + amountMultiplier = (int)itemCreationMultiplier.Value; } if (targetItem.Prefab.RandomDeconstructionOutput) @@ -187,18 +198,18 @@ namespace Barotrauma.Items.Components foreach (DeconstructItem deconstructProduct in products) { - CreateDeconstructProduct(deconstructProduct, inputItems); + CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier); } } else { foreach (DeconstructItem deconstructProduct in validDeconstructItems) { - CreateDeconstructProduct(deconstructProduct, inputItems); + CreateDeconstructProduct(deconstructProduct, inputItems, amountMultiplier); } } - void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable inputItems) + void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable inputItems, float amountMultiplier) { float percentageHealth = targetItem.Condition / targetItem.MaxCondition; @@ -221,7 +232,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); @@ -247,27 +258,35 @@ namespace Barotrauma.Items.Components } } - int amount = 1; - if (user != null && !user.Removed) { - var itemsCreated = new AbilityValueItem(amount, targetItem.Prefab); - user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemsCreated); - amount = (int)itemsCreated.Value; - // used to spawn items directly into the deconstructor var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab); user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer); } + int amount = (int)amountMultiplier; + for (int i = 0; i < amount; i++) { 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); - if (containedItem?.Combine(spawnedItem, null) ?? false) + if (containedItem?.OwnInventory != null) + { + foreach (Item subItem in containedItem.ContainedItems.ToList()) + { + if (subItem.Combine(spawnedItem, null)) + { + break; + } + } + } + else if (containedItem?.Combine(spawnedItem, null) ?? false) { break; } @@ -283,7 +302,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); @@ -401,4 +426,15 @@ namespace Barotrauma.Items.Components inputContainer.Inventory.Locked = IsActive; } } + class AbilityDeconstructedItem : AbilityObject, IAbilityItem, IAbilityCharacter + { + public AbilityDeconstructedItem(Item item, Character character) + { + Item = item; + Character = character; + } + public Item Item { get; set; } + public Character Character { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index c7d83ec42..c29086343 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; @@ -32,7 +37,7 @@ namespace Barotrauma.Items.Components [Serialize(1.0f, true)] public float SkillRequirementMultiplier { get; set; } - private const float TinkeringSpeedIncrease = 1.5f; + private const float TinkeringSpeedIncrease = 2.5f; private enum FabricatorState { @@ -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); } } }); @@ -332,7 +376,7 @@ namespace Barotrauma.Items.Components int quality = 0; if (user?.Info != null) { - foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID)) + foreach (Character character in Character.GetFriendlyCrew(user)) { character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem); } @@ -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; }); @@ -383,13 +433,12 @@ namespace Barotrauma.Items.Components { float userSkill = user.GetSkillLevel(skill.Identifier); float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); - var addedSkillValue = new AbilityValueString(0f, skill.Identifier); + var addedSkillValue = new AbilityValueString(addedSkill, skill.Identifier); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); - addedSkill += addedSkillValue.Value; user.Info.IncreaseSkillLevel( skill.Identifier, - addedSkill); + addedSkillValue.Value); } } @@ -408,6 +457,7 @@ namespace Barotrauma.Items.Components CancelFabricating(); } + } private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) @@ -446,8 +496,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 +540,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 +557,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 +602,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 +637,7 @@ namespace Barotrauma.Items.Components } } }); + RefreshAvailableIngredients(); } public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index 39049b9bd..d89269827 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -147,16 +147,12 @@ namespace Barotrauma.Items.Components { case "water_data_in": //cheating a bit because water detectors don't actually send the water level - float waterAmount; - if (source.GetComponent() == null) + bool fromWaterDetector = source.GetComponent() != null; + hullData.ReceivedWaterAmount = null; + if (fromWaterDetector) { - waterAmount = Rand.Range(0.0f, 1.0f); + hullData.ReceivedWaterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); } - else - { - waterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); - } - hullData.ReceivedWaterAmount = waterAmount; foreach (var linked in sourceHull.linkedTo) { if (!(linked is Hull linkedHull)) { continue; } @@ -165,7 +161,11 @@ namespace Barotrauma.Items.Components linkedHullData = new HullData(); hullDatas.Add(linkedHull, linkedHullData); } - linkedHullData.ReceivedWaterAmount = waterAmount; + linkedHullData.ReceivedWaterAmount = null; + if (fromWaterDetector) + { + linkedHullData.ReceivedWaterAmount = Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); + } } break; case "oxygen_data_in": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 351e57ef4..9317f68b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components public bool HasPower => IsActive && Voltage >= MinVoltage; public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f; - private const float TinkeringSpeedIncrease = 1.5f; + private const float TinkeringSpeedIncrease = 4.0f; public Pump(Item item, XElement element) : base(item, element) @@ -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..91d3d3b5f 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,25 @@ 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; } + + [Editable, Serialize(true, true)] + public bool ExplosionDamagesOtherSubs + { + get; + set; + } + public Reactor(Item item, XElement element) : base(item, element) { @@ -199,8 +211,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 +235,7 @@ namespace Barotrauma.Items.Components } #if CLIENT - if(PowerOn && AvailableFuel < 1) + if (PowerOn && AvailableFuel < 1) { HintManager.OnReactorOutOfFuel(this); } @@ -236,15 +248,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 +272,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 +288,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 +299,8 @@ namespace Barotrauma.Items.Components if (!PowerOn) { - targetFissionRate = 0.0f; - targetTurbineOutput = 0.0f; + TargetFissionRate = 0.0f; + TargetTurbineOutput = 0.0f; } else if (autoTemp) { @@ -317,56 +329,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 +371,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 +394,7 @@ namespace Barotrauma.Items.Components { item.CreateServerEvent(this); } -#endif -#if CLIENT +#elif CLIENT if (GameMain.Client != null) { item.CreateClientEvent(this); @@ -424,12 +410,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 +418,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 +435,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 +447,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 +494,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 +537,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; @@ -571,6 +551,20 @@ namespace Barotrauma.Items.Components if (item.Condition <= 0.0f) { return; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (!ExplosionDamagesOtherSubs && (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false)) + { + foreach (var statusEffect in statusEffectLists[ActionType.OnBroken]) + { + foreach (Explosion explosion in statusEffect.Explosions) + { + foreach (Submarine sub in Submarine.Loaded) + { + if (sub != item.Submarine) { explosion.IgnoredSubmarines.Add(sub); } + } + } + } + } + item.Condition = 0.0f; fireTimer = 0.0f; meltDownTimer = 0.0f; @@ -583,7 +577,6 @@ namespace Barotrauma.Items.Components containedItem.Condition = 0.0f; } } - #if SERVER GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction); if (GameMain.Server != null) @@ -628,7 +621,7 @@ namespace Barotrauma.Items.Components var container = item.GetComponent(); if (objective.SubObjectives.None()) { - var containObjective = AIContainItems(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true); + var containObjective = AIContainItems(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true); containObjective.Completed += ReportFuelRodCount; containObjective.Abandoned += ReportFuelRodCount; character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); @@ -696,15 +689,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 +723,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 +760,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..9b1c13af8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -61,6 +61,13 @@ namespace Barotrauma.Items.Components public List IgnoredBodies; + /// + /// The item that launched this projectile (if any) + /// + public Item Launcher; + + private Character stickTargetCharacter; + private Character _user; public Character User { @@ -322,6 +329,7 @@ namespace Barotrauma.Items.Components item.body.SetTransform(item.body.SimPosition, launchAngle); float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); + System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity); } } User = character; @@ -345,7 +353,7 @@ namespace Barotrauma.Items.Components launchPos = item.SimPosition; item.body.Enabled = true; - item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.9f); + item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); item.body.FarseerBody.OnCollision += OnProjectileCollision; item.body.FarseerBody.IsBullet = true; @@ -521,11 +529,13 @@ namespace Barotrauma.Items.Components if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; } - - //ignore everything else than characters, sub walls and level walls - if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) && - !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && - !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; } + if (!(fixture.Body.UserData is Holdable holdable && holdable.CanPush)) + { + //ignore everything else than characters, sub walls and level walls + if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) && + !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && + !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; } + } //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub if (submarine != null) @@ -564,7 +574,7 @@ namespace Barotrauma.Items.Components hits.Add(new HitscanResult(fixture, point, normal, fraction)); return 1; - }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel); + }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking); return hits; } @@ -614,8 +624,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(); } @@ -752,7 +762,7 @@ namespace Barotrauma.Items.Components if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); } if (limb.character != null) { character = limb.character; } } - else if (target.Body.UserData is Item targetItem) + else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item) is Item targetItem) { if (targetItem.Removed) { return false; } if (Attack != null && targetItem.Prefab.DamagedByProjectiles && targetItem.Condition > 0) @@ -866,7 +876,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 +975,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 +994,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..090028dc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -26,14 +26,18 @@ namespace Barotrauma.Items.Components RepairToolStructureRepairMultiplier, RepairToolStructureDamageMultiplier, RepairToolDeattachTimeMultiplier, + FirepowerMultiplier, + StrikingPowerMultiplier, + StrikingSpeedMultiplier, + FiringRateMultiplier, // unused as of now AttackMultiplier, + // unused as of now AttackSpeedMultiplier, ForceDoorsOpenSpeedMultiplier, RangedSpreadReduction, ChargeSpeedMultiplier, MovementSpeedMultiplier, - // generic stats to be used for various needs, declared just in case (localization) EffectivenessMultiplier, PowerOutputMultiplier, ConsumptionReductionMultiplier, @@ -43,7 +47,7 @@ namespace Barotrauma.Items.Components private int qualityLevel; - [Serialize(0, true)] + [Editable, Serialize(0, true)] public int QualityLevel { get { return qualityLevel; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 38331c971..0952af1c7 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; } @@ -199,7 +206,7 @@ namespace Barotrauma.Items.Components public float RepairDegreeOfSuccess(Character character, List skills) { - if (skills.Count == 0) return 1.0f; + if (skills.Count == 0) { return 1.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); float average = skillSum / skills.Count; @@ -207,6 +214,21 @@ namespace Barotrauma.Items.Components return ((average + 100.0f) / 2.0f) / 100.0f; } + public void RepairBoost(bool qteSuccess) + { + if (qteSuccess) + { + item.Condition += RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); + } + else if (Rand.Range(0.0f, 2.0f) > RepairDegreeOfSuccess(CurrentFixer, requiredSkills)) + { + ApplyStatusEffects(ActionType.OnFailure, 1.0f, CurrentFixer); +#if SERVER + GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, CurrentFixer.ID }); +#endif + } + } + public bool StartRepairing(Character character, FixActions action) { if (character == null || character.IsDead || action == FixActions.None) @@ -291,6 +313,8 @@ namespace Barotrauma.Items.Components currentRepairItem = null; currentFixerAction = FixActions.None; #if CLIENT + qteTimer = QteDuration; + qteCooldown = 0.0f; repairSoundChannel?.FadeOutAndDispose(); repairSoundChannel = null; #endif @@ -393,7 +417,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; } @@ -437,6 +461,7 @@ namespace Barotrauma.Items.Components SteamAchievementManager.OnItemRepaired(item, CurrentFixer); CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete); } + if (CurrentFixer?.SelectedConstruction == item) { CurrentFixer.SelectedConstruction = null; } deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); wasBroken = false; StopRepairing(CurrentFixer); @@ -524,7 +549,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/ColorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs index e272750a3..d3d41c271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs @@ -36,9 +36,9 @@ namespace Barotrauma.Items.Components if (UseHSV) { Color hsvColor = ToolBox.HSVToRGB(signalR, signalG, signalB); - signalR = hsvColor.R / (float) byte.MaxValue; - signalG = hsvColor.G / (float) byte.MaxValue; - signalB = hsvColor.B / (float) byte.MaxValue; + signalR = hsvColor.R; + signalG = hsvColor.G; + signalB = hsvColor.B; } output = signalR.ToString("G", CultureInfo.InvariantCulture); 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/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index 5d2a76a86..8d464e70a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -36,9 +36,17 @@ namespace Barotrauma.Items.Components { case FunctionType.Round: value = MathF.Round(value); + if (value == -0) + { + value = 0; + } break; case FunctionType.Ceil: value = MathF.Ceiling(value); + if (value == -0) + { + value = 0; + } break; case FunctionType.Floor: value = MathF.Floor(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..22fd92e52 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,41 @@ 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": + case "signal_in": + if (signal.value.Length > MaxMessageLength) + { + signal.value = signal.value.Substring(0, MaxMessageLength); + } - string inputSignal = signal.value.Replace("\\n", "\n"); - ShowOnDisplay(inputSignal, addToHistory: true); + 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(); + } + + CreateFillerBlock(); +#endif + break; + } } public override void OnItemLoaded() @@ -83,7 +145,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 +160,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 +172,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/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 763800065..629d07c37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components //item in water -> we definitely want to send the True output isInWater = true; } - else if (item.CurrentHull != null && item.CurrentHull.WaterPercentage > 0.0f) + else if (item.CurrentHull != null && item.CurrentHull.WaterPercentage > 0.0f && item.CurrentHull.WaterVolume > 1.0f) { //(center of the) item in not water -> check if the water surface is below the bottom of the item's rect if (item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height) @@ -100,7 +100,12 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null) { - int waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); + int waterPercentage = 0; + //ignore minuscule amounts of water + if (item.CurrentHull.WaterVolume > 1.0f) + { + waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); + } item.SendSignal(waterPercentage.ToString(), "water_%"); } string highPressureOut = (item.CurrentHull == null || item.CurrentHull.LethalPressure > 5.0f) ? "1" : "0"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index e768c3faa..4f5c1366f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -128,6 +128,9 @@ namespace Barotrauma.Items.Components return HasRequiredContainedItems(user: null, addMessage: false); } + /// + /// Returns the wifi components that can receive signals from this one + /// public IEnumerable GetReceiversInRange() { return list.Where(w => w != this && w.CanReceive(this)); @@ -136,10 +139,16 @@ namespace Barotrauma.Items.Components public bool CanReceive(WifiComponent sender) { if (sender == null || sender.channel != channel) { return false; } + if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } - if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) + //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) { - return false; + 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; } @@ -147,6 +156,21 @@ namespace Barotrauma.Items.Components return HasRequiredContainedItems(user: null, addMessage: false); } + /// + /// Returns the wifi components that can transmit signals to this one + /// + public IEnumerable GetTransmittersInRange() + { + return list.Where(w => w != this && w.CanTransmit(this)); + } + + public bool CanTransmit(WifiComponent sender) + { + if (sender == null || sender.channel != channel) { return false; } + if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } + if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; } + return HasRequiredContainedItems(user: null, addMessage: false); + } public override void Update(float deltaTime, Camera cam) { chatMsgCooldown -= deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 2b80a81c8..a3dae763f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -756,14 +756,17 @@ namespace Barotrauma.Items.Components { if (item.ParentInventory != null) { return; } #if CLIENT - if (!relativeToSub && Screen.Selected != GameMain.SubEditorScreen) { return; } + if (!relativeToSub) + { + if (Screen.Selected != GameMain.SubEditorScreen || (item.Submarine?.Loading ?? false)) { return; } + } #else if (!relativeToSub) { return; } #endif Vector2 refPos = item.Submarine == null ? Vector2.Zero : - item.Position - item.Submarine.HiddenSubPosition; + item.Position - item.Submarine.HiddenSubPosition; for (int i = 0; i < nodes.Count; i++) { 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..fdaca5d18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -64,10 +64,15 @@ namespace Barotrauma.Items.Components private Character currentTarget; const float aiFindTargetInterval = 5.0f; + private int currentLoaderIndex; + private const float TinkeringPowerCostReduction = 0.2f; 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 +367,7 @@ namespace Barotrauma.Items.Components UpdateTransformedBarrelPos(); } - if (user != null && user.Removed) + if (user is { Removed: true }) { user = null; } @@ -371,6 +376,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); @@ -576,16 +594,17 @@ namespace Barotrauma.Items.Components } else { - foreach (MapEntity e in item.linkedTo) + for (int j = 0; j < item.linkedTo.Count; j++) { + var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count]; //use linked projectile containers in case they have to react to the turret being launched somehow //(play a sound, spawn more projectiles) if (!(e is Item linkedItem)) { continue; } if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } - if (linkedItem.Condition <= 0.0f) + if (linkedItem.Condition <= 0.0f) { loaderBroken = true; - continue; + continue; } ItemContainer projectileContainer = linkedItem.GetComponent(); if (projectileContainer != null) @@ -594,7 +613,6 @@ namespace Barotrauma.Items.Components projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { break; } } - } } if (projectiles.Count == 0 && !LaunchWithoutProjectile) @@ -689,6 +707,10 @@ namespace Barotrauma.Items.Components { ShiftItemsInProjectileContainer(container.GetComponent()); } + if (item.linkedTo.Count > 0) + { + currentLoaderIndex = (currentLoaderIndex + 1) % item.linkedTo.Count; + } } } @@ -743,9 +765,12 @@ namespace Barotrauma.Items.Components Projectile projectileComponent = projectile.GetComponent(); if (projectileComponent != null) { + projectileComponent.Launcher = item; 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 +981,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 +1012,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 +1027,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 +1069,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 +1081,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 +1097,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 +1128,8 @@ namespace Barotrauma.Items.Components float closestDistance = maxDistance * maxDistance; - if (currentTarget != null) + bool hadCurrentTarget = currentTarget != null; + if (hadCurrentTarget) { if (currentTarget.Removed || currentTarget.IsDead) { @@ -1131,7 +1170,7 @@ namespace Barotrauma.Items.Components { targetPos = closestEnemy.WorldPosition; //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is - if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine) + if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item)) { targetPos = closestEnemy.CurrentHull.WorldPosition; } @@ -1222,24 +1261,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 +1294,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 +1338,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 +1440,7 @@ namespace Barotrauma.Items.Components crosshairSprite?.Remove(); crosshairSprite = null; crosshairPointerSprite?.Remove(); crosshairPointerSprite = null; moveSoundChannel?.Dispose(); moveSoundChannel = null; + WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite = null; #endif } @@ -1397,8 +1449,9 @@ namespace Barotrauma.Items.Components List projectiles = new List(); // check the item itself first CheckProjectileContainer(item, projectiles, out bool _); - foreach (MapEntity e in item.linkedTo) + for (int j = 0; j < item.linkedTo.Count; j++) { + var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count]; if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } if (e is Item projectileContainer) { @@ -1406,7 +1459,6 @@ namespace Barotrauma.Items.Components if (projectiles.Any() || stopSearching) { return projectiles; } } } - return projectiles; } @@ -1503,12 +1555,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 +1577,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 2b1b67724..f1806e40a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -480,7 +480,7 @@ namespace Barotrauma if (i < 0 || i >= slots.Length) { string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } @@ -531,7 +531,7 @@ namespace Barotrauma if (i < 0 || i >= slots.Length) { string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -812,13 +812,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 04a2b68b9..43cecd07b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -27,7 +27,7 @@ namespace Barotrauma public static bool ShowLinks = true; - private HashSet tags; + private readonly HashSet tags; private bool isWire, isLogic; @@ -41,6 +41,10 @@ namespace Barotrauma } } + public float HullOxygenPercentage + { + get { return CurrentHull?.OxygenPercentage ?? 0.0f; } + } private CampaignMode.InteractionType campaignInteractionType = CampaignMode.InteractionType.None; public CampaignMode.InteractionType CampaignInteractionType @@ -65,13 +69,13 @@ namespace Barotrauma #endif //components that determine the functionality of the item - private Dictionary componentsByType = new Dictionary(); - private List components; + private readonly Dictionary componentsByType = new Dictionary(); + private readonly List components; /// /// Components that are Active or need to be updated for some other reason (status effects, sounds) /// private readonly List updateableComponents = new List(); - private List drawableComponents; + private readonly List drawableComponents; private bool hasComponentsToDraw; public PhysicsBody body; @@ -100,7 +104,7 @@ namespace Barotrauma private readonly List repairables; - private Quality qualityComponent; + private readonly Quality qualityComponent; private readonly Queue impactQueue = new Queue(); @@ -359,7 +363,6 @@ namespace Barotrauma protected set; } - [Serialize("", false)] /// /// Can be used by status effects or conditionals to check what item this item is contained inside /// @@ -372,7 +375,6 @@ namespace Barotrauma ParentInventory?.Owner?.ToString() ?? ""; } - set { /*do nothing*/ } } @@ -393,7 +395,6 @@ namespace Barotrauma } } - [Serialize(false, false)] /// /// Can be used by status effects or conditionals to check if the physics body of the item is active /// @@ -403,7 +404,6 @@ namespace Barotrauma { return body != null && body.Enabled; } - set { /*do nothing*/ } } [Serialize(0.0f, false)] @@ -474,7 +474,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; @@ -536,9 +541,19 @@ namespace Barotrauma isActive = true; } } + + LastConditionChange = condition - prev; + ConditionLastUpdated = Timing.TotalTime; } } + private double ConditionLastUpdated { get; set; } + private float LastConditionChange { get; set; } + /// + /// Return true if the condition of this item increased within the last second. + /// + public bool ConditionIncreasedRecently => (Timing.TotalTime < ConditionLastUpdated + 1.0f) && LastConditionChange > 0.0f; + public float Health { get { return condition; } @@ -565,21 +580,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)] @@ -589,9 +609,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; } } } @@ -989,7 +1009,8 @@ namespace Barotrauma partial void InitProjSpecific(); - public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary) => Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary); + public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRestriction = false) + => Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary, requireConditionRestriction); public override MapEntity Clone() { @@ -1009,7 +1030,7 @@ namespace Barotrauma errorMsg += "Original components: " + string.Join(", ", components.Select(c => c.GetType().ToString())); errorMsg += ", cloned components: " + string.Join(", ", clone.components.Select(c => c.GetType().ToString())); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } for (int i = 0; i < components.Count && i < clone.components.Count; i++) @@ -1181,7 +1202,7 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "Item.SetPosition:InvalidPosition" + ID, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -1739,10 +1760,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 { @@ -2007,7 +2028,7 @@ namespace Barotrauma var wifiComponent = recipient.Item.GetComponent(); if (wifiComponent != null && wifiComponent.CanTransmit()) { - foreach (var wifiReceiver in wifiComponent.GetReceiversInRange()) + foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange()) { var receiverConnections = wifiReceiver.Item.Connections; if (receiverConnections == null) { continue; } @@ -2128,7 +2149,7 @@ namespace Barotrauma } - private IEnumerable DelaySignal(Signal signal, Connection connection) + private IEnumerable DelaySignal(Signal signal, Connection connection) { do { @@ -2164,7 +2185,7 @@ namespace Barotrauma return c != null && c.Character != null && c.Character.CanInteractWith(this); } - public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false) + public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false) { if (CampaignInteractionType != CampaignMode.InteractionType.None) { return false; } @@ -2174,12 +2195,11 @@ namespace Barotrauma Skill requiredSkill = null; float skillMultiplier = 1; #endif - if (!IsInteractable(picker)) { return false; } + if (!IsInteractable(user)) { return false; } foreach (ItemComponent ic in components) { bool pickHit = false, selectHit = false; - - if (picker.IsKeyDown(InputType.Aim)) + if (user.IsKeyDown(InputType.Aim)) { pickHit = false; selectHit = false; @@ -2188,26 +2208,44 @@ namespace Barotrauma { if (forceSelectKey) { - if (ic.PickKey == InputType.Select) pickHit = true; - if (ic.SelectKey == InputType.Select) selectHit = true; + if (ic.PickKey == InputType.Select) + { + pickHit = true; + } + if (ic.SelectKey == InputType.Select) + { + selectHit = true; + } } - else if (forceActionKey) + else if (forceUseKey) { - if (ic.PickKey == InputType.Use) pickHit = true; - if (ic.SelectKey == InputType.Use) selectHit = true; + if (ic.PickKey == InputType.Use) + { + pickHit = true; + } + if (ic.SelectKey == InputType.Use) + { + selectHit = true; + } } else { - pickHit = picker.IsKeyHit(ic.PickKey); - selectHit = picker.IsKeyHit(ic.SelectKey); + pickHit = user.IsKeyHit(ic.PickKey); + selectHit = user.IsKeyHit(ic.SelectKey); #if CLIENT //if the cursor is on a UI component, disable interaction with the left mouse button //to prevent accidentally selecting items when clicking UI elements - if (picker == Character.Controlled && GUI.MouseOn != null) + if (user == Character.Controlled && GUI.MouseOn != null) { - if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0) pickHit = false; - if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0) selectHit = false; + if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0) + { + pickHit = false; + } + if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0) + { + selectHit = false; + } } #endif } @@ -2218,50 +2256,50 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode) { pickHit = selectHit = GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None ? - picker.IsKeyHit(InputType.Use) : - picker.IsKeyHit(InputType.Select); + user.IsKeyHit(InputType.Use) : + user.IsKeyHit(InputType.Select); } #endif if (!pickHit && !selectHit) { continue; } bool showUiMsg = false; #if CLIENT - if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); } - showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; + if (!ic.HasRequiredSkills(user, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); } + showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; #endif - if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) { continue; } - if ((ic.CanBePicked && pickHit && ic.Pick(picker)) || - (ic.CanBeSelected && selectHit && ic.Select(picker))) + if (!ignoreRequiredItems && !ic.HasRequiredItems(user, showUiMsg)) { continue; } + if ((ic.CanBePicked && pickHit && ic.Pick(user)) || + (ic.CanBeSelected && selectHit && ic.Select(user))) { picked = true; - ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); + ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, user); #if CLIENT - if (picker == Character.Controlled) { GUI.ForceMouseOn(null); } + if (user == Character.Controlled) { GUI.ForceMouseOn(null); } if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (ic.CanBeSelected) { selected = true; } + if (ic.CanBeSelected && !(ic is Door)) { selected = true; } } } if (!picked) { return false; } - if (picker != null) + if (user != null) { - if (picker.SelectedConstruction == this) + if (user.SelectedConstruction == this) { - if (picker.IsKeyHit(InputType.Select) || forceSelectKey) + if (user.IsKeyHit(InputType.Select) || forceSelectKey) { - picker.SelectedConstruction = null; + user.SelectedConstruction = null; } } else if (selected) { - picker.SelectedConstruction = this; + user.SelectedConstruction = this; } } #if CLIENT - if (!hasRequiredSkills && Character.Controlled == picker && Screen.Selected != GameMain.SubEditorScreen) + if (!hasRequiredSkills && Character.Controlled == user && Screen.Selected != GameMain.SubEditorScreen) { if (requiredSkill != null) { @@ -2271,7 +2309,10 @@ namespace Barotrauma } #endif - if (Container != null) Container.RemoveContained(this); + if (Container != null) + { + Container.RemoveContained(this); + } return true; } @@ -2545,7 +2586,9 @@ namespace Barotrauma private void WritePropertyChange(IWriteMessage msg, object[] extraData, bool inGameEditableOnly) { - var allProperties = inGameEditableOnly ? GetInGameEditableProperties() : GetProperties(); + //ignoreConditions: true = include all ConditionallyEditable properties at this point, + //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients + var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties(); SerializableProperty property = extraData[1] as SerializableProperty; if (property != null) { @@ -2556,21 +2599,21 @@ namespace Barotrauma } object value = property.GetValue(propertyOwner.First); - if (value is string) + if (value is string stringVal) { - msg.Write((string)value); + msg.Write(stringVal); } - else if (value is float) + else if (value is float floatVal) { - msg.Write((float)value); + msg.Write(floatVal); } - else if (value is int) + else if (value is int intVal) { - msg.Write((int)value); + msg.Write(intVal); } - else if (value is bool) + else if (value is bool boolVal) { - msg.Write((bool)value); + msg.Write(boolVal); } else if (value is Color color) { @@ -2579,35 +2622,35 @@ namespace Barotrauma msg.Write(color.B); msg.Write(color.A); } - else if (value is Vector2) + else if (value is Vector2 vector2) { - msg.Write(((Vector2)value).X); - msg.Write(((Vector2)value).Y); + msg.Write(vector2.X); + msg.Write(vector2.Y); } - else if (value is Vector3) + else if (value is Vector3 vector3) { - msg.Write(((Vector3)value).X); - msg.Write(((Vector3)value).Y); - msg.Write(((Vector3)value).Z); + msg.Write(vector3.X); + msg.Write(vector3.Y); + msg.Write(vector3.Z); } - else if (value is Vector4) + else if (value is Vector4 vector4) { - msg.Write(((Vector4)value).X); - msg.Write(((Vector4)value).Y); - msg.Write(((Vector4)value).Z); - msg.Write(((Vector4)value).W); + msg.Write(vector4.X); + msg.Write(vector4.Y); + msg.Write(vector4.Z); + msg.Write(vector4.W); } - else if (value is Point) + else if (value is Point point) { - msg.Write(((Point)value).X); - msg.Write(((Point)value).Y); + msg.Write(point.X); + msg.Write(point.Y); } - else if (value is Rectangle) + else if (value is Rectangle rect) { - msg.Write(((Rectangle)value).X); - msg.Write(((Rectangle)value).Y); - msg.Write(((Rectangle)value).Width); - msg.Write(((Rectangle)value).Height); + msg.Write(rect.X); + msg.Write(rect.Y); + msg.Write(rect.Width); + msg.Write(rect.Height); } else if (value is Enum) { @@ -2632,16 +2675,25 @@ namespace Barotrauma } } - private List> GetInGameEditableProperties() + private List> GetInGameEditableProperties(bool ignoreConditions = false) { - return GetProperties() - .Where(ce => ce.Second.GetAttribute().IsEditable(this)) - .Union(GetProperties()).ToList(); + if (ignoreConditions) + { + return GetProperties().Union(GetProperties()).ToList(); + } + else + { + return GetProperties() + .Where(ce => ce.Second.GetAttribute().IsEditable(this)) + .Union(GetProperties()).ToList(); + } } private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Client sender = null) { - var allProperties = inGameEditableOnly ? GetInGameEditableProperties() : GetProperties(); + //ignoreConditions: true = include all ConditionallyEditable properties at this point, + //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients + var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties(); if (allProperties.Count == 0) { return; } int propertyIndex = 0; @@ -2658,9 +2710,12 @@ namespace Barotrauma if (!ic.AllowInGameEditing) { allowEditing = false; } } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !CanClientAccess(sender)) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - allowEditing = false; + if (!CanClientAccess(sender) || !(property.GetAttribute()?.IsEditable(this) ?? true)) + { + allowEditing = false; + } } Type type = property.PropertyType; @@ -2759,7 +2814,7 @@ namespace Barotrauma #endif GameAnalyticsManager.AddErrorEventOnce( "Item.ReadPropertyChange:" + Name + ":" + type, - GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, + GameAnalyticsManager.ErrorSeverity.Warning, "Failed to convert the int value \"" + intVal + "\" to " + type + " (item " + Name + ")"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index cf1870762..7df7912f9 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; @@ -135,7 +141,7 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "ItemInventory.CreateServerEvent:EventForUninitializedItem" + container.Item.Name + container.Item.ID, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 8b501cd71..dfec46927 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); @@ -137,7 +138,7 @@ namespace Barotrauma float maxCondition = subElement.GetAttributeFloat("maxcondition", 1.0f); //Substract mincondition from required item's condition or delete it regardless? bool useCondition = subElement.GetAttributeBool("usecondition", true); - int count = subElement.GetAttributeInt("count", 1); + int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1)); if (!string.IsNullOrEmpty(requiredItemIdentifier)) { @@ -152,11 +153,11 @@ namespace Barotrauma MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing == null) { - RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, maxCondition, useCondition)); + RequiredItems.Add(new RequiredItem(requiredItem, amount, minCondition, maxCondition, useCondition)); } else { - existing.Amount += count; + existing.Amount += amount; } } else @@ -174,11 +175,11 @@ namespace Barotrauma MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing == null) { - RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, maxCondition, useCondition)); + RequiredItems.Add(new RequiredItem(matchingItems, amount, minCondition, maxCondition, useCondition)); } else { - existing.Amount += count; + existing.Amount += amount; } } break; @@ -548,6 +549,13 @@ namespace Barotrauma private set; } + [Serialize(0.0f, false)] + public float AddedPickingSpeedMultiplier + { + get; + private set; + } + [Serialize(false, false)] public bool CannotRepairFail { @@ -915,7 +923,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 +980,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 +991,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 +1001,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 +1011,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 +1022,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 +1032,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 +1042,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 +1052,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 +1078,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) @@ -1297,21 +1305,24 @@ namespace Barotrauma public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) { Dictionary priceLocations = new Dictionary(); - foreach (KeyValuePair locationPrice in locationPrices) + if (locationPrices != null) { - PriceInfo priceInfo = locationPrice.Value; + foreach (KeyValuePair locationPrice in locationPrices) + { + PriceInfo priceInfo = locationPrice.Value; - if (priceInfo == null) - { - continue; - } - if (!priceInfo.CanBeBought) - { - continue; - } - if (priceInfo.Price < maxCost || maxCost == 0) - { - priceLocations.Add(locationPrice.Key, priceInfo); + if (priceInfo == null) + { + continue; + } + if (!priceInfo.CanBeBought) + { + continue; + } + if (priceInfo.Price < maxCost || maxCost == 0) + { + priceLocations.Add(locationPrice.Key, priceInfo); + } } } return priceLocations.ToImmutableDictionary(); @@ -1342,17 +1353,18 @@ namespace Barotrauma return priceLocations.ToImmutableDictionary(); } - public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary) + public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; if (!isPreferencesDefined) { return true; } - if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer))) + if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer))) { return true; } isSecondary = true; - return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer)); + return PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer)); + static bool HasConditionRequirement(PreferredContainer pc) => pc.MinCondition > 0 || pc.MaxCondition < 100; } public bool IsContainerPreferred(Item item, string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 47525f812..4c356b657 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(), - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace.CleanupStackTrace()); + $"Entity.RemoveAll:Exception{e}", + GameAnalyticsManager.ErrorSeverity.Error, + $"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(); @@ -201,7 +223,7 @@ namespace Barotrauma { DebugConsole.ThrowError(errorLine); } - GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg.ToString()); + GameAnalyticsManager.AddErrorEventOnce("Entity.RemoveAll", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.ToString()); } dictionary.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, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace.CleanupStackTrace()); + $"Entity.FreeID:EntityNotFound{ID}", + GameAnalyticsManager.ErrorSeverity.Error, + $"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); - } + GameAnalyticsManager.ErrorSeverity.Error, + $"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 5d9dc228d..348bc996d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -27,25 +27,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 @@ -55,11 +55,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); } } @@ -320,7 +320,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 { @@ -376,7 +376,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; @@ -406,7 +406,7 @@ namespace Barotrauma { hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2); } - + flowForce = new Vector2(delta, 0.0f); } @@ -449,7 +449,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; @@ -457,7 +457,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 @@ -495,7 +495,7 @@ namespace Barotrauma hull1.LethalPressure = avgLethality; hull2.LethalPressure = avgLethality; } - else + else { hull1.LethalPressure = 0.0f; hull2.LethalPressure = 0.0f; @@ -512,7 +512,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; @@ -527,7 +527,7 @@ namespace Barotrauma if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { flowForce = new Vector2(-delta, 0.0f); - + } else { @@ -555,7 +555,7 @@ namespace Barotrauma hull1.WaveVel[0] += vel; hull1.WaveVel[1] += vel; - } + } } else { @@ -657,12 +657,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) @@ -730,7 +730,7 @@ namespace Barotrauma { if (!DisableHullRechecks) FindHulls(); } - + public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = Rectangle.Empty; @@ -761,6 +761,8 @@ namespace Barotrauma { linkedToID = new List(), }; + + g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame).ToLower(), g.HiddenInGame); return g; } @@ -770,7 +772,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..6993379c2 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; + } } } @@ -1565,7 +1575,7 @@ namespace Barotrauma { string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return null; } 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/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 3eb478afb..8d1c11c4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -440,7 +440,7 @@ namespace Barotrauma DebugConsole.ThrowError("Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + ")"); GameAnalyticsManager.AddErrorEventOnce( "CaveGenerator.GeneratePolygons:InvalidTriangle", - GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, + GameAnalyticsManager.ErrorSeverity.Warning, "Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + "). Seed: " + level.Seed); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 698b5fbde..94b486cc2 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); } @@ -953,6 +954,13 @@ namespace Barotrauma { for (int i = 0; i < cavePathCells.Count; i++) { + var connectingEdge = i > 0 ? cavePathCells[i].Edges.Find(e => e.AdjacentCell(cavePathCells[i]) == cavePathCells[i - 1]) : null; + if (connectingEdge != null) + { + var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null); + ConnectWaypoints(prevWp, edgeWayPoint, 500.0f); + prevWp = edgeWayPoint; + } var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null); ConnectWaypoints(prevWp, newWaypoint, 500.0f); prevWp = newWaypoint; @@ -1004,7 +1012,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 +1195,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(); @@ -1381,6 +1389,7 @@ namespace Barotrauma if (tunnel.Cells.Count == 0) { return; } List wayPoints = new List(); + WayPoint prevWayPoint = null; for (int i = 0; i < tunnel.Cells.Count; i++) { tunnel.Cells[i].CellType = CellType.Path; @@ -1390,10 +1399,58 @@ namespace Barotrauma }; wayPoints.Add(newWaypoint); - if (wayPoints.Count > 1) + if (prevWayPoint != null) { - wayPoints[wayPoints.Count - 2].ConnectTo(newWaypoint); + bool solidCellBetween = false; + foreach (GraphEdge edge in tunnel.Cells[i].Edges) + { + if (edge.AdjacentCell(tunnel.Cells[i])?.CellType == CellType.Solid && + MathUtils.LinesIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.Point1, edge.Point2)) + { + solidCellBetween = true; + break; + } + } + + if (solidCellBetween) + { + //something between the previous waypoint and this one + // -> find the edge that connects the cells and place a waypoint there, instead of connecting the centers of the cells directly + var edgeBetweenCells = tunnel.Cells[i].Edges.Find(e => e.AdjacentCell(tunnel.Cells[i]) == tunnel.Cells[i - 1]); + if (edgeBetweenCells != null) + { + var edgeWaypoint = new WayPoint(new Rectangle((int)edgeBetweenCells.Center.X, (int)edgeBetweenCells.Center.Y, 10, 10), null) + { + Tunnel = tunnel + }; + prevWayPoint.ConnectTo(edgeWaypoint); + prevWayPoint = edgeWaypoint; + } + } + prevWayPoint.ConnectTo(newWaypoint); + + //look back at the tunnel cells before the previous one, and see if the current cell shares edges with them + //= if we can "skip" from cell #1 to cell #3, create a waypoint between them. + //Fixes there sometimes not being a path past a destructible ice chunk even if there's space to go past it. + for (int j = i - 2; j > 0 && j > i - 5; j--) + { + foreach (GraphEdge edge in tunnel.Cells[i].Edges) + { + if (Vector2.DistanceSquared(edge.Point1, edge.Point2) < 30.0f * 30.0f) { continue; } + if (!edge.IsSolid && edge.AdjacentCell(tunnel.Cells[i]) == tunnel.Cells[j]) + { + var edgeWaypoint = new WayPoint(new Rectangle((int)edge.Center.X, (int)edge.Center.Y, 10, 10), null) + { + Tunnel = tunnel + }; + wayPoints[j].ConnectTo(edgeWaypoint); + edgeWaypoint.ConnectTo(newWaypoint); + break; + } + } + } } + prevWayPoint = newWaypoint; } tunnel.WayPoints.AddRange(wayPoints); @@ -1953,6 +2010,8 @@ namespace Barotrauma wayPoints.RemoveAt(i); } + Debug.Assert(wayPoints.Any(), "Couldn't generate waypoints around ruins."); + //connect ruin entrances to the outside waypoints foreach (Gap g in Gap.GapList) { @@ -1996,6 +2055,13 @@ namespace Barotrauma { for (int i = 0; i < ruin.PathCells.Count; i++) { + var connectingEdge = i > 0 ? ruin.PathCells[i].Edges.Find(e => e.AdjacentCell(ruin.PathCells[i]) == ruin.PathCells[i - 1]) : null; + if (connectingEdge != null) + { + var edgeWayPoint = new WayPoint(connectingEdge.Center, SpawnType.Path, submarine: null); + ConnectWaypoints(prevWp, edgeWayPoint, outSideWaypointInterval); + prevWp = edgeWayPoint; + } var newWaypoint = new WayPoint(ruin.PathCells[i].Center, SpawnType.Path, submarine: null); ConnectWaypoints(prevWp, newWaypoint, outSideWaypointInterval); prevWp = newWaypoint; @@ -2321,7 +2387,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 +2804,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); @@ -2930,7 +3012,7 @@ namespace Barotrauma if (!suitablePositions.Any()) { string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif @@ -2955,7 +3037,7 @@ namespace Barotrauma if (!farEnoughPositions.Any()) { string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); #if DEBUG DebugConsole.ThrowError(errorMsg); #endif @@ -3067,7 +3149,25 @@ namespace Barotrauma foreach (LevelWall wall in ExtraWalls) { - if (wall is DestructibleLevelWall destructibleWall && destructibleWall.Destroyed) { continue; } + if (wall == SeaFloor) + { + if (SeaFloorTopPos < worldPos.Y - searchDepth * GridCellSize) { continue; } + } + else + { + if (wall is DestructibleLevelWall destructibleWall && destructibleWall.Destroyed) { continue; } + bool closeEnough = false; + foreach (VoronoiCell cell in wall.Cells) + { + if (Math.Abs(cell.Center.X - worldPos.X) < (searchDepth + 1) * GridCellSize && + Math.Abs(cell.Center.Y - worldPos.Y) < (searchDepth + 1) * GridCellSize) + { + closeEnough = true; + break; + } + } + if (!closeEnough) { continue; } + } foreach (VoronoiCell cell in wall.Cells) { tempCells.Add(cell); @@ -3197,12 +3297,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 +3314,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(); @@ -3721,7 +3822,7 @@ namespace Barotrauma subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); } float outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; @@ -3731,7 +3832,7 @@ namespace Barotrauma outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset, -5000.0f, 5000.0f); string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); } Vector2 spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, subDockingPortOffset - outpostDockingPortOffset, verticalMoveDir: 1); @@ -3971,7 +4072,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..d4a2ba6e5 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(); @@ -761,7 +764,7 @@ namespace Barotrauma { string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } @@ -780,7 +783,7 @@ namespace Barotrauma { string errorMsg = "Failed to select a mission (current location not set)."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } 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/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index b14ee383e..c53c98b81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -386,7 +386,7 @@ namespace Barotrauma DebugConsole.ThrowError("Cloning entity \"" + e.Name + "\" failed.", ex); GameAnalyticsManager.AddErrorEventOnce( "MapEntity.Clone:" + e.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Cloning entity \"" + e.Name + "\" failed (" + ex.Message + ").\n" + ex.StackTrace.CleanupStackTrace()); return clones; } @@ -447,7 +447,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone."); GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectedNotFound" + connectedItem.ID, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone."); continue; } @@ -458,7 +458,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\"."); GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectionNotFound" + connectedItem.ID, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\"."); continue; } 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/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 2907db5ce..9169f06e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -1407,7 +1407,7 @@ namespace Barotrauma { string errorMsg = $"Error while loading structure \"{s.Name}\". Section damage index out of bounds. Index: {index}, section count: {s.SectionCount}."; DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Structure.Load:SectionIndexOutOfBounds", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Structure.Load:SectionIndexOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 3273bb4c3..9dad1fd8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -137,6 +137,9 @@ namespace Barotrauma get { return subBody?.Body; } } + /// + /// Extents of the solid items/structures (ones with a physics body) and hulls + /// public Rectangle Borders { get @@ -145,6 +148,17 @@ namespace Barotrauma } } + /// + /// Extents of all the visible items/structures/hulls (including ones without a physics body) + /// + public Rectangle VisibleBorders + { + get + { + return subBody == null ? Rectangle.Empty : subBody.VisibleBorders; + } + } + public override Vector2 Position { get { return subBody == null ? Vector2.Zero : subBody.Position - HiddenSubPosition; } @@ -274,14 +288,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 +1132,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 +1388,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..913d9e2cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -68,12 +68,24 @@ namespace Barotrauma } } + /// + /// Extents of the solid items/structures (ones with a physics body) and hulls + /// public Rectangle Borders { get; private set; } - + + /// + /// Extents of all the visible items/structures/hulls (including ones without a physics body) + /// + public Rectangle VisibleBorders + { + get; + private set; + } + public Vector2 Velocity { get { return Body.LinearVelocity; } @@ -122,14 +134,21 @@ namespace Barotrauma HullVertices = convexHull; Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; + Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero; farseerBody = GameMain.World.CreateBody(); farseerBody.UserData = this; - foreach (Structure wall in Structure.WallList) + foreach (var mapEntity in MapEntity.mapEntityList) { - if (wall.Submarine != submarine || wall.IsPlatform) { continue; } + if (mapEntity.Submarine != submarine || !(mapEntity is Structure wall)) { continue; } Rectangle rect = wall.Rect; + visibleMinExtents.X = Math.Min(rect.X, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(rect.Y - rect.Height, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(rect.Right, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(rect.Y, visibleMaxExtents.Y); + + if (!wall.HasBody || wall.IsPlatform || wall.StairDirection != Direction.None) { continue; } farseerBody.CreateRectangle( ConvertUnits.ToSimUnits(wall.BodyWidth), @@ -138,10 +157,10 @@ namespace Barotrauma -wall.BodyRotation, ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall; - minExtents.X = Math.Min(rect.X, minExtents.X); - minExtents.Y = Math.Min(rect.Y - rect.Height, minExtents.Y); - maxExtents.X = Math.Max(rect.Right, maxExtents.X); - maxExtents.Y = Math.Max(rect.Y, maxExtents.Y); + minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); + minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); + maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); + maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); } foreach (Hull hull in Hull.hullList) @@ -155,14 +174,20 @@ namespace Barotrauma 100.0f, ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull; - minExtents.X = Math.Min(rect.X, minExtents.X); - minExtents.Y = Math.Min(rect.Y - rect.Height, minExtents.Y); - maxExtents.X = Math.Max(rect.Right, maxExtents.X); - maxExtents.Y = Math.Max(rect.Y, maxExtents.Y); + visibleMinExtents.X = Math.Min(rect.X, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(rect.Y - rect.Height, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(rect.Right, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(rect.Y, visibleMaxExtents.Y); + + minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); + minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); + maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); + maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); } foreach (Item item in Item.ItemList) { + if (item.Submarine != submarine) { continue; } if (item.StaticBodyConfig == null || item.Submarine != submarine) { continue; } float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale; @@ -183,43 +208,48 @@ namespace Barotrauma { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); - minExtents.X = Math.Min(item.Position.X - width / 2, minExtents.X); - minExtents.Y = Math.Min(item.Position.Y - height / 2, minExtents.Y); - maxExtents.X = Math.Max(item.Position.X + width / 2, maxExtents.X); - maxExtents.Y = Math.Max(item.Position.Y + height / 2, maxExtents.Y); + visibleMinExtents.X = Math.Min(item.Position.X - width / 2, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(item.Position.Y - height / 2, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(item.Position.X + width / 2, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(item.Position.Y + height / 2, visibleMaxExtents.Y); } else if (radius > 0.0f && width > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2)); - minExtents.X = Math.Min(item.Position.X - width / 2 - radius, minExtents.X); - minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y); - maxExtents.X = Math.Max(item.Position.X + width / 2 + radius, maxExtents.X); - maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y); + visibleMinExtents.X = Math.Min(item.Position.X - width / 2 - radius, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(item.Position.X + width / 2 + radius, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); } else if (radius > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2)); - minExtents.X = Math.Min(item.Position.X - radius, minExtents.X); - minExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, minExtents.Y); - maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X); - maxExtents.Y = Math.Max(item.Position.Y + height / 2 + radius, maxExtents.Y); + visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(item.Position.Y + height / 2 + radius, visibleMaxExtents.Y); } else if (radius > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos)); - minExtents.X = Math.Min(item.Position.X - radius, minExtents.X); - minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y); - maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X); - maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y); + visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); } item.StaticFixtures.ForEach(f => f.UserData = item); + minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); + minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); + maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); + maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); } Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); + VisibleBorders = new Rectangle((int)visibleMinExtents.X, (int)visibleMaxExtents.Y, (int)(visibleMaxExtents.X - visibleMinExtents.X), (int)(visibleMaxExtents.Y - visibleMinExtents.Y)); } farseerBody.BodyType = BodyType.Dynamic; @@ -575,7 +605,7 @@ namespace Barotrauma if (newHull != null) { CoroutineManager.Invoke(() => - character.AnimController.FindHull(newHull.WorldPosition, true)); + character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true)); } return false; @@ -660,7 +690,7 @@ namespace Barotrauma { GameAnalyticsManager.AddErrorEventOnce( "SubmarineBody.HandleLimbCollision:" + submarine.ID, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Invalid velocity change in SubmarineBody.HandleLimbCollision (submarine velocity: " + Body.LinearVelocity + ", avgContactNormal: " + avgContactNormal + ", contactDot: " + contactDot @@ -835,7 +865,7 @@ namespace Barotrauma if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "SubmarineBody.ApplyImpact:InvalidImpulse", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index de504a73e..98b33bb6e 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++) { @@ -859,20 +861,20 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(this != wayPoint2); if (!linkedTo.Contains(wayPoint2)) { + linkedTo.Add(wayPoint2); OnLinksChanged?.Invoke(this); - linkedTo.Add(wayPoint2); } if (!wayPoint2.linkedTo.Contains(this)) { + wayPoint2.linkedTo.Add(this); wayPoint2.OnLinksChanged?.Invoke(wayPoint2); - wayPoint2.linkedTo.Add(this); } } - 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)), @@ -1103,7 +1105,6 @@ namespace Barotrauma Ladders = null; OnLinksChanged = null; WayPointList.Remove(this); - } - + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index d43ef274d..c5095ed42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -1,39 +1,35 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; -using Barotrauma.IO; -using System.IO.Pipes; +using System.Collections.Concurrent; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Barotrauma.Extensions; + +#if SERVER +using PipeType = System.IO.Pipes.AnonymousPipeClientStream; +#else +using PipeType = System.IO.Pipes.AnonymousPipeServerStream; +#endif namespace Barotrauma.Networking { static partial class ChildServerRelay { - private static System.IO.Stream writeStream; - private static System.IO.Stream readStream; - private static volatile bool shutDown; - public static bool HasShutDown - { - get { return shutDown; } - } + private static PipeType writeStream; + private static PipeType readStream; + private static ManualResetEvent writeManualResetEvent; - private static byte[] tempBytes; - private enum ReadState - { - WaitingForPacketStart, - WaitingForPacketEnd - }; - private static ReadState readState; - private static byte[] readIncBuf; + private static volatile bool shutDown; + public static bool HasShutDown => shutDown; + + private const int ReadBufferSize = MsgConstants.MTU * 2; + private static byte[] readTempBytes; private static int readIncOffset; private static int readIncTotal; - private static Queue msgsToWrite; - private static Queue msgsToRead; + private static ConcurrentQueue msgsToWrite; + private static ConcurrentQueue msgsToRead; private static Thread readThread; private static Thread writeThread; @@ -42,14 +38,13 @@ namespace Barotrauma.Networking private static void PrivateStart() { - readState = ReadState.WaitingForPacketStart; readIncOffset = 0; readIncTotal = 0; - tempBytes = new byte[MsgConstants.MTU * 2]; + readTempBytes = new byte[ReadBufferSize]; - msgsToWrite = new Queue(); - msgsToRead = new Queue(); + msgsToWrite = new ConcurrentQueue(); + msgsToRead = new ConcurrentQueue(); shutDown = false; @@ -88,113 +83,86 @@ namespace Barotrauma.Networking private static int ReadIncomingMsgs() { - Task readTask = readStream?.ReadAsync(tempBytes, 0, tempBytes.Length, readCancellationToken.Token); + Task readTask = readStream?.ReadAsync(readTempBytes, 0, readTempBytes.Length, readCancellationToken.Token); + if (readTask is null) { return -1; } + TimeSpan timeOut = TimeSpan.FromMilliseconds(100); for (int i = 0; i < 150; i++) { if (shutDown) { readCancellationToken?.Cancel(); - shutDown = true; return -1; } - if ((readTask?.IsCompleted ?? true) || (readTask?.Wait(timeOut) ?? true)) + if (readTask.IsCompleted || readTask.Wait(timeOut)) { break; } } - if (readTask == null || !readTask.IsCompleted) - { - readCancellationToken?.Cancel(); - shutDown = true; - return -1; - } - if (readTask.Status != TaskStatus.RanToCompletion) { - shutDown = true; - return -1; + bool swallowException = shutDown + && ((readTask.Exception?.InnerException is ObjectDisposedException) + || (readTask.Exception?.InnerException is System.IO.IOException)); + if (swallowException) + { + readCancellationToken?.Cancel(); + return -1; + } + throw new Exception( + $"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.", + readTask.Exception); } return readTask.Result; } + private static void CheckPipeConnected(string name, PipeType pipe) + { + if (!(pipe is { IsConnected: true })) + { + throw new Exception($"{name} was disconnected unexpectedly"); + } + } private static void UpdateRead() { + Span msgLengthSpan = stackalloc byte[2]; while (!shutDown) { -#if SERVER - if (!((readStream as AnonymousPipeClientStream)?.IsConnected ?? false)) + CheckPipeConnected(nameof(readStream), readStream); + + bool readBytes(Span readTo) { - shutDown = true; - return; - } -#else - if (!((readStream as AnonymousPipeServerStream)?.IsConnected ?? false)) - { - shutDown = true; - return; - } -#endif - - int readLen = ReadIncomingMsgs(); - - if (readLen < 0) { shutDown = true; return; } - - int procIndex = 0; - - while (procIndex < readLen) - { - if (readState == ReadState.WaitingForPacketStart) + for (int i = 0; i < readTo.Length; i++) { - readIncTotal = tempBytes[procIndex]; - procIndex++; - - if (procIndex >= readLen) + if (readIncOffset >= readIncTotal) { - readLen = ReadIncomingMsgs(); - - if (readLen < 0) { shutDown = true; return; } - - procIndex = 0; + readIncTotal = ReadIncomingMsgs(); + readIncOffset = 0; + if (readIncTotal == 0) { Thread.Yield(); continue; } + if (readIncTotal < 0) { return false; } } - - readIncTotal |= (tempBytes[procIndex] << 8); - procIndex++; - - if (readIncTotal <= 0) { continue; } - - readIncOffset = 0; - readIncBuf = new byte[readIncTotal]; - readState = ReadState.WaitingForPacketEnd; + readTo[i] = readTempBytes[readIncOffset]; + readIncOffset++; } - else if (readState == ReadState.WaitingForPacketEnd) - { - if ((readIncTotal - readIncOffset) > (readLen - procIndex)) - { - Array.Copy(tempBytes, procIndex, readIncBuf, readIncOffset, readLen - procIndex); - readIncOffset += (readLen - procIndex); - procIndex = readLen; - } - else - { - Array.Copy(tempBytes, procIndex, readIncBuf, readIncOffset, readIncTotal - readIncOffset); - procIndex += (readIncTotal - readIncOffset); - readIncOffset = readIncTotal; - lock (msgsToRead) - { - msgsToRead.Enqueue(readIncBuf); - } - readIncBuf = null; - readState = ReadState.WaitingForPacketStart; - } - } - - if (shutDown) { break; } + return true; } + + if (!readBytes(msgLengthSpan)) { shutDown = true; break; } + + int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8); + + if (msgLength > 0) + { + byte[] msg = new byte[msgLength]; + if (!readBytes(msg.AsSpan())) { shutDown = true; break; } + + msgsToRead.Enqueue(msg); + } + Thread.Yield(); } } @@ -203,81 +171,62 @@ namespace Barotrauma.Networking { while (!shutDown) { -#if SERVER - if (!((writeStream as AnonymousPipeClientStream)?.IsConnected ?? false)) - { - shutDown = true; - return; - } -#else - if (!((writeStream as AnonymousPipeServerStream)?.IsConnected ?? false)) - { - shutDown = true; - return; - } -#endif - bool msgAvailable; byte[] msg; - lock (msgsToWrite) - { - msgAvailable = msgsToWrite.TryDequeue(out msg); - } - while (msgAvailable) - { - byte[] lengthBytes = new byte[2]; - lengthBytes[0] = (byte)(msg.Length & 0xFF); - lengthBytes[1] = (byte)((msg.Length >> 8) & 0xFF); + CheckPipeConnected(nameof(writeStream), writeStream); - msg = lengthBytes.Concat(msg).ToArray(); + bool msgAvailable; byte[] msg; + + void writeMsg() + { + // It's SUPER IMPORTANT that this stack allocation + // remains in this local function and is never inlined, + // because C# is stupid and only calls for deallocation + // when the function returns; placing it in the loop + // this method is based around would lead to a stack + // overflow real quick! + Span bytesToWrite = stackalloc byte[2 + msg.Length]; + + bytesToWrite[0] = (byte)(msg.Length & 0xFF); + bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF); + Span msgSlice = bytesToWrite.Slice(2, msg.Length); + + msg.AsSpan().CopyTo(msgSlice); try { - writeStream?.Write(msg, 0, msg.Length); + writeStream?.Write(bytesToWrite); } - catch (ObjectDisposedException) + catch (Exception exception) { - shutDown = true; - break; - } - catch (System.IO.IOException) - { - shutDown = true; - break; + switch (exception) + { + case ObjectDisposedException _: + case System.IO.IOException _: + if (!shutDown) { throw; } + break; + default: + throw; + }; } + } + + msgAvailable = msgsToWrite.TryDequeue(out msg); + while (msgAvailable) + { + writeMsg(); if (shutDown) { break; } - lock (msgsToWrite) - { - msgAvailable = msgsToWrite.TryDequeue(out msg); - } + msgAvailable = msgsToWrite.TryDequeue(out msg); } if (!shutDown) { writeManualResetEvent.Reset(); if (!writeManualResetEvent.WaitOne(1000)) { - if (shutDown) - { - return; - } - try - { - //heartbeat to keep the other end alive - byte[] lengthBytes = new byte[2]; - lengthBytes[0] = (byte)0; - lengthBytes[1] = (byte)0; - writeStream?.Write(lengthBytes, 0, 2); - } - catch (ObjectDisposedException) - { - shutDown = true; - break; - } - catch (System.IO.IOException) - { - shutDown = true; - break; - } + if (shutDown) { return; } + + //heartbeat to keep the other end alive + msg = Array.Empty(); writeMsg(); } } } @@ -287,21 +236,15 @@ namespace Barotrauma.Networking { if (shutDown) { return; } - lock (msgsToWrite) - { - msgsToWrite.Enqueue(msg); - writeManualResetEvent.Set(); - } + msgsToWrite.Enqueue(msg); + writeManualResetEvent.Set(); } public static bool Read(out byte[] msg) { if (shutDown) { msg = null; return false; } - lock (msgsToRead) - { - return msgsToRead.TryDequeue(out msg); - } + return msgsToRead.TryDequeue(out msg); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index b5f155b33..1e3495f4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -257,7 +257,7 @@ namespace Barotrauma { string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); @@ -270,7 +270,7 @@ namespace Barotrauma { string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); @@ -283,7 +283,7 @@ namespace Barotrauma { string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) @@ -301,7 +301,7 @@ namespace Barotrauma { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); @@ -314,7 +314,7 @@ namespace Barotrauma { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); @@ -327,7 +327,7 @@ namespace Barotrauma { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); 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/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index f65151764..b5551eb43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -11,6 +11,7 @@ namespace Barotrauma.Networking public const int MaxPlayers = 256; public const int ServerNameMaxLength = 60; + public const int ServerMessageMaxLength = 2000; public static string MasterServerUrl = GameMain.Config.MasterServerUrl; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index 4a5441b57..f2bb95b69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -27,12 +27,11 @@ namespace Barotrauma.Networking { WriteEvent(tempEventBuffer, e, recipient); } - catch (Exception exception) { DebugConsole.ThrowError("Failed to write an event for the entity \"" + e.Entity + "\"", exception); GameAnalyticsManager.AddErrorEventOnce("NetEntityEventManager.Write:WriteFailed" + e.Entity.ToString(), - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, "Failed to write an event for the entity \"" + e.Entity + "\"\n" + exception.StackTrace.CleanupStackTrace()); //write an empty event to avoid messing up IDs @@ -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/NetIdUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs index ca34f5b4a..be2a6d9df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Networking static class NetIdUtils { /// - /// Is newID more recent than oldID + /// Is newID more recent than oldID, i.e. newId > oldId accounting for ushort rollover /// public static bool IdMoreRecent(ushort newID, ushort oldID) { @@ -22,6 +22,12 @@ namespace Barotrauma.Networking (id2 > id1) && (id2 - id1 > ushort.MaxValue / 2); } + /// + /// newId >= oldId accounting for ushort rollover (newer or equals) + /// + public static bool IdMoreRecentOrMatches(ushort newId, ushort oldId) + => !IdMoreRecent(oldId, newId); + public static ushort Difference(ushort id1, ushort id2) { int diff = id2 > id1 ? id2 - id1 : id1 - id2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs index 3f0f520db..e3ccaab6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs @@ -2,14 +2,14 @@ namespace Barotrauma.Networking { - public enum DeliveryMethod : byte + public enum DeliveryMethod : int { Unreliable = 0x0, Reliable = 0x1, ReliableOrdered = 0x2 } - public enum ConnectionInitialization : byte + public enum ConnectionInitialization : int { //used by all peer implementations SteamTicketAndVersion = 0x1, @@ -22,7 +22,7 @@ namespace Barotrauma.Networking } [Flags] - public enum PacketHeader : byte + public enum PacketHeader : int { //used by all peer implementations None = 0x0, @@ -34,5 +34,23 @@ namespace Barotrauma.Networking IsServerMessage = 0x8, IsHeartbeatMessage = 0x10 } + + public static class NetworkEnumExtensions + { + public static bool IsCompressed(this PacketHeader h) + => h.IsBitSet(PacketHeader.IsCompressed); + + public static bool IsConnectionInitializationStep(this PacketHeader h) + => h.IsBitSet(PacketHeader.IsConnectionInitializationStep); + + public static bool IsDisconnectMessage(this PacketHeader h) + => h.IsBitSet(PacketHeader.IsDisconnectMessage); + + public static bool IsServerMessage(this PacketHeader h) + => h.IsBitSet(PacketHeader.IsServerMessage); + + public static bool IsHeartbeatMessage(this PacketHeader h) + => h.IsBitSet(PacketHeader.IsHeartbeatMessage); + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index deb765620..def9c670e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -219,7 +219,7 @@ namespace Barotrauma.Networking internal static void EnsureBufferSize(ref byte[] buf, int numberOfBits) { - int byteLen = ((numberOfBits + 7) >> 3); + int byteLen = (numberOfBits + 7) / 8; if (buf == null) { buf = new byte[byteLen + MsgConstants.BufferOverAllocateAmount]; @@ -461,7 +461,7 @@ namespace Barotrauma.Networking { get { - return (LengthBits + ((8 - (LengthBits % 8)) % 8)) / 8; + return (LengthBits + 7) / 8; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index f425c0074..b863e11b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -201,7 +201,7 @@ namespace Barotrauma.Networking partial void UpdateReturningProjSpecific(float deltaTime); - private IEnumerable ForceShuttleToPos(Vector2 position, float speed) + private IEnumerable ForceShuttleToPos(Vector2 position, float speed) { #if SERVER 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 de713ce8f..02f2cb829 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 { @@ -44,11 +45,13 @@ namespace Barotrauma.Networking [Flags] public enum NetFlags : byte { + None = 0x0, Name = 0x1, Message = 0x2, Properties = 0x4, Misc = 0x8, - LevelSeed = 0x10 + LevelSeed = 0x10, + HiddenSubs = 0x20 } public static readonly string PermissionPresetFile = "Data" + Path.DirectorySeparatorChar + "permissionpresets.xml"; @@ -284,6 +287,8 @@ namespace Barotrauma.Networking ExtraCargo = new Dictionary(); + HiddenSubs = new HashSet(); + PermissionPreset.LoadAll(PermissionPresetFile); InitProjSpecific(); @@ -339,8 +344,14 @@ namespace Barotrauma.Networking get { return serverName; } set { - serverName = value; - if (serverName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } + string val = value; + if (val.Length > NetConfig.ServerNameMaxLength) { val = val.Substring(0, NetConfig.ServerNameMaxLength); } + if (serverName == val) { return; } + serverName = val; + ServerDetailsChanged = true; +#if SERVER + UpdateFlag(NetFlags.Name); +#endif } } @@ -350,9 +361,14 @@ namespace Barotrauma.Networking get { return serverMessageText; } set { - if (serverMessageText == value) { return; } - serverMessageText = value; + string val = value; + if (val.Length > NetConfig.ServerMessageMaxLength) { val = val.Substring(0, NetConfig.ServerMessageMaxLength); } + if (serverMessageText == val) { return; } + serverMessageText = val; ServerDetailsChanged = true; +#if SERVER + UpdateFlag(NetFlags.Message); +#endif } } @@ -370,8 +386,12 @@ namespace Barotrauma.Networking public Dictionary MonsterEnabled { get; private set; } + public const int MaxExtraCargoItemsOfType = 10; + public const int MaxExtraCargoItemTypes = 20; public Dictionary ExtraCargo { get; private set; } + public HashSet HiddenSubs { get; private set; } + private float selectedLevelDifficulty; private string password; @@ -509,6 +529,13 @@ namespace Barotrauma.Networking } } + [Serialize(Barotrauma.LosMode.Opaque, true)] + public LosMode LosMode + { + get; + set; + } + [Serialize(800, true)] public int LinesPerLogFile { @@ -993,16 +1020,17 @@ namespace Barotrauma.Networking { bool changed = false; UInt32 count = msg.ReadUInt32(); - if (ExtraCargo == null || count != ExtraCargo.Count) changed = true; + if (ExtraCargo == null || count != ExtraCargo.Count) { changed = true; } Dictionary extraCargo = new Dictionary(); for (int i = 0; i < count; i++) { string prefabIdentifier = msg.ReadString(); byte amount = msg.ReadByte(); - var itemPrefab = MapEntityPrefab.Find(null, prefabIdentifier, showErrorMessages: false) as ItemPrefab; - if (itemPrefab != null && amount > 0) + if (MapEntityPrefab.Find(null, prefabIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab && amount > 0) { + if (ExtraCargo.Keys.Count() >= MaxExtraCargoItemTypes) { continue; } + if (ExtraCargo.ContainsKey(itemPrefab) && ExtraCargo[itemPrefab] >= MaxExtraCargoItemsOfType) { continue; } if (changed || !ExtraCargo.ContainsKey(itemPrefab) || ExtraCargo[itemPrefab] != amount) { changed = true; } extraCargo.Add(itemPrefab, amount); } @@ -1026,5 +1054,36 @@ namespace Barotrauma.Networking msg.Write((byte)kvp.Value); } } + + public void ReadHiddenSubs(IReadMessage msg) + { + var subList = GameMain.NetLobbyScreen.GetSubList(); + + HiddenSubs.Clear(); + uint count = msg.ReadVariableUInt32(); + for (int i = 0; i < count; i++) + { + int index = msg.ReadUInt16(); + if (index < 0 || index >= subList.Count) { continue; } + string submarineName = subList[index].Name; + HiddenSubs.Add(submarineName); + } + +#if SERVER + MultiPlayerCampaign.UpdateCampaignSubs(); + SelectNonHiddenSubmarine(); +#endif + } + + public void WriteHiddenSubs(IWriteMessage msg) + { + var subList = GameMain.NetLobbyScreen.GetSubList(); + + msg.WriteVariableUInt32((uint)HiddenSubs.Count); + foreach (string submarineName in HiddenSubs) + { + msg.Write((UInt16)subList.FindIndex(s => s.Name.Equals(submarineName, StringComparison.OrdinalIgnoreCase))); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index 5e236219e..98e6022d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -74,6 +74,19 @@ namespace Barotrauma.Steam return Steamworks.SteamClient.Name; } + private static Steamworks.AuthTicket currentTicket = null; + public static Steamworks.AuthTicket GetAuthSessionTicket() + { + if (!isInitialized) + { + return null; + } + + currentTicket?.Cancel(); + currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); + return currentTicket; + } + public static bool OverlayCustomURL(string url) { if (!isInitialized || !Steamworks.SteamClient.IsValid) 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..5b4fd2ac9 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 @@ -539,7 +546,7 @@ namespace Barotrauma if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } @@ -566,7 +573,7 @@ namespace Barotrauma if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs index d2b2e9d27..def8398f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs @@ -27,7 +27,7 @@ namespace Barotrauma prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small - var collision = prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier); + var collision = prefabs.Find(p => p.Identifier != prefab.Identifier && p.UIntIdentifier == prefab.UIntIdentifier); if (collision != null) { DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})"); 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/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index feeb54020..8c461594f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -54,6 +54,7 @@ using Barotrauma; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; namespace Voronoi2 { @@ -194,9 +195,14 @@ namespace Voronoi2 public bool IsPointInside(Vector2 point) { + Vector2 transformedPoint = point - Translation; + if (Edges.All(e => e.Point1.X < transformedPoint.X && e.Point2.X < transformedPoint.X)) { return false; } + if (Edges.All(e => e.Point1.Y < transformedPoint.Y && e.Point2.Y < transformedPoint.Y)) { return false; } + if (Edges.All(e => e.Point1.X > transformedPoint.X && e.Point2.X > transformedPoint.X)) { return false; } + if (Edges.All(e => e.Point1.Y > transformedPoint.Y && e.Point2.Y > transformedPoint.Y)) { return false; } foreach (GraphEdge edge in Edges) { - if (MathUtils.LinesIntersect(point, Center, edge.Point1 + Translation, edge.Point2 + Translation)) { return false; } + if (MathUtils.LinesIntersect(transformedPoint, Center - Translation, edge.Point1, edge.Point2)) { return false; } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index eecd32a46..f6ad777c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -231,7 +231,7 @@ namespace Barotrauma Vector2 screenTargetPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) * 0.5f; if (CharacterHealth.OpenHealthWindow != null) { - screenTargetPos.X = GameMain.GraphicsWidth * (CharacterHealth.OpenHealthWindow.Alignment == Alignment.Left ? 0.75f : 0.25f); + screenTargetPos.X = GameMain.GraphicsWidth * (CharacterHealth.OpenHealthWindow.Alignment == Alignment.Left ? 0.6f : 0.4f); } else if (ConversationAction.IsDialogOpen) { @@ -305,7 +305,7 @@ namespace Barotrauma { string errorMsg = "Attempted to modify the state of the physics simulation while a time step was running."; DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("GameScreen.Update:WorldLockedException" + e.Message, GameAnalyticsSDK.Net.EGAErrorSeverity.Critical, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameScreen.Update:WorldLockedException" + e.Message, GameAnalyticsManager.ErrorSeverity.Critical, errorMsg); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index f8ce3190b..2b8e658f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -37,45 +37,6 @@ namespace Barotrauma #endif } - public void SetRadiationEnabled(bool enabled) - { -#if CLIENT - if (radiationEnabledTickBox == null) { return; } - radiationEnabledTickBox.Selected = enabled; -#endif - } - - public bool IsRadiationEnabled() - { -#if CLIENT - return radiationEnabledTickBox != null && radiationEnabledTickBox.Selected; -#elif SERVER - return GameMain.Server.ServerSettings.RadiationEnabled; -#endif - } - - public void SetMaxMissionCount(int maxMissionCount) - { -#if SERVER - if (GameMain.Server != null) - { - if (maxMissionCount < CampaignSettings.MinMissionCountLimit) maxMissionCount = CampaignSettings.MaxMissionCountLimit; - if (maxMissionCount > CampaignSettings.MaxMissionCountLimit) maxMissionCount = CampaignSettings.MinMissionCountLimit; - - GameMain.Server.ServerSettings.MaxMissionCount = maxMissionCount; - lastUpdateID++; - } -#endif -#if CLIENT - (maxMissionCountText as GUITextBlock).Text = maxMissionCount.ToString(); -#endif - } - - public int GetMaxMissionCount() - { - return GameMain.NetworkMember?.ServerSettings?.MaxMissionCount ?? 0; - } - public void ToggleTraitorsEnabled(int dir) { #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index c66595a6c..c5ee6f977 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Xml; using System.Xml.Linq; @@ -414,6 +415,15 @@ namespace Barotrauma return ushortValue; } + public static T GetAttributeEnum(this XElement element, string name, T defaultValue) where T : struct, Enum + { + var attr = element?.GetAttribute(name); + if (attr == null) { return defaultValue; } + return Enum.TryParse(attr.Value, true, out T result) ? result : + int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt) ? Unsafe.As(ref resultInt) : + defaultValue; + } + public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { if (element?.Attribute(name) == null) { return defaultValue; } 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..43cd51e95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -138,6 +139,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 +181,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); @@ -197,6 +203,15 @@ namespace Barotrauma } } + public class AbilityStatusEffectIdentifier : AbilityObject + { + public AbilityStatusEffectIdentifier(string effectIdentifier) + { + EffectIdentifier = effectIdentifier; + } + public string EffectIdentifier { get; set; } + } + public class GiveTalentInfo { public string[] TalentIdentifiers; @@ -209,6 +224,23 @@ namespace Barotrauma } } + public class GiveSkill + { + public string SkillIdentifier; + public float Amount; + + public GiveSkill(XElement element, string parentDebugName) + { + SkillIdentifier = element.GetAttributeString("skillidentifier", string.Empty); + Amount = element.GetAttributeFloat("amount", 0); + + if (SkillIdentifier == string.Empty) + { + DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!"); + } + } + } + public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; @@ -332,7 +364,7 @@ namespace Barotrauma private set; } - private readonly bool modifyAfflictionsByMaxVitality; + private readonly bool multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters { @@ -341,8 +373,9 @@ namespace Barotrauma public readonly List<(string affliction, float amount)> ReduceAffliction; + private readonly List talentTriggers; private readonly List giveExperiences; - private readonly List<(string identifier, float amount)> giveSkills; + private readonly List giveSkills; public float Duration => duration; @@ -394,9 +427,10 @@ namespace Barotrauma Explosions = new List(); triggeredEvents = new List(); ReduceAffliction = new List<(string affliction, float amount)>(); + talentTriggers = new List(); giveExperiences = new List(); - giveSkills = new List<(string, float)>(); - modifyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); + giveSkills = new List(); + multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); @@ -691,11 +725,14 @@ namespace Barotrauma case "aitrigger": aiTriggers.Add(new AITrigger(subElement)); break; + case "talenttrigger": + talentTriggers.Add(subElement.GetAttributeString("effectidentifier", string.Empty)); + break; case "giveexperience": giveExperiences.Add(subElement.GetAttributeInt("amount", 0)); break; case "giveskill": - giveSkills.Add((subElement.GetAttributeString("skillidentifier", ""), subElement.GetAttributeFloat("amount", 0))); + giveSkills.Add(new GiveSkill(subElement, parentDebugName)); break; } } @@ -891,20 +928,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 +1243,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 +1261,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); @@ -1245,6 +1288,7 @@ namespace Barotrauma float reduceAmount = amount * GetAfflictionMultiplier(entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.AIController?.OnHealed(healer: user, targetCharacter.Vitality - prevVitality); if (user != null && user != targetCharacter) { if (!targetCharacter.IsDead) @@ -1284,6 +1328,19 @@ namespace Barotrauma } } + if (talentTriggers.Any()) + { + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter != null && !targetCharacter.Removed) + { + foreach (string talentTrigger in talentTriggers) + { + targetCharacter.CheckTalents(AbilityEffectType.OnStatusEffectIdentifier, new AbilityStatusEffectIdentifier(talentTrigger)); + } + + } + } + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { // these effects do not need to be run clientside, as they are replicated from server to clients anyway @@ -1299,23 +1356,18 @@ namespace Barotrauma if (giveSkills.Any()) { - foreach ((string skillIdentifier, float amount) in giveSkills) + foreach (GiveSkill giveSkill in giveSkills) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) { - if (skillIdentifier?.ToLowerInvariant() == "randomskill") - { - targetCharacter.Info?.IncreaseSkillLevel(GetRandomSkill(), amount); + string skillIdentifier = giveSkill.SkillIdentifier.ToLowerInvariant() == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; - string GetRandomSkill() - { - return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandom(); - } - } - else + targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount); + + string GetRandomSkill() { - targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier?.ToLowerInvariant(), amount); + return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandom(); } } } @@ -1489,7 +1541,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 +1575,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 +1691,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); } @@ -1664,6 +1726,7 @@ namespace Barotrauma targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); if (element.User != null && element.User != targetCharacter) { + targetCharacter.AIController?.OnHealed(healer: element.User, targetCharacter.Vitality - prevVitality); if (!targetCharacter.IsDead) { targetCharacter.TryAdjustAttackerSkill(element.User, targetCharacter.Vitality - prevVitality); @@ -1696,6 +1759,11 @@ namespace Barotrauma if (entity is Item sourceItem && sourceItem.HasTag("medical")) { multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); + + if (user != null) + { + multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + } } return multiplier * AfflictionMultiplier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index c19d2db20..6def082f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -439,7 +439,7 @@ namespace Barotrauma catch (FormatException) { string errorMsg = "Failed to format text \"" + text + "\", args: " + string.Join(", ", args); - GameAnalyticsManager.AddErrorEventOnce("TextManager.GetFormatted:FormatException", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("TextManager.GetFormatted:FormatException", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return text; } } @@ -721,7 +721,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError(errorMsg, exception); #endif - GameAnalyticsManager.AddErrorEventOnce("TextManager.GetServerMessage:" + serverMessage, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("TextManager.GetServerMessage:" + serverMessage, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return errorMsg; } } 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/Either.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs index 76897c56d..2f24d0601 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs @@ -30,12 +30,20 @@ namespace Barotrauma } public override bool TryGet(out T t) { t = Value; return true; } - public override bool TryGet(out U u) { u = default(U); return false; } + public override bool TryGet(out U u) { u = default; return false; } public override bool TryCast(out V v) { - if (Value is V result) { v = result; return true; } - else { v = default(V); return false; } + if (Value is V result) + { + v = result; + return true; + } + else + { + v = default; + return false; + } } } @@ -50,13 +58,21 @@ namespace Barotrauma return Value.ToString(); } - public override bool TryGet(out T t) { t = default(T); return false; } + public override bool TryGet(out T t) { t = default; return false; } public override bool TryGet(out U u) { u = Value; return true; } public override bool TryCast(out V v) { - if (Value is V result) { v = result; return true; } - else { v = default(V); return false; } + if (Value is V result) + { + v = result; + return true; + } + else + { + v = default; + return false; + } } } } \ No newline at end of file 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..4233862e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text; +using System.Runtime.CompilerServices; namespace Barotrauma { @@ -425,7 +426,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)); } @@ -534,6 +535,16 @@ namespace Barotrauma } } + // Enum.HasFlag() sucks + public static bool IsBitSet(this T self, T bit) where T : struct, Enum + { + // This uses Unsafe.As for performance reasons, as + // C# will otherwise not allow a T -> int cast + // without first casting to object, which would make + // this not any better than Enum.HasFlag + return (Unsafe.As(ref self) & Unsafe.As(ref bit)) != 0; + } + public static string ByteArrayToString(byte[] ba) { StringBuilder hex = new StringBuilder(ba.Length * 2); diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index e41b82691..e6b23855e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index 773c3ee81..fc12f5627 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 88858bb97..3ef87f69f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 90a86631d..1884c10ea 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index c7ca33522..6089bab6b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index 0b77952ca..c88e6472d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 6b3646cab..803ea52cb 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub new file mode 100644 index 000000000..744b20f4a Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index 97053ee07..c35ec236e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index a79c97e86..119454742 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index 4d2f9b30f..f1401653e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index e75e187ce..8a614c4c1 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index efb5e3678..956eafdbd 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..18b9417a8 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,257 @@ +--------------------------------------------------------------------------------------------------------- +v0.15.21.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed "allow linking wifi to chat" server setting preventing clients without the permission to manage server settings from using their headsets. +- Fixed repair minigame not working correclty if the user has a talent that allows repairing past max condition. +- Fixed "error in Order Pump Water - the number of option names doesn't match the number of options" errors in Russian, Korean and Traditional Chinese. + +--------------------------------------------------------------------------------------------------------- +v0.15.20.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- New submarine: Orca 2. +- Improved monster spawning: different monsters spawn in different biomes and tougher monsters are introduced gradually during the course of the campaign. WIP, probably requires balancing and adjusting. Feedback is welcome. +- 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. +- Readded GameAnalytics, a library that allows us to collect gameplay statistics and error reports automatically. Sending statistics is completely optional: when you launch the game, you're asked whether you want to allow the game to send us data or not. If you decline, no data of any kind is collected. The collected data is completely anonymous and doesn't contain any personal information, but only things such as error reports and statistics about the selected game mode, submarine, missions and mods. +- Added a simple QTE "minigame" to the repair interface. Allows speeding up the repairs slightly and hopefully makes repairing feel a little more engaging. +- Characters can climb up ladders faster by holding the sprint key. +- Changed the way purchasable submarines for the multiplayer campaign are selected: instead of choosing them when creating the campaign, all of the server's visible submarines are made available for purchase. +- Diving suits play a different warning sound when low on oxygen (<5% left in tank). +- PUCS doesn't consume oxygen tanks in rooms with oxygen. +- 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. +- Connected the diving suit lockers on the vanilla submarines to the power grid so that they refill the contained oxygen tanks. +- Connected the oxygen shelves on Humpback to the power grid. +- Allowed in-game editing of lamps in R-29. +- Mudraptor shell shields block projectiles and can be destroyed by firing at them. +- Prototype Steam Cannon can damage ballast flora. +- Water detectors treat minuscule amounts (< 1.0 water volume) as 0. +- Improvements to the "infiltration" outpost event: you can talk to any of the clowns to progress, not just the one that spawned 1st, and the clowns head to the airlock when told to leave. +- Medical curtains can be opened and closed. +- Restricted the amount of additional cargo you can choose in multiplayer: max 10 items of a type, max 20 types of items. +- Allow editing item quality in the sub editor. +- Removed stun tools from riot officer loadout, replaced with riot shotgun. +- Burns slowly heal by themselves, adjusted radiation poisoning accordingly. +- Shotguns now spawn with 6 shells, autoshotguns with 8 (a full stack). + +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). +- Fixed other clients' characters sometimes appearing to teleport outside the sub for 1 frame when moving from sub to another, leading to further issues if the client has water-sensitive items in their inventory. +- Fixed whitelist controls getting disabled when adding new players to the list if whitelist wasn't enabled when opening the server settings. +- Fixed bots losing their items if you quit the 1st MP campaign round without saving, continue, and repeat that again. +- Fixed playstyle always being displayed as Serious in the server lobby unless you're hosting yourself. +- Reduced the severity and frequency of the "Maximum packet size exceeded" error in the server lobby. +- Fixed context menus not opening when right-clicking player names in the chat and server log. +- Improved the error message shown when a Steam lobby could not be created. + +Talent improvements and fixes: +- Overall balancing to the stats boots from talents. +- Adjustments to some of the ability descriptions to address some inaccuracies/ambiguities. +- Reworked "implacable": now allows the character to stay conscious for 15 seconds after falling below 0 health. +- Reworked "field medic": if you complete a mission and all crew members survived, you gain medical skill. +- Reworked "beat cop": now gives 25% repair speed and increases inflicted stuns by 25% (and the tackle). +- Reworked "reverse engineer": whenever you deconstruct an alien artifact, you have a 50% chance to gain double output from it and gain 8 to a random skill. Small alien artifacts, when deconstructed, give 4 skill instead. +- Reworked "curiosity": whenever you deconstruct an alien artifact, you and another random allied crewmember gains 125 experience. Small alien artifacts, when deconstructed, give 50 experience instead. +- Reworked "expert commando": now gives, to ranged weapons, 70% spread reduction, 40% attack multiplier and 30% attack speed reduction while crouched. +- Reworked "gene therapist": now gives an additional 25% medical item potency, correctly displays as adding a flat 10% to each refined genetic material. +- Reworked "genetic stability": no longer gives 25% repair speed, and instead gives 15 to all skills. Probability of tainting genetic material reduced to 50%. +- Reworked "still kicking": now rapidly heals over a short duration instead. +- Reworked "stand and deliver": assistant skillbook can no longer be used on another character, instead the nearest ally gains 5 to all skills when the book is read by the talent owner. +- Reworked "first aid training": Now gives a flat 35% increase to medical item potency when applied to the character. +- Reworked "evasive maneuvers": now gives 15% more money for nest/swarm missions instead of giving attack bonus inside submarine. +- Reworked "expanded research": whenever you or another allied crewmember deconstructs depleted fuel, they gain 400 experience. This effect can only occur up to 3 times until you finish another mission. +- "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. +- "Advanced splicing", "optimized power-flow" and "elbow grease" can now only give up to 100 skill bonus. +- 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 autofill not working on the new talent items. +- Fixed "crew layabout" applying stat boosts to enemies as well. +- Fixed "expert commando" talent affecting turrets. +- Fixed hardened/dementonite crowbars not opening doors faster than normal ones. +- Fixed unlocking "all seeing eye" preventing you from unlocking further talents until you've gained enough EXP to compensate for the 3 "free" talents. +- Fixed high-quality items with whose max condition is above 100% not being accepted as fabrication ingredients. +- Fixed "tandem fire" crashing the game if there are no other crewmates present. +- Fixed "safety first" talent giving you mechanical skill when you gain electrical skill instead of the other way around. +- Added PreferredContainers to the new talent items to allow the bots to clean them up. +- Rebalanced combat stimulant: now applies faster, but less in total per dose. Healing reduced. +- Dropped type "poison" from chem addiction, making it non-healable (only chem withdrawal can be healed). + +AI: +- New order: Find Weapon +- New order: Prepare for Expedition. +- New order: "Load Items", with options "Recharge battery cells", "Refill oxygen tanks", and "Reload ammo". Bots will take battery cells to be recharged, take oxygen tanks to be refilled, or place more ammo to loaders depending on the order option. +- 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. +- Changes to the default order assignment priorities, which determine the initial order of the orders when multiple is issued. You can still alter the order manually by dragging the order icons. +- 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. +- Improved the bots door interaction so that they don't slam the door at your face, nor bump into closed doors so often, or keep "smashing the select key" as frequently. There's now a three second cooldown after succesfully pressing a button before next attempt. +- Bots now hold still while waiting a door to open instead of moving towards it. +- Bots now only put oxygen tanks into oxygen generators with the "Refill Oxygen Tanks" order option. +- Bots take previous heals into account when determining when to react to attacks. Fixes deusizine and liquid oxygenite aggroing the bots when using them to treat oxygen loss. +- Bots now treat handcuffed targets as neutralized, unless they are hostiles spawned from events. Fixes e.g. bots killing captured prisoners, but also allows you to submit to the outpost guards by handcuffing yourself. +- Prevent bots from healing the targets that have recently attacked them. Fixes bots healing and shooting you at the same time. +- Fixed rescue order (healing) not working outside of the sub. +- Fixed bots not knowing how to use the Rapid Fissile Accelerator. Also improves on how they use smg, assault rifle, and autoshotgun. +- Fixed unconscious NPCs being able to see you steal. +- Fixed bots sometimes repathing too eagerly while they are on ladders, sometimes causing them to get stuck in places where they should switch from a ladder to another one nearby. +- Fixed hostages in the abandoned outpost rescue mission refusing to follow a player that has some diving gear on they don't have access to. +- Fixed pirates giving redundant orders and consequently talking too much in the chat. +- Fixed pirates not always shooting monsters if there's only a few of them. +- Fixed bots not always trying to press the right button to interact with a door. +- Fixed bots getting stuck when trying to follow a target with some diving gear they don't have access to. Now they should self-dismiss the order instead. +- Fixed bots sometimes deploying Kastrull's drone for no reason. +- Improvements to cave waypoints: should fix bots getting stuck next to destructible ice chunks even if there's a way past them. +- Husks now remain neutral towards characters that are late in the husk transformation, unless they act offensively. +- Reduced Husk and Humanhusk sight from 1 to 0.5. +- Oxygenite shards are no longer oxygen sources and can't be inserted into diving gear or tools directly. They are still a source of liquid oxygenite, which can be used to craft oxygenite tanks. +- Added some dialog for the bots when they are getting attacked outside a friendly sub to tell the player why they are fleeing from an enemy and that they don't have a weapon. +- Fixed combat objective overriding the priority of find safety objective when the combat objective shouldn't even be active, leading to bots sometimes suffocating after combat if they don't have enough oxygen in the currently equipped tank. + +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. +- Fixed most outpost modules being named as airlock and consequently defined as "wet rooms". Fixes bots still slamming doors and hatches on each others face while moving around in the outposts. +- Fixed portable pump consuming batteries when it's not pumping in/out. +- Fixed waypoint connections on Remora drone causing navigation issues for the bots. +- Fixed undocking a drone (e.g. on Remora/Kastrull) permanently breaking the navigation between the shuttle and the main sub. +- Fixed mudraptors and crawlers trying to swim to the bottom of the ocean. +- Fixed structures without a collider not being taken into account when determining which submarines are visible, causing them to vanish if there are only structures without a collider on the screen. +- Fixed Round and Ceil components returning -0 when rounding a negative value to 0. +- Fixed inability to swim up through a small hull on top of another one (e.g. the upper airlock in R-29). +- Fixed skillbooks, oxygenite shards and sulphurite shards not spawning in crates. +- Fixed name of the currently installed turret not being displayed in the submarine upgrade menu. +- Fixed AI gunners not shooting at visible enemies inside the enemy submarine. +- Fixed status monitor's electrical view not showing the power/load values in Korean. +- Fixed bots trying to equip a scooter when their hands are occupied by something that can't be put in the inventory (e.g. a crate), leading to weird twitching when they drop and immediately pick up the carried item. +- Bots stop moving before starting to repair a device. Fixes bots "flying away" when the repair fails, because they'd start the repair while still running at full speed and would keep the momentum if they get stunned because of the failed repair. +- Fixed looking at the sonar sometimes causing a significant performance hit (most often in the abyss). If a path to a marker couldn't be found when determining the distance to the marker, the game would keep trying to find a path every frame. +- Fixed Alien_Entrance3's left opening getting connected to other modules instead of being used as an entrance. +- Fixed coilgun draining ammo boxes at the same rate regardless of the number of linked loaders. +- Fixed some of the artifacts still playing sounds when inside a transport case. +- Fixed inability to put genetic materials into a container inside another container. +- Fixed characters' legs sometimes bending the wrong way around when climbing (seems to happen most often when trying to get on ladders while swimming up-side down). +- Fixed borked "Front E P2" collider. +- Fixed certain wires in vanilla subs being impossible to remove. +- Fixed gaps sometimes getting connected to incorrect hulls between docking ports, preventing water from flowing down from the space between the ports. +- Fixed mudraptor shell armor's sprite depth getting messed up when climbing ladders. +- Fixed drunkenness, opiate withdrawal and chem withdrawal not causing nausea. +- Fixed wire nodes getting misplaced in the sub editor when loading mirrored wires. +- Fixed campaign setup menu going crazy if you try to scroll it with arrow keys. +- Fixed handheld status monitor drawing the water levels and character icons of the previous sub when it's taken outside. +- Fixed spawnitem command not working in the sub editor. +- Fixed alien pistol not having a crosshair. +- Fixed weapon skill above 100 making weapons less accurate. +- Fixed cargo sometimes spawning inside the floor. Happened because the spawn position was determined based on the purchased item's size, instead of the crate the item spawns in. +- Fixed campaign getting "locked" if the submarine undocks immediately at the start of the round (on the first frame, e.g. as a result of some custom docking circuit). +- Fixed deconstructor being unable to combine the output items if they're inside a container in the output slots. +- Fixed misaligned outpost reactor meters. +- Fixed sprite bleed in the outpost computer terminal sprite. + +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. +- Fixed overriding a Controller component (e.g. button) resetting its Toggle and State settings. +- Fixed ParticleEmitter's angle not getting mirrored when spawned by a mirrored item. +- Fixed certain explosion flashduration values causing the light to flicker/loop. +- Allow the enemy AI to target using groups in addition to species names. +- Moved the turret ammo box definitions from containers.xml to where the gun and the ammo are defined. +- Fixed hulls' "hidden in game" setting doing nothing. +- Fixed certain items causing the "analyzeitem" and "deconstructvalue" console commands to crash. + --------------------------------------------------------------------------------------------------------- v0.15.13.0 --------------------------------------------------------------------------------------------------------- @@ -200,9 +454,6 @@ Additions and changes: - Enabled NVidia Optimus on Windows. Fixes: -- Fixed crashing when an attack is applied on a character from a source other than another character, e.g. propeller (unstable only). -- Fixed current_position_y output not working on nav terminals (unstable only). -- Fixed fuel rods having a bullet as a contained indicator (unstable only). - Removed duplicate welcome messages from humpack's terminal. - Fixed start and spectate buttons shrinking in the server lobby every time they're hidden and re-enabled. - Fixed contained items inside contained items not moving when repositioning a container in the sub editor (e.g. when moving a weapon holder that contains a weapon with a magazine). @@ -246,7 +497,6 @@ Fixes: - The overdosed NPC in the "good samaritan" event can't die until the player has triggered the event (completing the event after the NPC had already died made no sense). - Fixed console errors when an item a bot has been ordered to target was removed between rounds (e.g. an ignore order targeting a mission item that gets removed at the end of the round). - Fixes to oxygen generator logic: the generator now periodically recalculates how to distribute the oxygen between the vents, as opposed to doing it once at the start of the round. Just doing it once caused issues if there were e.g. vents or doors that are initially open between the rooms. -- Fixed characters sometimes getting "stuck" when swimming in partially filled multi-hull rooms. Happened because the bottom of the current hull was used as the "floor" if the actual floor was too far below, even if there was another hull below the current one, causing the ragdoll to switch to walking animation and being unable to move because it's not touching the floor (unstable only). - Fixed outpost events always unlocking the same escort mission. - The hints about flooded rooms and ballast flora aren't shown in ruins, wrecks or enemy subs. - Fixed "stowaway" event triggering an event cooldown, preventing monsters from spawning at the beginning of the round. diff --git a/Barotrauma/BarotraumaShared/daedalic_privacypolicy.txt b/Barotrauma/BarotraumaShared/daedalic_privacypolicy.txt new file mode 100644 index 000000000..3f55a5247 --- /dev/null +++ b/Barotrauma/BarotraumaShared/daedalic_privacypolicy.txt @@ -0,0 +1,71 @@ +1. Name and contact data of the controllers responsible for processing, as well as the company data protection officer + +This data protection information applies to data processing by: + +Controller: + Daedalic Entertainment GmbH + Papenreye 51 + 22453 Hamburg + +represented by the CEO Carsten Fichtelmann and the COO Stephan Harms, + e-mail: info@daedalic.com, + fax: + 49 40 356 741 36 + +The companies’ data protection officer is + + Frederik Bockslaff + +who can be reached under: + + Nimrod Rechtsanwälte + Emser Straße 9 + 10719 Berlin + E-mail: info@nimrod-rechtsanwaelte.de + tel: +49 (0)30 544 61 793 + fax: +49 (0)30 544 61 794 + +2. Collection and storage of personal data and the purpose of their use +Your personal data will be processed by Valve Corporation as the provider of the Steam store. You can find further relevant information regarding your rights in this respect in the privacy statements of Valve Corporation under the following link: + +https://store.steampowered.com/privacy_agreement/ + +Additionally, subject to your consent, for the purposes of quality management, analytics and marketing purposes the following personal data is processed and controlled by GameAnalytics Ltd: + • Time spent playing different game modes + • Round statistics (e.g. selected submarine, duration, mission success/failure, difficulty, selected job, visited caves/wrecks/ruins etc...) + • Campaign statistics (e.g. number of visited outposts and completed missions, amount of money, time used per campaign, purchased/sold items, radiation enabled/disabled, etc...) + • Tutorial completion + • Purchased submarine upgrades + • Character deaths and causes of death + • Info of server settings (anti-griefing and traitor settings) + • Enabled mods + • Error messages + • Crash logs + +The legal basis for the data processing is Art. 6 (1) lit. b, f GDPR. Our legitimate interest is derived from the purposes listed above for data collection. +For more information about privacy related to GameAnalytics Ltd, see the following link: https://gameanalytics.com/privacy/ + +3. Duration of personal data processing +The personal data which is processed according to clause 2, is processed as long, as your player client is active. If you delete your account, any personal data processed so far will be erased. + +4. Forwarding data +Unless stated otherwise under clause 2, your personal data will not be transferred to any third party for any purpose other than those listed below. +We shall forward your personal data to third parties only + • if you have given your explicit consent, in accordance with Art. 6 (1) lit. a GDPR + • if under Art. 6 (1) lit. f GDPR the transfer is necessary for the establishment, exercise or defence of legal claims, and there is no reason to assume that you have an overriding interest, which must be protected, in the non-forwarding of your data + • in the event that there is a legal obligation to forward the data under Art. 6 (1) lit. c GDPR and + • if this is legally permissible and necessary under Art. 6 (1) lit. b GDPR for the processing of contractual relationships with you. + +5. Data subject rights +You have the right: +• pursuant to Art. 15 GDPR, to demand information about your personal data that we have processed. In particular, you can obtain information about the purposes of the processing, The categories of personal data concerned, the recipients or category of recipients to whom you data have been or will be disclosed, the envisaged period for which the data will be stored, the existence of the right to request rectification, erasure, or restriction of the processing, or to object to it, the right to lodge a complaint with a supervisory authority, the source of your data, if these have not been collected by us, and on the existence of automated decision-making, including profiling, and any other meaningful information about their details or the logic involved; + + • pursuant to Art. 16 GDPR, to demand the immediate rectification or completion of inaccurate personal data stored by us; + • pursuant to Art. 17 GDPR, to demand the erasure of personal data stored by us, unless their processing is necessary for exercising the right of freedom of expression and information, for compliance with a legal obligation, for reasons of public interest, or for the establishment, exercise or defence of legal claims; + • pursuant to Art. 18 GDPR, to demand the restriction of the processing of your personal data in cases where you contest the accuracy of the data, where the processing is unlawful yet you oppose the erasure of the personal data, where we no longer need the data but you still require them to establish, exercise or defend legal claims, or where you have objected to the processing of the data pursuant to Art. 21 GDPR; + • pursuant to Art. 20 GDPR, to obtain your personal data that you have provided to us in a structured, commonly used and machine-readable format and to demand the transfer of these data to another controller; + • pursuant to Art. 7 (3) GDPR, to withdraw your consent at any time, which will mean that in future we may no longer carry out the data processing that was contingent upon this consent and + • pursuant to Art. 77 GDPR, to lodge a complaint with a supervisory authority. In general, you can contact the supervisory authority of your usual residence or place of employment, or that of our company headquarters for this purpose. + +6. Right of objection +Insofar as your personal data is processed on the basis of legitimate interests in accordance with Art. 6 (1) f GDPR, you have the right, under Art. 21 GDPR, to object to the processing of your personal data, provided there are reasons relating to your particular situation or if the objection relates to direct marketing. In the latter case, you have a general right to object, which shall be implemented by us without any reference to a particular situation. +If you wish to avail of your right to withdraw or object, an e-mail to info@nimrod-rechtsanwaelte.de will suffice. \ No newline at end of file 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 diff --git a/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj b/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj index 99073a079..725dda808 100644 --- a/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj +++ b/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj @@ -1,20 +1,20 @@ - - - - netstandard2.1 - AnyCPU;x64 - Concentus - Logan Stromberg - 1.1.6.0 - Copyright © Xiph.Org Foundation, Skype Limited, CSIRO, Microsoft Corp. - This package is a pure portable C# implementation of the Opus audio compression codec (see https://opus-codec.org/ for more details). This package contains the Opus encoder, decoder, multistream codecs, repacketizer, as well as a port of the libspeexdsp resampler. It does NOT contain code to parse .ogg or .opus container files or to manage RTP packet streams - - https://github.com/lostromb/concentus - - - - full - true - - - + + + + netstandard2.1 + AnyCPU;x64 + Concentus + Logan Stromberg + 1.1.6.0 + Copyright © Xiph.Org Foundation, Skype Limited, CSIRO, Microsoft Corp. + This package is a pure portable C# implementation of the Opus audio compression codec (see https://opus-codec.org/ for more details). This package contains the Opus encoder, decoder, multistream codecs, repacketizer, as well as a port of the libspeexdsp resampler. It does NOT contain code to parse .ogg or .opus container files or to manage RTP packet streams + + https://github.com/lostromb/concentus + + + + full + true + + + diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs index 9c7463ba7..10374743c 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs @@ -166,9 +166,9 @@ namespace Steamworks public static async Task CreateLobbyAsync( int maxMembers = 100 ) { var lobby = await Internal.CreateLobby( LobbyType.Invisible, maxMembers ); - if ( !lobby.HasValue || lobby.Value.Result != Result.OK ) return null; + if ( !lobby.HasValue ) { return null; } - return new Lobby { Id = lobby.Value.SteamIDLobby }; + return new Lobby { Id = lobby.Value.SteamIDLobby, Result = lobby.Value.Result }; } /// diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 70ecdf3d7..1a3b3fb2d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -8,11 +8,13 @@ namespace Steamworks.Data public struct Lobby { public SteamId Id { get; internal set; } + public Result Result { get; internal set; } public Lobby( SteamId id ) { Id = id; + Result = Result.OK; } /// diff --git a/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj b/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj index 69fbafa2f..96e8ca92c 100644 --- a/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj +++ b/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj @@ -1,40 +1,40 @@ - - - - netstandard2.1 - FarseerPhysics - Copyright Ian Qvist © 2013 - Farseer Physics Engine - - 3.5.0.0 - Ian Qvist - AnyCPU;x64 - - - - TRACE - portable - true - - - - - - - - - - - - - - - - - - - - - - - + + + + netstandard2.1 + FarseerPhysics + Copyright Ian Qvist © 2013 + Farseer Physics Engine + + 3.5.0.0 + Ian Qvist + AnyCPU;x64 + + + + TRACE + portable + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj index cd6803b0e..aab701817 100644 --- a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj +++ b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj @@ -1,35 +1,35 @@ - - - - netstandard2.1 - GameAnalytics.NetStandard - GameAnalytics.Net - AnyCPU;x64 - Game Analytics - Copyright (c) 2016 Game Analytics - - - - TRACE;MONO - - - - TRACE;MONO - - - - TRACE;MONO - - - - TRACE;MONO - - - - - - - - - - + + + + netstandard2.1 + GameAnalytics.NetStandard + GameAnalytics.Net + AnyCPU;x64 + Game Analytics + Copyright (c) 2016 Game Analytics + + + + TRACE;MONO + + + + TRACE;MONO + + + + TRACE;MONO + + + + TRACE;MONO + + + + + + + + + + diff --git a/Libraries/Hyper.ComponentModel/Hyper.ComponentModel.NetStandard.csproj b/Libraries/Hyper.ComponentModel/Hyper.ComponentModel.NetStandard.csproj index 4cd0d3594..3e53cc156 100644 --- a/Libraries/Hyper.ComponentModel/Hyper.ComponentModel.NetStandard.csproj +++ b/Libraries/Hyper.ComponentModel/Hyper.ComponentModel.NetStandard.csproj @@ -1,16 +1,16 @@ - - - - netstandard2.1 - Pavel Nosovich - - Hyper.ComponentModel - Copyright (c) 2014 Pavel Nosovich - AnyCPU;x64 - - - - - - - + + + + netstandard2.1 + Pavel Nosovich + + Hyper.ComponentModel + Copyright (c) 2014 Pavel Nosovich + AnyCPU;x64 + + + + + + + diff --git a/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj b/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj index 3e265503b..4cf45b8e4 100644 --- a/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj +++ b/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj @@ -1,33 +1,33 @@ - - - - netstandard2.1 - Lidgren - - Lidgren.Network - Copyright (c) 2015 lidgren - 2012.1.7.0 - AnyCPU;x64 - - - - 1701;1702;3021 - - - - 1701;1702;3021 - - - - 1701;1702;3021 - - - - 1701;1702;3021 - - - - - - - + + + + netstandard2.1 + Lidgren + + Lidgren.Network + Copyright (c) 2015 lidgren + 2012.1.7.0 + AnyCPU;x64 + + + + 1701;1702;3021 + + + + 1701;1702;3021 + + + + 1701;1702;3021 + + + + 1701;1702;3021 + + + + + + + diff --git a/Libraries/MonoGame.Framework/Src/.gitignore b/Libraries/MonoGame.Framework/Src/.gitignore index f605f607e..b0bd399e1 100644 --- a/Libraries/MonoGame.Framework/Src/.gitignore +++ b/Libraries/MonoGame.Framework/Src/.gitignore @@ -1,93 +1,93 @@ -#OS junk files -[Tt]humbs.db -*.DS_Store - -#Output Linux Installer -Installers/Linux/tmp_deb/ -Installers/Linux/tmp_run/ -*.run -*.deb - -#Visual Studio files -*.pidb -*.userprefs -*.[Oo]bj -*.exe -*.pdb -*.user -*.aps -*.pch -*.vspscc -*.vssscc -*_i.c -*_p.c -*.ncb -*.suo -*.tlb -*.tlh -*.bak -*.[Cc]ache -*.ilk -*.log -*.lib -*.sbr -*.sdf -*.csproj.csdat -ipch/ -obj/ -[Bb]in -[Dd]ebug*/ -[Rr]elease*/ -Ankh.NoLoad -.vs/ -project.lock.json -/MonoGame.Framework/MonoGame.Framework.Net.WindowsUniversal.project.lock.json -/MonoGame.Framework/MonoGame.Framework.WindowsUniversal.project.lock.json -artifacts/ - -# JetBrains Rider -.idea/ - -#Tooling -_ReSharper*/ -*.resharper -[Tt]est[Rr]esult* - -#Visual Studio Rebracer extension, allows the user to automatically change the style configuration by project -rebracer.xml - -#Subversion files -.svn - -# Office Temp Files -~$* - -#monodroid private beta -monodroid*.msi - -#Unix temporary files -*~ - -# Output docs -Documentation/Output/ - -# Protobuild Generated Files -*.speccache -*.ncrunchproject -*.ncrunchsolution - -#Mac Package Files -*.pkg -*.mpack -**/packages - -#Nuget Packages -**/*.nupkg - -#Zip files -*.zip - -Installers/MacOS/Scripts/Framework/postinstall -IDE/MonoDevelop/MonoDevelop.MonoGame/templates/Common/MonoGame.Framework.dll.config - -ThirdParty/* +#OS junk files +[Tt]humbs.db +*.DS_Store + +#Output Linux Installer +Installers/Linux/tmp_deb/ +Installers/Linux/tmp_run/ +*.run +*.deb + +#Visual Studio files +*.pidb +*.userprefs +*.[Oo]bj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*.vssscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.[Cc]ache +*.ilk +*.log +*.lib +*.sbr +*.sdf +*.csproj.csdat +ipch/ +obj/ +[Bb]in +[Dd]ebug*/ +[Rr]elease*/ +Ankh.NoLoad +.vs/ +project.lock.json +/MonoGame.Framework/MonoGame.Framework.Net.WindowsUniversal.project.lock.json +/MonoGame.Framework/MonoGame.Framework.WindowsUniversal.project.lock.json +artifacts/ + +# JetBrains Rider +.idea/ + +#Tooling +_ReSharper*/ +*.resharper +[Tt]est[Rr]esult* + +#Visual Studio Rebracer extension, allows the user to automatically change the style configuration by project +rebracer.xml + +#Subversion files +.svn + +# Office Temp Files +~$* + +#monodroid private beta +monodroid*.msi + +#Unix temporary files +*~ + +# Output docs +Documentation/Output/ + +# Protobuild Generated Files +*.speccache +*.ncrunchproject +*.ncrunchsolution + +#Mac Package Files +*.pkg +*.mpack +**/packages + +#Nuget Packages +**/*.nupkg + +#Zip files +*.zip + +Installers/MacOS/Scripts/Framework/postinstall +IDE/MonoDevelop/MonoDevelop.MonoGame/templates/Common/MonoGame.Framework.dll.config + +ThirdParty/* diff --git a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj index 6f2368424..a4477adaa 100644 --- a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj +++ b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj @@ -1,45 +1,45 @@ - - - - netstandard2.1 - SharpFont - SharpFont - Cross-platform FreeType bindings for C# - Robmaister - SharpFont - - Copyright (c) Robert Rouhani 2012-2016 - AnyCPU;x64 - - - - TRACE;DEBUG;SHARPFONT_PORTABLE - true - - - - TRACE;DEBUG;SHARPFONT_PORTABLE - true - 1701;1702;3021 - - - - TRACE;SHARPFONT_PORTABLE - true - - - - TRACE;SHARPFONT_PORTABLE - true - 1701;1702;3021 - - - - - - - - - - - + + + + netstandard2.1 + SharpFont + SharpFont + Cross-platform FreeType bindings for C# + Robmaister + SharpFont + + Copyright (c) Robert Rouhani 2012-2016 + AnyCPU;x64 + + + + TRACE;DEBUG;SHARPFONT_PORTABLE + true + + + + TRACE;DEBUG;SHARPFONT_PORTABLE + true + 1701;1702;3021 + + + + TRACE;SHARPFONT_PORTABLE + true + + + + TRACE;SHARPFONT_PORTABLE + true + 1701;1702;3021 + + + + + + + + + + + diff --git a/Libraries/XNATypes/XNATypes.csproj b/Libraries/XNATypes/XNATypes.csproj index b3b90d794..57fd8b083 100644 --- a/Libraries/XNATypes/XNATypes.csproj +++ b/Libraries/XNATypes/XNATypes.csproj @@ -1,10 +1,10 @@ - - - - netstandard2.1 - AnyCPU;x64 - - - - - + + + + netstandard2.1 + AnyCPU;x64 + + + + + diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/common.props b/Libraries/webm_mem_playback/opus/win32/VS2015/common.props index 6c757d8b7..03cd45b0c 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/common.props +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/common.props @@ -1,82 +1,82 @@ - - - - - - $(Platform)\$(Configuration)\ - $(Platform)\$(Configuration)\$(ProjectName)\ - Unicode - - - true - true - false - - - false - false - true - - - - Level3 - false - false - ..\..;..\..\include;..\..\silk;..\..\celt;..\..\win32;%(AdditionalIncludeDirectories) - HAVE_CONFIG_H;WIN32;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) - false - false - - - Console - - - true - Console - - - - - Guard - ProgramDatabase - NoExtensions - false - true - false - Disabled - false - false - Disabled - MultiThreadedDebug - MultiThreadedDebugDLL - true - false - - - true - - - - - false - None - true - true - false - Speed - Fast - Precise - true - true - true - MaxSpeed - MultiThreaded - MultiThreadedDLL - 16Bytes - - - false - - - + + + + + + $(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + Unicode + + + true + true + false + + + false + false + true + + + + Level3 + false + false + ..\..;..\..\include;..\..\silk;..\..\celt;..\..\win32;%(AdditionalIncludeDirectories) + HAVE_CONFIG_H;WIN32;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + false + false + + + Console + + + true + Console + + + + + Guard + ProgramDatabase + NoExtensions + false + true + false + Disabled + false + false + Disabled + MultiThreadedDebug + MultiThreadedDebugDLL + true + false + + + true + + + + + false + None + true + true + false + Speed + Fast + Precise + true + true + true + MaxSpeed + MultiThreaded + MultiThreadedDLL + 16Bytes + + + false + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj index ae420d508..fc2241116 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj @@ -1,399 +1,399 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - Win32Proj - opus - {219EC965-228A-1824-174D-96449D05F88A} - - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\silk\fixed;..\..\silk\float;%(AdditionalIncludeDirectories) - DLL_EXPORT;%(PreprocessorDefinitions) - FIXED_POINT;%(PreprocessorDefinitions) - /arch:IA32 %(AdditionalOptions) - - - /ignore:4221 %(AdditionalOptions) - - - "$(ProjectDir)..\..\win32\genversion.bat" "$(ProjectDir)..\..\win32\version.h" PACKAGE_VERSION - Generating version.h - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4244;%(DisableSpecificWarnings) - - - - - - - - - - - - - - - false - - - false - - - true - - - - - - - true - - - true - - - false - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + Win32Proj + opus + {219EC965-228A-1824-174D-96449D05F88A} + + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\silk\fixed;..\..\silk\float;%(AdditionalIncludeDirectories) + DLL_EXPORT;%(PreprocessorDefinitions) + FIXED_POINT;%(PreprocessorDefinitions) + /arch:IA32 %(AdditionalOptions) + + + /ignore:4221 %(AdditionalOptions) + + + "$(ProjectDir)..\..\win32\genversion.bat" "$(ProjectDir)..\..\win32\version.h" PACKAGE_VERSION + Generating version.h + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4244;%(DisableSpecificWarnings) + + + + + + + + + + + + + + + false + + + false + + + true + + + + + + + true + + + true + + + false + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters index 97eb46551..47185c67d 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters @@ -1,744 +1,744 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj index 7ad4b5e21..fcd971bb6 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj @@ -1,171 +1,171 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - - - - {016C739D-6389-43BF-8D88-24B2BF6F620F} - Win32Proj - opus_demo - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + + + + {016C739D-6389-43BF-8D88-24B2BF6F620F} + Win32Proj + opus_demo + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters index 2eb113ac8..dbcc8ae92 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters @@ -1,22 +1,22 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Source Files - - + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj index 4ba7c8ae5..e428bd3f7 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj @@ -1,171 +1,171 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - {1D257A17-D254-42E5-82D6-1C87A6EC775A} - Win32Proj - test_opus_api - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + {1D257A17-D254-42E5-82D6-1C87A6EC775A} + Win32Proj + test_opus_api + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters index 383d19f71..070c8ab01 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters @@ -1,14 +1,14 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - - - Source Files - - + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj index 8e4640094..cbf562183 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj @@ -1,171 +1,171 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - {8578322A-1883-486B-B6FA-E0094B65C9F2} - Win32Proj - test_opus_api - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + {8578322A-1883-486B-B6FA-E0094B65C9F2} + Win32Proj + test_opus_api + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters index 3036a4e70..588637e83 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters @@ -1,14 +1,14 @@ - - - - - {4a0dd677-931f-4728-afe5-b761149fc7eb} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - - - Source Files - - + + + + + {4a0dd677-931f-4728-afe5-b761149fc7eb} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj index 6804918a3..5a313c31d 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj @@ -1,172 +1,172 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - {84DAA768-1A38-4312-BB61-4C78BB59E5B8} - Win32Proj - test_opus_api - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + {84DAA768-1A38-4312-BB61-4C78BB59E5B8} + Win32Proj + test_opus_api + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters index 4ed3bb9e7..f04776380 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters @@ -1,17 +1,17 @@ - - - - - {546c8d9a-103e-4f78-972b-b44e8d3c8aba} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - - - Source Files - - - Source Files - - + + + + + {546c8d9a-103e-4f78-972b-b44e8d3c8aba} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/genversion.bat b/Libraries/webm_mem_playback/opus/win32/genversion.bat index 1def7460b..aea557393 100644 --- a/Libraries/webm_mem_playback/opus/win32/genversion.bat +++ b/Libraries/webm_mem_playback/opus/win32/genversion.bat @@ -1,37 +1,37 @@ -@echo off - -setlocal enableextensions enabledelayedexpansion - -for /f %%v in ('cd "%~dp0.." ^&^& git status ^>NUL 2^>NUL ^&^& git describe --tags --match "v*" --dirty 2^>NUL') do set version=%%v - -if not "%version%"=="" set version=!version:~1! && goto :gotversion - -if exist "%~dp0..\package_version" goto :getversion - -echo Git cannot be found, nor can package_version. Generating unknown version. - -set version=unknown - -goto :gotversion - -:getversion - -for /f "delims== tokens=2" %%v in (%~dps0..\package_version) do set version=%%v -set version=!version:"=! - -:gotversion - -set version=!version: =! -set version_out=#define %~2 "%version%" - -echo %version_out%> "%~1_temp" - -echo n | comp "%~1_temp" "%~1" > NUL 2> NUL - -if not errorlevel 1 goto exit - -copy /y "%~1_temp" "%~1" - -:exit - -del "%~1_temp" +@echo off + +setlocal enableextensions enabledelayedexpansion + +for /f %%v in ('cd "%~dp0.." ^&^& git status ^>NUL 2^>NUL ^&^& git describe --tags --match "v*" --dirty 2^>NUL') do set version=%%v + +if not "%version%"=="" set version=!version:~1! && goto :gotversion + +if exist "%~dp0..\package_version" goto :getversion + +echo Git cannot be found, nor can package_version. Generating unknown version. + +set version=unknown + +goto :gotversion + +:getversion + +for /f "delims== tokens=2" %%v in (%~dps0..\package_version) do set version=%%v +set version=!version:"=! + +:gotversion + +set version=!version: =! +set version_out=#define %~2 "%version%" + +echo %version_out%> "%~1_temp" + +echo n | comp "%~1_temp" "%~1" > NUL 2> NUL + +if not errorlevel 1 goto exit + +copy /y "%~1_temp" "%~1" + +:exit + +del "%~1_temp" diff --git a/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj b/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj index 6e90faa56..5a3253ee6 100644 --- a/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj +++ b/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj @@ -1,148 +1,148 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 15.0 - {D0097438-DA4F-4E6D-87AC-7D99DDD276B2} - vpxmemplayback - 10.0 - webm_mem_playback - - - - DynamicLibrary - true - v142 - MultiByte - - - DynamicLibrary - false - v142 - true - MultiByte - - - DynamicLibrary - true - v142 - MultiByte - - - DynamicLibrary - false - v142 - true - MultiByte - - - - - - - - - - - - - - - - - - - - - $(ProjectName)_$(Platform) - - - - Level3 - Disabled - true - true - ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) - - - %(AdditionalDependencies) - - - - - Level3 - Disabled - true - true - %(AdditionalIncludeDirectories) - MultiThreadedDebug - - - %(AdditionalDependencies) - - - - - Level3 - MaxSpeed - true - true - true - true - ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) - - - true - true - %(AdditionalDependencies) - - - - - Level3 - MaxSpeed - true - true - true - true - ..\libwebm_x86_vs19;..\libvpx_x64_vs15;..\opus\include;%(AdditionalIncludeDirectories) - MultiThreaded - Speed - - - true - true - ../libvpx_x64_vs15/$(Platform)/$(Configuration)/vpxmt.lib;../libwebm_x64_vs19/Release/libwebm.lib;../opus/win32/VS2015/x64/Release/opus.lib;%(AdditionalDependencies) - - - - - - - - - - - - - - - + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {D0097438-DA4F-4E6D-87AC-7D99DDD276B2} + vpxmemplayback + 10.0 + webm_mem_playback + + + + DynamicLibrary + true + v142 + MultiByte + + + DynamicLibrary + false + v142 + true + MultiByte + + + DynamicLibrary + true + v142 + MultiByte + + + DynamicLibrary + false + v142 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + $(ProjectName)_$(Platform) + + + + Level3 + Disabled + true + true + ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) + + + %(AdditionalDependencies) + + + + + Level3 + Disabled + true + true + %(AdditionalIncludeDirectories) + MultiThreadedDebug + + + %(AdditionalDependencies) + + + + + Level3 + MaxSpeed + true + true + true + true + ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) + + + true + true + %(AdditionalDependencies) + + + + + Level3 + MaxSpeed + true + true + true + true + ..\libwebm_x86_vs19;..\libvpx_x64_vs15;..\opus\include;%(AdditionalIncludeDirectories) + MultiThreaded + Speed + + + true + true + ../libvpx_x64_vs15/$(Platform)/$(Configuration)/vpxmt.lib;../libwebm_x64_vs19/Release/libwebm.lib;../opus/win32/VS2015/x64/Release/opus.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + \ No newline at end of file