diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 49c4b4309..1e7554604 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } } - else if (SelectedAiTarget?.Entity != null) + else if (SelectedAiTarget?.Entity != null && AttackLimb != null) { Vector2 targetPos = SelectedAiTarget.Entity.DrawPosition; if (State == AIState.Attack) @@ -37,15 +37,16 @@ namespace Barotrauma targetPos = attackWorldPos; } targetPos.Y = -targetPos.Y; - - GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); + Vector2 attackLimbPos = AttackLimb.DrawPosition; + attackLimbPos.Y = -attackLimbPos.Y; + GUI.DrawLine(spriteBatch, attackLimbPos, targetPos, GUIStyle.Red * 0.75f, 0, 4); if (wallTarget != null && !IsCoolDownRunning) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.DrawPosition; } wallTargetPos.Y = -wallTargetPos.Y; GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); - GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); + GUI.DrawLine(spriteBatch, attackLimbPos, wallTargetPos, Color.Orange * 0.75f, 0, 5); } GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black); GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {CurrentTargetMemory?.Priority.FormatZeroDecimal()}, P: {CurrentTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 4e628af8b..9e3ac4669 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -23,6 +23,8 @@ namespace Barotrauma //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black); } + Vector2 spacing = new Vector2(0, GUIStyle.Font.MeasureChar('T').Y); + Vector2 stringDrawPos = pos + textOffset; GUI.DrawString(spriteBatch, stringDrawPos, Character.Name, Color.White, Color.Black); @@ -33,14 +35,14 @@ namespace Barotrauma currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); for (int i = 0; i < currentOrders.Count; i++) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; var order = currentOrders[i]; GUI.DrawString(spriteBatch, stringDrawPos, $"ORDER {i + 1}: {order.Objective.DebugTag} ({order.Objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } } else if (ObjectiveManager.WaitTimer > 0) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos - textOffset, $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); } var currentObjective = ObjectiveManager.CurrentObjective; @@ -49,19 +51,19 @@ namespace Barotrauma int offset = currentOrder != null ? 20 + ((ObjectiveManager.CurrentOrders.Count - 1) * 20) : 0; if (currentOrder == null || currentOrder.Priority <= 0) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos, $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var subObjective = currentObjective.CurrentSubObjective; if (subObjective != null) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos, $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var activeObjective = ObjectiveManager.GetActiveObjective(); if (activeObjective != null) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos, $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } if (currentObjective is AIObjectiveCombat @@ -85,12 +87,12 @@ namespace Barotrauma } } - Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, 40); + Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, spacing.Y * 2); for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) { var objective = ObjectiveManager.Objectives[i]; GUI.DrawString(spriteBatch, objectiveStringDrawPos, $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); - objectiveStringDrawPos += new Vector2(0, 18); + objectiveStringDrawPos += spacing * 0.8f; } if (steeringManager is IndoorsSteeringManager pathSteering) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 6e2ac141b..ce45568d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -547,7 +547,7 @@ namespace Barotrauma } } - public void Draw(SpriteBatch spriteBatch, Camera cam) + public void Draw(SpriteBatch spriteBatch, Camera cam, bool onlyDrawSeveredLimbs) { if (simplePhysicsEnabled) { return; } @@ -573,8 +573,12 @@ namespace Barotrauma { foreach (Limb limb in limbs) { limb.ActiveSprite.Depth += depthOffset; } } - for (int i = 0; i < limbs.Length; i++) + for (int i = 0; i < inversedLimbDrawOrder.Length; i++) { + if (onlyDrawSeveredLimbs && !inversedLimbDrawOrder[i].IsSevered) + { + continue; + } inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color); } if (!MathUtils.NearlyEqual(depthOffset, 0.0f)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0180efb8c..c38878268 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -938,8 +938,8 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam) { - if (!Enabled || InvisibleTimer > 0.0f) { return; } - AnimController.Draw(spriteBatch, cam); + if (!Enabled) { return; } + AnimController.Draw(spriteBatch, cam, onlyDrawSeveredLimbs: InvisibleTimer > 0.0f); } public void DrawHUD(SpriteBatch spriteBatch, Camera cam, bool drawHealth = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs index 897637950..cd9ba6820 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma; @@ -106,12 +107,17 @@ public static class InteractionLabelManager } RectangleF textRect = GetLabelRect(interactableInRange, cam); - - if (labels.None(l => l.Item == interactableInRange)) + var existingLabel = labels.FirstOrDefault(l => l.Item == interactableInRange); + if (existingLabel == null) { var labelData = new LabelData(interactableInRange, textRect, RichString.Rich(interactableInRange.Prefab.Name), cam); labels.Add(labelData); } + //size of the label doesn't match - can happen when we're using a CJK font which we asynchronously render new symbols for + else if (existingLabel.TextRect.Size != textRect.Size) + { + existingLabel.TextRect = textRect; + } } PreventInteractionLabelOverlap(centerPos: character.Position); @@ -127,7 +133,11 @@ public static class InteractionLabelManager private static RectangleF GetLabelRect(Item item, Camera cam) { // create rectangle for overlap prevention - Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(RichString.Rich(item.Prefab.Name).SanitizedValue) * LabelScale; + + string nameText = RichString.Rich(item.Prefab.Name).SanitizedValue; + + var font = GUIStyle.SubHeadingFont.GetFontForStr(nameText)!; + Vector2 itemTextSizeScreen = font.MeasureString(nameText) * LabelScale; Vector2 interactablePosScreen = cam.WorldToScreen(item.Position); RectangleF textRect = new RectangleF(interactablePosScreen.X, interactablePosScreen.Y, itemTextSizeScreen.X, itemTextSizeScreen.Y); // center the rectangle on the item diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 47e90a6cb..0d9c45be1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -340,10 +340,6 @@ namespace Barotrauma break; case "randomcolor": randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandomUnsynced(); - if (randomColor.HasValue) - { - Params.GetSprite().Color = randomColor.Value; - } break; case "lightsource": LightSource = new LightSource(subElement, GetConditionalTarget()) @@ -631,6 +627,8 @@ namespace Barotrauma SoundPlayer.PlayDamageSound(damageSoundType, Math.Max(damage, bleedingDamage), WorldPosition); } + if (character.InvisibleTimer > 0.0f) { return; } + // spawn damage particles float damageParticleAmount = damage < 1 ? 0 : Math.Min(damage / 5, 1.0f) * damageMultiplier; if (damageParticleAmount > 0.001f) @@ -734,7 +732,8 @@ namespace Barotrauma if (spriteParams == null || Alpha <= 0) { return; } float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; float brightness = Math.Max(1.0f - burn, 0.2f); - Color tintedColor = spriteParams.Color; + Color baseColor = randomColor ?? spriteParams.Color; + Color tintedColor = baseColor; if (!spriteParams.IgnoreTint) { tintedColor = tintedColor.Multiply(ragdoll.RagdollParams.Color); @@ -752,7 +751,7 @@ namespace Barotrauma } } Color color = new Color(tintedColor.Multiply(brightness), tintedColor.A); - Color colorWithoutTint = new Color(spriteParams.Color.Multiply(brightness), spriteParams.Color.A); + Color colorWithoutTint = new Color(baseColor.Multiply(brightness), baseColor.A); Color blankColor = new Color(brightness, brightness, brightness, 1); if (deadTimer > 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs index d9658086f..7e0a3b547 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -31,7 +31,7 @@ namespace Barotrauma GUILayoutGroup connLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), labelList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), connLayout.RectTransform), text: conn.Connection.DisplayName, font: GUIStyle.SubHeadingFont); - GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DisplayName.Value); + GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DefaultDisplayName.Value); box.MaxTextLength = MaxConnectionLabelLength; textBoxes.Add(conn.Name, box); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 3a7988c21..e7538dc03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -277,6 +277,14 @@ namespace Barotrauma int selectedOption = (userdata as int?) ?? 0; if (actionInstance != null) { + var option = actionInstance.Options[selectedOption]; + if (GameMain.Client == null && option.ForceSay) + { + Character.Controlled.ForceSay( + option.ForceSayText.IsNullOrEmpty() ? TextManager.Get(option.Text).Fallback(option.Text) : TextManager.Get(option.ForceSayText).Fallback(option.ForceSayText), + option.ForceSayInRadio, + option.ForceSayRemoveQuotes); + } actionInstance.selectedOption = selectedOption; DisableButtons(optionButtons, btn); btn.ExternalHighlight = true; @@ -340,7 +348,8 @@ namespace Barotrauma if (speaker?.Info != null && drawChathead) { // chathead - new GUICustomComponent(new RectTransform(new Vector2(0.15f, 0.8f), content.RectTransform), onDraw: (sb, customComponent) => + int chatHeadWidth = (int)(content.RectTransform.Rect.Width * 0.15f); + new GUICustomComponent(new RectTransform(new Point(chatHeadWidth, chatHeadWidth), content.RectTransform, isFixedSize: true), onDraw: (sb, customComponent) => { speaker.Info.DrawIcon(sb, customComponent.Rect.Center.ToVector2(), customComponent.Rect.Size.ToVector2()); }); @@ -382,7 +391,7 @@ namespace Barotrauma } textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16)); - content.RectTransform.MinSize = new Point(0, content.Children.Sum(c => c.Rect.Height)); + content.RectTransform.MinSize = textContent.RectTransform.MinSize; // Recalculate the text size as it is scaled up and no longer matching the text height due to the textContent's minSize increasing textBlock.CalculateHeightFromText(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index e3a607d75..d8644061b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -61,17 +61,9 @@ namespace Barotrauma { Item.ReadSpawnData(msg); } - if (character.Submarine != null && character.AIController is EnemyAIController enemyAi) + if (character.AIController is EnemyAIController enemyAi && character.Submarine is Submarine ownSub) { - enemyAi.UnattackableSubmarines.Add(character.Submarine); - if (Submarine.MainSub != null) - { - enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); - foreach (Submarine sub in Submarine.MainSub.DockedTo) - { - enemyAi.UnattackableSubmarines.Add(sub); - } - } + enemyAi.SetUnattackableSubmarines(ownSub); } } if (characters.Contains(null)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CustomMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CustomMission.cs new file mode 100644 index 000000000..601d4ec09 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CustomMission.cs @@ -0,0 +1,8 @@ +#nullable enable +namespace Barotrauma; + +internal sealed partial class CustomMission : Mission +{ + public override bool DisplayAsCompleted => State == SuccessState; + public override bool DisplayAsFailed => State == FailureState; +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 8b9d8c492..fbf3e7a16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -14,7 +14,7 @@ namespace Barotrauma private void TryShowRetrievedMessage() { - if (DetermineCompleted()) + if (DetermineCompleted(CampaignMode.TransitionType.None)) { HandleMessage(ref allRetrievedMessage); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 918b67d16..af674c696 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1435,8 +1435,15 @@ namespace Barotrauma Uri baseAddress = new Uri(url); Uri remoteDirectory = new Uri(baseAddress, "."); string remoteFileName = Path.GetFileName(baseAddress.LocalPath); - IRestClient client = new RestClient(remoteDirectory); - var response = client.Execute(new RestRequest(remoteFileName, Method.GET)); + var client = RestFactory.CreateClient(remoteDirectory.ToString()); + var request = RestFactory.CreateRequest(remoteFileName); + var response = client.Execute(request); + if (response.ErrorException != null) + { + DebugConsole.AddWarning($"Connection error: Failed to load remote sprite from {url} " + + $"({response.ErrorException.Message})."); + return null; + } if (response.ResponseStatus != ResponseStatus.Completed) { return null; } if (response.StatusCode != HttpStatusCode.OK) { return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 3116cfd81..2b0041c13 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -26,7 +26,9 @@ namespace Barotrauma public OnSelectedHandler OnDropped; - private readonly GUIButton button; + private readonly GUIButton button; + public GUIButton Button => button; + private readonly GUIImage icon; private readonly GUIListBox listBox; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs index 928ae3519..ea17ff7b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs @@ -710,19 +710,24 @@ namespace Barotrauma if (listBox == pendingList || listBox == crewList) { - nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height)); - nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width); - nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height)); - Point size = new Point((int)(0.7f * nameBlock.Rect.Height)); - new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false }; - size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height); - new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) + //if the character is already in the crew, only check permissions - reputation doesn't matter for renaming an already-hired bot + bool canRename = listBox == crewList ? HasPermissionToHire : CanHire(characterInfo); + if (canRename) { - Enabled = CanHire(characterInfo), - ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), - UserData = characterInfo, - OnClicked = CreateRenamingComponent - }; + nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height)); + nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width); + nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height)); + Point iconSize = new Point((int)(0.7f * nameBlock.Rect.Height)); + new GUIImage(new RectTransform(iconSize, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false }; + Point buttonSize = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width + (int)(iconSize.X * 1.5f), mainGroup.Rect.Height); + new GUIButton(new RectTransform(buttonSize, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) + { + ClampMouseRectToParent = false, + ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), + UserData = characterInfo, + OnClicked = CreateRenamingComponent + }; + } } //recalculate everything and truncate texts if needed diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 0452c45e7..e33660954 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -487,7 +487,21 @@ namespace Barotrauma.Items.Components return 0.0f; } - public virtual bool ShouldDrawHUD(Character character) + public bool ShouldDrawHUD(Character character) + { + if (Character.Controlled?.SelectedItem != null) + { + Controller controller = item.GetComponent(); + if (controller != null && controller.User == Character.Controlled && controller.HideAllItemComponentHUDs) + { + return false; + } + } + + return ShouldDrawHUDComponentSpecific(character); + } + + protected virtual bool ShouldDrawHUDComponentSpecific(Character character) { return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index c0a6ece27..31af8751a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -552,9 +552,9 @@ namespace Barotrauma.Items.Components if (flippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; } float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth; - if (i < containedSpriteDepths.Length) + if (targetSlotIndex < containedSpriteDepths.Length) { - containedSpriteDepth = containedSpriteDepths[i]; + containedSpriteDepth = containedSpriteDepths[targetSlotIndex]; } containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 4e637d1dc..82183702e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -52,10 +52,12 @@ namespace Barotrauma.Items.Components partial void SetLightSourceTransformProjSpecific() { - Vector2 offset = Vector2.Zero; - if (LightOffset != Vector2.Zero) + Vector2 offset = LightOffset * item.Scale; + if (offset != Vector2.Zero) { - offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale; + if (item.FlippedX) { offset.X *= -1; } + if (item.FlippedY) { offset.Y *= -1; } + offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad)); } if (ParentBody != null) @@ -101,7 +103,10 @@ namespace Barotrauma.Items.Components if (Light?.LightSprite == null) { return; } if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) { - Vector2 offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale; + Vector2 offset = LightOffset * item.Scale; + if (item.FlippedX) { offset.X *= -1; } + if (item.FlippedY) { offset.Y *= -1; } + offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad)); Vector2 origin = Light.LightSprite.Origin; if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } @@ -114,6 +119,7 @@ namespace Barotrauma.Items.Components { color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); } + Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, @@ -128,8 +134,16 @@ namespace Barotrauma.Items.Components { if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX) { - Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ? - SpriteEffects.FlipHorizontally : SpriteEffects.None; + Light.LightSpriteEffect ^= SpriteEffects.FlipHorizontally; + } + SetLightSourceTransformProjSpecific(); + } + + public override void FlipY(bool relativeToSub) + { + if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipY) + { + Light.LightSpriteEffect ^= SpriteEffects.FlipVertically; } SetLightSourceTransformProjSpecific(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 710dcb9f6..bee454c79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -8,6 +8,30 @@ namespace Barotrauma.Items.Components { private bool isHUDsHidden; + public void UpdateMsg() + { + if (Character.Controlled == null) { return; } + + if (!string.IsNullOrEmpty(KickOutCharacterMsg) && + SelectingKicksCharacterOut && + User != null && !User.Removed) + { + DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(KickOutCharacterMsg)); + } + else if (!string.IsNullOrEmpty(PutOtherCharacterMsg) && + AllowPuttingInOtherCharacters && + CanPutSelectedCharacter(Character.Controlled.SelectedCharacter)) + { + DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(PutOtherCharacterMsg)); + } + else + { + DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)); + } + + CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled); + } + public override void DrawHUD(SpriteBatch spriteBatch, Character character) { base.DrawHUD(spriteBatch, character); @@ -69,21 +93,33 @@ namespace Barotrauma.Items.Components ushort userID = msg.ReadUInt16(); if (userID == 0) { - if (user != null) + if (User != null) { IsActive = false; - CancelUsing(user); - user = null; + CancelUsing(User); + User = null; } } else { Character newUser = Entity.FindEntityByID(userID) as Character; - if (newUser != user) + if (newUser != User) { - CancelUsing(user); + CancelUsing(User); } - user = newUser; + User = newUser; + + // If the server assigned a user to this controller but the character is not selecting the item + // on the client-side, force the selection to prevent desync. This is required for force attaching, + // since the character placed into the controller may be unconscious, and in that state + // the server no longer syncs the current SelectedItem to clients. + if (ForceUserToStayAttached && + user != null && + !user.IsAnySelectedItem(Item)) + { + user.SelectedItem = Item; + } + IsActive = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index b791fcf87..5dca29d13 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -434,18 +434,13 @@ namespace Barotrauma.Items.Components foreach (FabricationRecipe fi in fabricationRecipes.Values) { - RichString recipeTooltip = - fi.RequiresRecipe ? - RichString.Rich(fi.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖") : - RichString.Rich(fi.TargetItem.Description); - var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) { UserData = fi, HoverColor = Color.Gold * 0.2f, SelectedColor = Color.Gold * 0.5f, - ToolTip = recipeTooltip }; + SetRecipeTooltip(frame, fi); var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f }; @@ -457,7 +452,7 @@ namespace Barotrauma.Items.Components itemIcon, scaleToFit: true) { Color = itemIcon == fi.TargetItem.Sprite ? fi.TargetItem.SpriteColor : fi.TargetItem.InventoryIconColor, - ToolTip = recipeTooltip + CanBeFocused = false }; } @@ -466,7 +461,7 @@ namespace Barotrauma.Items.Components { Padding = Vector4.Zero, AutoScaleVertical = true, - ToolTip = recipeTooltip + CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight), @@ -478,6 +473,20 @@ namespace Barotrauma.Items.Components } } + private void SetRecipeTooltip(GUIComponent component, FabricationRecipe recipe) + { + if (!recipe.RequiresRecipe) + { + component.ToolTip = RichString.Rich(recipe.TargetItem.Description); + } + else + { + component.ToolTip = AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem) ? + RichString.Rich(recipe.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Green)}‖{TextManager.Get("unlockedrecipe.true")}‖color:end‖") : + RichString.Rich(recipe.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖"); + } + } + private void InitInventoryUIs() { if (inputInventoryHolder != null) @@ -927,16 +936,24 @@ namespace Barotrauma.Items.Components } } - if (recipe.RequiresRecipe && recipe.HideIfNoRecipe) + if (recipe.RequiresRecipe) { - if (Character.Controlled != null) + if (recipe.HideIfNoRecipe) { - if (!AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem)) + bool anyOneHasRecipe = AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem); + if (Character.Controlled != null) { - child.Visible = false; - continue; + if (!anyOneHasRecipe) + { + child.Visible = false; + continue; + } } } + else + { + SetRecipeTooltip(child, recipe); + } } child.Visible = @@ -1147,7 +1164,16 @@ namespace Barotrauma.Items.Components var lines = description.WrappedText.Split('\n'); if (lines.Count <= 1) { break; } string newString = string.Join('\n', lines.Take(lines.Count - 1)); - description.Text = newString.Substring(0, newString.Length - 4) + "..."; + + if (newString.Length > 4) + { + description.Text = newString.Substring(0, newString.Length - 4) + "..."; + } + else + { + description.Text = newString + "..."; + } + description.CalculateHeightFromText(); description.ToolTip = richDescription; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 3c08f7fe7..f76b9506a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -443,6 +443,7 @@ namespace Barotrauma.Items.Components var wire = targetItem.GetComponent(); if (wire != null && wire.Connections.Any(c => c != null)) { return false; } + if (targetItem.Container is { NonInteractable: true }) { return false; } if (targetItem.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } if (targetItem.HasTag(Tags.TraitorMissionItem)) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 872b9be0c..adb44aa8e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -575,6 +575,22 @@ namespace Barotrauma.Items.Components pos /= c.Resources.Count; MineralClusters.Add((center: pos, resources: c.Resources)); } + + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + if (mission is MineralMission mineralMission) + { + foreach (var minerals in mineralMission.SpawnedResources) + { + MineralClusters.Add(( + center: new Vector2(minerals.Average(m => m.WorldPosition.X), minerals.Average(m => m.WorldPosition.Y)), + resources: minerals)); + } + } + } + } } else { @@ -823,18 +839,20 @@ namespace Barotrauma.Items.Components if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } + float sonarSoundRange = t.SoundRange * t.SoundRangeOnSonarMultiplier; + float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); - if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } + if (distSqr > sonarSoundRange * sonarSoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) { Ping(t.WorldPosition, transducerCenter, - t.SoundRange * DisplayScale, 0, DisplayScale, range, + sonarSoundRange * DisplayScale, 0, DisplayScale, range, passive: true, pingStrength: 0.5f, needsToBeInSector: t); if (t.IsWithinSector(transducerCenter)) { - sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(t.SoundRange / 2000, 1.0f, 5.0f))); + sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(sonarSoundRange / 2000, 1.0f, 5.0f))); } } } @@ -977,7 +995,9 @@ namespace Barotrauma.Items.Components if (aiTarget.InDetectable) { continue; } if (aiTarget.SonarLabel.IsNullOrEmpty() || aiTarget.SoundRange <= 0.0f) { continue; } - if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) + float sonarSoundRange = aiTarget.SoundRange * aiTarget.SoundRangeOnSonarMultiplier; + + if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < sonarSoundRange * sonarSoundRange) { DrawMarker(spriteBatch, aiTarget.SonarLabel.Value, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index e86923ef7..ad7305e89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - public override bool ShouldDrawHUD(Character character) + protected override bool ShouldDrawHUDComponentSpecific(Character character) { if (item.IsHidden) { return false; } if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 9d8ba4214..cf4f06813 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components } } - public override bool ShouldDrawHUD(Character character) + protected override bool ShouldDrawHUDComponentSpecific(Character character) => character == Character.Controlled && (character.SelectedItem == item || character.SelectedSecondaryItem == item); public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 50ee85985..d7d63d74d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components MoveConnectedWires(amount); } - public override bool ShouldDrawHUD(Character character) + protected override bool ShouldDrawHUDComponentSpecific(Character character) { return character == Character.Controlled && character == user && (character.SelectedItem == item || character.SelectedSecondaryItem == item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 9b999dbae..c6a7231fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -287,20 +287,24 @@ namespace Barotrauma.Items.Components texts.Add(target.CustomInteractHUDText); textColors.Add(GUIStyle.Green); } - if (!target.IsIncapacitated && target.IsPet) + if (equipper?.FocusedCharacter == target) { - texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); - textColors.Add(GUIStyle.Green); - } - if (equipper?.FocusedCharacter == target && target.CanBeHealedBy(equipper, checkFriendlyTeam: false)) - { - texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); - textColors.Add(GUIStyle.Green); - } - if (target.CanBeDraggedBy(Character.Controlled)) - { - texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); - textColors.Add(GUIStyle.Green); + if (!target.IsIncapacitated && target.IsPet && + target.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior.CanPlayWith(Character.Controlled)) + { + texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); + textColors.Add(GUIStyle.Green); + } + if (target.CanBeHealedBy(equipper, checkFriendlyTeam: false)) + { + texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); + textColors.Add(GUIStyle.Green); + } + if (target.CanBeDraggedBy(Character.Controlled)) + { + texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); + textColors.Add(GUIStyle.Green); + } } if (target.IsUnconscious) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 21f681e5e..3f7bfdfcd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1597,7 +1597,8 @@ namespace Barotrauma { if (DraggingSlot == null || (!DraggingSlot.MouseOn())) { - Sprite sprite = DraggingItems.First().Prefab.InventoryIcon ?? DraggingItems.First().Sprite; + Item firstDraggingItem = DraggingItems.First(); + Sprite sprite = firstDraggingItem.OverrideInventorySprite ?? firstDraggingItem.Prefab.InventoryIcon ?? firstDraggingItem.Sprite; int iconSize = (int)(64 * GUI.Scale); float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f); @@ -1854,7 +1855,7 @@ namespace Barotrauma if (item != null && drawItem) { - Sprite sprite = item.Prefab.InventoryIcon ?? item.Sprite; + Sprite sprite = item.OverrideInventorySprite ?? item.Prefab.InventoryIcon ?? item.Sprite; float scale = Math.Min(Math.Min((rect.Width - 10) / sprite.size.X, (rect.Height - 10) / sprite.size.Y), 2.0f); Vector2 itemPos = rect.Center.ToVector2(); if (itemPos.Y > GameMain.GraphicsHeight) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index eb371af3f..78be7c33a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -419,7 +419,7 @@ namespace Barotrauma if (fadeInBrokenSprite != null) { - float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + float d = MathHelper.Clamp(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.0f, 0.999f); fadeInBrokenSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, textureScale: Vector2.One * Scale, depth: d); @@ -435,7 +435,7 @@ namespace Barotrauma activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth); if (fadeInBrokenSprite != null) { - float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + float d = MathHelper.Clamp(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.0f, 0.999f); fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, RotationRad, Scale, activeSprite.effects, d); } } @@ -885,7 +885,12 @@ namespace Barotrauma Spacing = (int)(25 * GUI.Scale) }; - var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; + var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, + titleFont: GUIStyle.LargeFont, + dimOutDefaultValues: false) + { + UserData = this + }; activeEditors.Add(itemEditor); itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) @@ -1045,7 +1050,12 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); - var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUIStyle.SubHeadingFont) { UserData = ic }; + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, + titleFont: GUIStyle.SubHeadingFont, + dimOutDefaultValues: false) + { + UserData = ic + }; componentEditor.Children.First().Color = Color.Black * 0.7f; activeEditors.Add(componentEditor); @@ -1064,7 +1074,12 @@ namespace Barotrauma requiredItems.Add(relatedItem); } } - requiredItems.AddRange(ic.DisabledRequiredItems); + //if we have some actual requirements, no need to keep the empty requirement + //as a "placeholder" for the user to add requirements in the sub editor + if (ic.RequiredItems.None()) + { + requiredItems.AddRange(ic.DisabledRequiredItems); + } foreach (RelatedItem relatedItem in requiredItems) { @@ -1626,12 +1641,16 @@ namespace Barotrauma activeComponents.Clear(); activeComponents.AddRange(components); - foreach (MapEntity entity in linkedTo) + Controller controller = GetComponent(); + if (controller == null || controller.User != Character.Controlled || !controller.HideAllItemComponentHUDs) { - if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i) + foreach (MapEntity entity in linkedTo) { - if (!i.DisplaySideBySideWhenLinked) { continue; } - activeComponents.AddRange(i.components); + if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i) + { + if (!i.DisplaySideBySideWhenLinked) { continue; } + activeComponents.AddRange(i.components); + } } } @@ -1701,7 +1720,9 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter != character && - otherCharacter.SelectedItem == this) + otherCharacter.SelectedItem == this && + // Prevent the in use message from being shown if a character is, for example, inside the deconstructor + !otherCharacter.IsAttachedToController()) { ItemInUseWarning.Visible = true; if (mergedHUDRect.Width > GameMain.GraphicsWidth / 2) { mergedHUDRect.Inflate(-GameMain.GraphicsWidth / 4, 0); } @@ -1751,6 +1772,11 @@ namespace Barotrauma } } + public void ClearActiveHUDs() + { + activeHUDs.Clear(); + } + readonly List texts = new(); public List GetHUDTexts(Character character, bool recreateHudTexts = true) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 7a7701b9e..6f5041c4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -166,6 +166,14 @@ namespace Barotrauma subElement.GetAttributeBool("fadein", false), subElement.GetAttributePoint("offset", Point.Zero)); + if (brokenSprite.FadeIn && brokenSprite.MaxConditionPercentage <= 0.0f) + { + DebugConsole.AddWarning( + $"Potential error in item {Identifier}: a broken sprite that's set to fade in despite the max condition being 0."+ + " The sprite cannot fade in if it's set to only appear when the item is fully broken.", + ContentPackage); + } + int spriteIndex = 0; for (int i = 0; i < brokenSprites.Count && brokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 129ef0958..49ab79cca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -16,15 +16,17 @@ namespace Barotrauma { public readonly UInt32 DecalId; public readonly int SpriteIndex; - public Vector2 NormalizedPos; + public readonly Vector2 NormalizedPos; public readonly float Scale; + public readonly float DecalAlpha; - public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale) + public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale, float decalAlpha) { DecalId = decalId; SpriteIndex = spriteIndex; NormalizedPos = normalizedPos; Scale = scale; + DecalAlpha = decalAlpha; } } @@ -696,7 +698,7 @@ namespace Barotrauma var decal = decalEventData.Decal; int decalIndex = decals.IndexOf(decal); msg.WriteByte((byte)(decalIndex < 0 ? 255 : decalIndex)); - msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(decal.BaseAlpha, 0f, 1f, 8); break; default: throw new Exception($"Malformed hull event: did not expect {eventData.GetType().Name}"); @@ -752,7 +754,9 @@ namespace Barotrauma float normalizedXPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); float normalizedYPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); float decalScale = msg.ReadRangedSingle(0.0f, 2.0f, 12); - remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale)); + float decalAlpha = msg.ReadRangedSingle(0f, 1f, 8); + + remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale, decalAlpha)); } break; case EventType.BallastFlora: @@ -804,7 +808,8 @@ namespace Barotrauma decalPosX += Submarine.Position.X; decalPosY += Submarine.Position.Y; } - AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex); + Decal decal = AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex); + decal.BaseAlpha = remoteDecal.DecalAlpha; } remoteDecals.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 8202240aa..9c145eca0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -296,13 +296,9 @@ namespace Barotrauma.Lights light.Priority = lightPriority(range, light); - int i = 0; - while (i < activeLights.Count && light.Priority < activeLights[i].Priority) - { - i++; - } - activeLights.Insert(i, light); + activeLights.Add(light); } + activeLights.Sort(static (a, b) => b.Priority.CompareTo(a.Priority)); ActiveLightCount = activeLights.Count; float lightPriority(float range, LightSource light) @@ -332,7 +328,7 @@ namespace Barotrauma.Lights activeLights.Remove(activeShadowCastingLights[i]); } } - activeLights.Sort((l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime)); + activeLights.Sort(static (l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime)); //draw light sprites attached to characters //render into a separate rendertarget using alpha blending (instead of on top of everything else with alpha blending) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index e54f37f21..2912d88f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -50,8 +50,14 @@ namespace Barotrauma.Lights [Serialize("0, 0", IsPropertySaveable.Yes), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] public Vector2 Offset { get; set; } + public float RotationRad { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] - public float Rotation { get; set; } + public float Rotation + { + get => MathHelper.ToDegrees(RotationRad); + set => RotationRad = MathHelper.ToRadians(value); + } [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+ " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")] @@ -314,6 +320,10 @@ namespace Barotrauma.Lights private float prevCalculatedRotation; private float rotation; + + /// + /// Current rotation in radians. Note that LightSourceParams.RotationRad also affects the final rotation of the light. + /// public float Rotation { get { return rotation; } @@ -322,7 +332,7 @@ namespace Barotrauma.Lights if (Math.Abs(value - rotation) < 0.001f) { return; } rotation = value; - dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation)); + RefreshDirection(); if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { @@ -486,6 +496,9 @@ namespace Barotrauma.Lights break; } } + //make sure the rotation defined in the parameters is taken into account + RefreshDirection(); + NeedsRecalculation = true; } public LightSource(LightSourceParams lightSourceParams) @@ -497,6 +510,9 @@ namespace Barotrauma.Lights { DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true); } + //make sure the rotation defined in the parameters is taken into account + RefreshDirection(); + NeedsRecalculation = true; } public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true) @@ -511,6 +527,14 @@ namespace Barotrauma.Lights if (addLight) { GameMain.LightManager.AddLight(this); } } + /// + /// Refresh the direction vector of the light (which is used for calculating shadows) based on the rotation and + /// + private void RefreshDirection() + { + dir = new Vector2(MathF.Cos(rotation - LightSourceParams.RotationRad), -MathF.Sin(rotation - LightSourceParams.RotationRad)); + } + public void Update(float time) { float brightness = 1.0f; @@ -773,9 +797,6 @@ namespace Barotrauma.Lights float boundsExtended = TextureRange; if (OverrideLightTexture != null) { - float cosAngle = (float)Math.Cos(rotation); - float sinAngle = -(float)Math.Sin(rotation); - var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); Vector2 origin = OverrideLightTextureOrigin; @@ -790,8 +811,11 @@ namespace Barotrauma.Lights origin *= TextureRange; - drawOffset.X = -origin.X * cosAngle - origin.Y * sinAngle; - drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; + //rotate the origin based on the direction + float cos = dir.X; + float sin = dir.Y; + drawOffset.X = -origin.X * cos - origin.Y * sin; + drawOffset.Y = origin.X * sin + origin.Y * cos; } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -1536,7 +1560,6 @@ namespace Barotrauma.Lights Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; lightEffect.World = Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * - Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) * Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) * transform; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index e15d5bd48..4db180177 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -193,7 +193,12 @@ namespace Barotrauma { CanTakeKeyBoardFocus = false }; - var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; + var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, + titleFont: GUIStyle.LargeFont, + dimOutDefaultValues: false) + { + UserData = this + }; if (editor.Fields.TryGetValue(nameof(Scale).ToIdentifier(), out GUIComponent[] scaleFields) && scaleFields.FirstOrDefault() is GUINumberInput scaleInput) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index c0656420b..96f56708a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -30,6 +30,13 @@ namespace Barotrauma.Networking { DualStack = GameSettings.CurrentConfig.UseDualModeSockets }; + if (NetConfig.UseLenientHandshake) + { + // More lenient timeouts for local testing, so the server would start even without perfect conditions + netPeerConfiguration.ConnectionTimeout = 60.0f; + netPeerConfiguration.ResendHandshakeInterval = 5.0f; + netPeerConfiguration.MaximumHandshakeAttempts = 20; + } if (endpoint.NetEndpoint.Address.AddressFamily == AddressFamily.InterNetworkV6) { netPeerConfiguration.LocalAddress = System.Net.IPAddress.IPv6Any; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 77c3a4005..13f701023 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -351,7 +351,7 @@ namespace Barotrauma { Level.Loaded.DrawBack(graphics, spriteBatch, cam); } - else if (GameMain.GameSession.GameMode is TestGameMode testMode) + else if (GameMain.GameSession?.GameMode is TestGameMode testMode) { graphics.Clear(testMode.BackgroundParams.BackgroundColor); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 61f536f38..e42d09894 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -49,6 +49,9 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; private GUIDropDown languageDropdown, serverExecutableDropdown; +#if DEBUG + private GUITickBox lenientHandshakeBox; +#endif private readonly GUIButton joinServerButton, hostServerButton; private readonly GUIFrame modsButtonContainer; @@ -1075,7 +1078,7 @@ namespace Barotrauma "-public", isPublicBox.Selected.ToString(), "-playstyle", ((PlayStyle)playstyleBanner.UserData).ToString(), "-banafterwrongpassword", wrongPasswordBanBox.Selected.ToString(), - "-karmaenabled", (!karmaBox.Selected).ToString(), + "-karmaenabled", (karmaBox.Selected).ToString(), "-maxplayers", maxPlayersBox.Text, "-language", languageDropdown.SelectedData.ToString() }; @@ -1114,6 +1117,13 @@ namespace Barotrauma int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); arguments.Add("-ownerkey"); arguments.Add(ownerKey.ToString()); +#if DEBUG + if (lenientHandshakeBox.Selected) + { + arguments.Add("-lenienthandshake"); + NetConfig.UseLenientHandshake = true; + } +#endif var processInfo = new ProcessStartInfo { @@ -1368,7 +1378,7 @@ namespace Barotrauma } int maxPlayers = Math.Clamp(maxPlayersElement, min: 1, max: NetConfig.MaxPlayers); - var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", true); + var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", false); var selectedPlayStyle = serverSettings.GetAttributeEnum("playstyle", PlayStyle.Casual); Vector2 textLabelSize = new Vector2(1.0f, 0.05f); @@ -1579,10 +1589,18 @@ namespace Barotrauma karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), TextManager.Get("HostServerKarmaSetting")) { - Selected = !karmaEnabled, + Selected = karmaEnabled, ToolTip = TextManager.Get("hostserverkarmasettingtooltip") }; +#if DEBUG + lenientHandshakeBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), "DEBUG: Lenient server startup timeouts") + { + Selected = true, + ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load." + }; +#endif + tickboxAreaLower.RectTransform.IsFixedSize = true; //spacing @@ -1671,8 +1689,8 @@ namespace Barotrauma if (string.IsNullOrEmpty(remoteContentUrl)) { return; } try { - var client = new RestClient(remoteContentUrl); - var request = new RestRequest("MenuContent.xml", Method.GET); + var client = RestFactory.CreateClient(remoteContentUrl); + var request = RestFactory.CreateRequest("MenuContent.xml"); TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), RemoteContentReceived); } @@ -1693,12 +1711,17 @@ namespace Barotrauma try { if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (remoteContentResponse.ErrorException != null) + { + DebugConsole.AddWarning($"Connection error: Failed to fetch remote main menu content " + + $"({remoteContentResponse.ErrorException.Message})."); + return; + } if (remoteContentResponse.StatusCode != HttpStatusCode.OK) { DebugConsole.AddWarning( "Failed to receive remote main menu content. " + - "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + - $"(error code: {remoteContentResponse.StatusCode})"); + $"The master server might be temporarily unavailable (HTTP error: {remoteContentResponse.StatusCode})"); return; } string xml = remoteContentResponse.Content; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 1e3393406..2aa54d20c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -398,7 +398,7 @@ namespace Barotrauma string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); SaveUtil.DecompressToDirectory(path, dir); - var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName)); + var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform()); if (!result.TryUnwrapSuccess(out var newPackage)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index a42a29457..206009ddc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -733,6 +733,8 @@ namespace Barotrauma AutoHideScrollBar = false, OnSelected = (component, userdata) => { + //if we're clicking on a checkbox (toggle visibility) on the list, don't select the entry on the list + if (GUI.MouseOn is GUITickBox) { return false; } //toggling selection is not how listboxes normally work, need to do that manually here SoundPlayer.PlayUISound(GUISoundType.Select); if (layerList.SelectedData == userdata) @@ -3253,6 +3255,20 @@ namespace Barotrauma = new GUITextBox(new RectTransform((1.0f, 0.15f), saveInPackageLayout.RectTransform), createClearButton: true); + packToSaveInFilter.OnTextChanged += (GUITextBox textBox, string text) => + { + + foreach (GUIComponent child in packageToSaveInList.Content.Children) + { + child.Visible = + // Get the pkgText from below + !(child.GetChild()?.GetChild() is GUITextBlock textBlock && + !textBlock.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); + } + + return true; + }; + GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPackage p) { var listItem = new GUIFrame(new RectTransform((1.0f, 0.15f), packageToSaveInList.Content.RectTransform), @@ -3273,28 +3289,26 @@ namespace Barotrauma return retVal; } + ContentPackage ownerPkg = null; + #if DEBUG //this is a debug-only option so I won't bother submitting it for localization var modifyVanillaListItem = addItemToPackageToSaveList("Modify Vanilla content package", ContentPackageManager.VanillaCorePackage); var modifyVanillaListIcon = modifyVanillaListItem.GetChild(); GUIStyle.Apply(modifyVanillaListIcon, "WorkshopMenu.EditButton"); + + if (MainSub?.Info != null && IsVanillaSub(MainSub.Info)) + { + ownerPkg = ContentPackageManager.VanillaCorePackage; + } #endif var newPackageListItem = addItemToPackageToSaveList(TextManager.Get("CreateNewLocalPackage"), null); var newPackageListIcon = newPackageListItem.GetChild(); var newPackageListText = newPackageListItem.GetChild(); GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); - new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), - onUpdate: (f, component) => - { - foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) - { - contentChild.Visible &= !(contentChild.GetChild()?.GetChild() is GUITextBlock tb && - !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); - } - }); - ContentPackage ownerPkg = null; - if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } + + if (ownerPkg == null && MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } foreach (var p in ContentPackageManager.LocalPackages) { var packageListItem = addItemToPackageToSaveList(p.Name, p); @@ -3849,6 +3863,10 @@ namespace Barotrauma return true; }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), deleteButtonHolder.RectTransform), TextManager.Get("DragAndDropSubmarineTip").Fallback(LocalizedString.EmptyString), textAlignment: Alignment.Center, font: GUIStyle.Font) + { + Wrap = true + }; if (AutoSaveInfo?.Root != null) { @@ -4486,6 +4504,7 @@ namespace Barotrauma public void ReconstructLayers() { + Dictionary previousLayers = Layers.ToDictionary(); ClearLayers(); foreach (MapEntity entity in MapEntity.MapEntityList) { @@ -4494,6 +4513,13 @@ namespace Barotrauma Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden)); } } + foreach ((string layerName, LayerData data) in previousLayers) + { + if (Layers.ContainsKey(layerName)) + { + Layers[layerName] = data; + } + } UpdateLayerPanel(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 668b1bde8..22b885e50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -26,6 +26,8 @@ namespace Barotrauma public static DateTime NextCommandPush; public static Tuple CommandBuffer; + private bool dimOutDefaultValues; + private bool isReadonly; public bool Readonly { @@ -316,16 +318,17 @@ namespace Barotrauma } } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null, bool dimOutDefaultValues = true) : this(parent, entity, inGame ? SerializableProperty.GetProperties(entity).Union(SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? false)) - : SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont) + : SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont, dimOutDefaultValues) { } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null, bool dimOutDefaultValues = true) : base(style, new RectTransform(Vector2.One, parent)) { + this.dimOutDefaultValues = dimOutDefaultValues; elementHeight = (int)(elementHeight * GUI.Scale); var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox"); var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox"); @@ -523,9 +526,67 @@ namespace Barotrauma { propertyField = CreateStringField(entity, property, value.ToString(), displayName, toolTip); } + if (propertyField != null && dimOutDefaultValues) + { + UpdateTextColors(property, entity, propertyField); + } return propertyField; } + + private void UpdateTextColors(SerializableProperty property, object parentObject, GUIComponent parentElement) + { + if (!dimOutDefaultValues) { return; } + + bool isSetToDefaultValue = false; + object currentValue = property.GetValue(parentObject); + foreach (var attribute in property.Attributes.OfType()) + { + if (XMLExtensions.DefaultValueEquals(attribute.DefaultValue, currentValue) || + //treat null and empty strings as identical, because there's no way to differentiate between those in the editor + (currentValue == null && attribute.DefaultValue is string defaultValueStr && defaultValueStr.IsNullOrEmpty())) + { + isSetToDefaultValue = true; + break; + } + } + foreach (var component in parentElement.GetAllChildren()) + { + UpdateTextColors(component, isSetToDefaultValue); + } + } + + private void UpdateTextColors(GUIComponent component, bool isSetToDefaultValue) + { + if (!dimOutDefaultValues) { return; } + + if (component is GUINumberInput numberInput) + { + SetTextColor(numberInput.TextBox.TextBlock); + } + else if (component is GUIDropDown dropDown) + { + SetTextColor(dropDown.Button.TextBlock); + } + else if (component is GUITextBox textBox) + { + SetTextColor(textBox.TextBlock); + } + else if (component is GUITextBlock textBlock) + { + SetTextColor(textBlock); + } + else if (component is GUITickBox tickBox) + { + SetTextColor(tickBox.TextBlock); + } + + void SetTextColor(GUITextBlock textBlock) + { + textBlock.TextColor = new Color(textBlock.TextColor, alpha: isSetToDefaultValue ? 0.5f : 1.0f); + } + } + public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, LocalizedString displayName, LocalizedString toolTip) { var editableAttribute = property.GetAttribute(); @@ -564,6 +625,7 @@ namespace Barotrauma tickBox.Selected = propertyValue; tickBox.Flash(Color.Red); } + UpdateTextColors(property, entity, tickBox); return true; } }; @@ -611,6 +673,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; refresh += () => { @@ -654,6 +717,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; HandleSetterValueTampering(numberInput, () => property.GetFloatValue(entity)); @@ -711,6 +775,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); return true; }; refresh += () => @@ -829,6 +894,7 @@ namespace Barotrauma TrySendNetworkUpdate(entity, property); textBox.Text = StripPrefabTags(property.GetValue(entity).ToString()); textBox.Flash(GUIStyle.Green, flashDuration: 1f); + UpdateTextColors(property, entity, frame); } //restore the entities that were selected before applying MapEntity.SelectedList.Clear(); @@ -973,6 +1039,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1046,6 +1113,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; HandleSetterValueTampering(numberInput, () => { @@ -1126,6 +1194,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1206,6 +1275,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1299,6 +1369,7 @@ namespace Barotrauma TrySendNetworkUpdate(entity, property); colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = newVal; } + UpdateTextColors(property, entity, frame); }; colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = (Color)property.GetValue(entity); fields[i] = numberInput; @@ -1373,6 +1444,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1437,6 +1509,7 @@ namespace Barotrauma TrySendNetworkUpdate(entity, property); textBox.Flash(color: GUIStyle.Green, flashDuration: 1f); } + UpdateTextColors(property, entity, frame); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs index 2214848d4..47d3d1b58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -387,13 +387,11 @@ These will hide all servers that have a discord.gg link in their name or descrip try { - var client = new RestClient($"{remoteContentUrl}spamfilter") - { - CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore) - }; + var client = RestFactory.CreateClient($"{remoteContentUrl}spamfilter"); + client.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore); client.AddDefaultHeader("Cache-Control", "no-cache"); client.AddDefaultHeader("Pragma", "no-cache"); - var request = new RestRequest("serve_spamlist.php", Method.GET); + var request = RestFactory.CreateRequest("serve_spamlist.php"); TaskPool.Add("RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived); } catch (Exception e) @@ -410,12 +408,18 @@ These will hide all servers that have a discord.gg link in their name or descrip try { if (!t.TryGetResult(out IRestResponse? remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (remoteContentResponse.ErrorException != null) + { + DebugConsole.AddWarning( + "Connection error: Failed to receive global spam filter " + + $"({remoteContentResponse.ErrorException.Message})."); + return; + } if (remoteContentResponse.StatusCode != HttpStatusCode.OK) { DebugConsole.AddWarning( - "Failed to receive global spam filter." + - "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + - $"(error code: {remoteContentResponse.StatusCode})"); + "Failed to receive global spam filter. " + + $"The master server might be temporarily unavailable, HTTP status: {remoteContentResponse.StatusCode}"); return; } string data = remoteContentResponse.Content; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index cc1ef9c79..5f33df3bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -23,13 +23,27 @@ namespace Barotrauma.Steam "submarine", "item", "monster", - "art", "mission", + "outpost", + "beaconstation", + "wreck", + "ruin", + "weapons", + "medical", + "equipment", + "art", "event set", "total conversion", + "gamemode", + "gameplaymechanics", "environment", "item assembly", "language", + "qol", + "clientside", + "serverside", + "outdated", + "library" }.ToIdentifiers().ToImmutableArray(); public class ItemThumbnail : IDisposable @@ -113,10 +127,14 @@ namespace Barotrauma.Steam string? thumbnailUrl = item.PreviewImageUrl; if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } - var client = new RestClient(thumbnailUrl); - var request = new RestRequest(".", Method.GET); + var client = RestFactory.CreateClient(thumbnailUrl); + var request = RestFactory.CreateRequest("."); IRestResponse response = await client.ExecuteAsync(request, cancellationToken); - if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) + if (response.ErrorException != null) + { + DebugConsole.NewMessage($"Connection error: Failed to load workshop item thumbnail for {item.Id} ({response.ErrorException.Message})."); + } + else if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) { using var dataStream = new System.IO.MemoryStream(); await dataStream.WriteAsync(response.RawBytes, cancellationToken); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 91038dab5..3c7ec45b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -535,9 +535,9 @@ namespace Barotrauma.Steam = new GUIListBox(rectT, style: null, isHorizontal: false) { UseGridLayout = true, - ScrollBarEnabled = false, + ScrollBarEnabled = true, ScrollBarVisible = false, - HideChildrenOutsideFrame = false, + HideChildrenOutsideFrame = true, Spacing = GUI.IntScale(4) }; tagsList.Content.ClampMouseRectToParent = false; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 89eec0609..0fd9b5f60 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.11.5.0 + 1.12.6.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 4dcfb80d3..ced8f9692 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.11.5.0 + 1.12.6.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 086a023fa..a62c8335f 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.11.5.0 + 1.12.6.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 1cc47a6fc..050b41dce 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.11.5.0 + 1.12.6.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 11fed9443..b255712af 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.11.5.0 + 1.12.6.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 6850a5174..50e04783c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -79,6 +79,15 @@ namespace Barotrauma convAction.SelectedOption = selectedOption; if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption)) { + var option = convAction.Options[selectedOption]; + if (option.ForceSay && sender.Character != null) + { + sender.Character.ForceSay( + option.ForceSayText.IsNullOrEmpty() ? TextManager.Get(option.Text).Fallback(option.Text) : TextManager.Get(option.ForceSayText).Fallback(option.ForceSayText), + option.ForceSayInRadio, + option.ForceSayRemoveQuotes); + } + foreach (Client c in convAction.TargetClients) { if (c == sender) { continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 736c98542..836dc9f1c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -133,7 +133,7 @@ namespace Barotrauma switch (winCondition) { case WinCondition.LastManStanding: - if (crews[0].Count == 0 || crews[1].Count == 0) + if (crews[0].Count == 0 && crews[1].Count == 0) { //if there are no characters in either crew, end the round teamDead[0] = teamDead[1] = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index ecada43df..2d50a32ea 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -230,6 +230,9 @@ namespace Barotrauma //handled in TryStartChildServerRelay i += 2; break; + case "-lenienthandshake": + NetConfig.UseLenientHandshake = true; + break; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs index 8d8fe2933..5a6f1708c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteBoolean(State); - msg.WriteUInt16(user == null ? (ushort)0 : user.ID); + msg.WriteUInt16(User == null || User.Removed ? (ushort)0 : User.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 089fc03df..f37295790 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -16,8 +16,11 @@ namespace Barotrauma.Items.Components public void ServerEventRead(IReadMessage msg, Client c) { if (c.Character == null) { return; } - var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); - var QTESuccess = msg.ReadBoolean(); + FixActions requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); + bool QTESuccess = msg.ReadBoolean(); + + if (!item.CanClientAccess(c) || !HasRequiredItems(c.Character, addMessage: false)) { return; } + if (requestedFixAction != FixActions.None) { if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 3d85e08ad..38e09b975 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -121,9 +121,9 @@ namespace Barotrauma if (shouldBeRemoved) { bool itemAccessDenied = prevItems.Contains(item) && // if the item was in the inventory before - !itemAccessibility[item] && // and the sender is not allowed to access it - (item.PreviousParentInventory == null || // and either the item has no previous inventory - !sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory + !itemAccessibility[item] && // and the sender is not allowed to access it + (item.PreviousParentInventory == null || // and either the item has no previous inventory + !sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory if (itemAccessDenied) { @@ -136,7 +136,7 @@ namespace Barotrauma Item droppedItem = item; Entity prevOwner = Owner; Inventory previousInventory = droppedItem.ParentInventory; - droppedItem.Drop(null); + droppedItem.Drop(sender.Character); droppedItem.PreviousParentInventory = previousInventory; var previousCharacterInventory = prevOwner switch @@ -188,9 +188,18 @@ namespace Barotrauma if (holdable != null && !holdable.CanBeDeattached()) { continue; } - bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] && - (sender.Character == null || item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory)); - + bool itemAccessDenied = !prevItems.Contains(item) && + !itemAccessibility[item] && + (item.PreviousParentInventory == null || + !sender.Character.CanAccessInventory(item.PreviousParentInventory)); + + // Prevent modified clients from being able to steal items from characters by item swapping with an existing item + // due to drag and drop being enabled + if (!sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets) && GetItemAt(slotIndex) != null) + { + itemAccessDenied = true; + } + //more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 0a66bbd08..216aeafa8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -12,8 +12,6 @@ namespace Barotrauma { private CoroutineHandle logPropertyChangeCoroutine; - public Inventory PreviousParentInventory; - public override Sprite Sprite { get { return base.Prefab?.Sprite; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index cfb0592cd..705c06fa1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -112,6 +112,7 @@ namespace Barotrauma msg.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); msg.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8); msg.WriteRangedSingle(decal.Scale, 0f, 2f, 12); + msg.WriteRangedSingle(decal.BaseAlpha, 0f, 1f, 8); } break; case BallastFloraEventData ballastFloraEventData: @@ -251,7 +252,7 @@ namespace Barotrauma break; case EventType.Decal: byte decalIndex = msg.ReadByte(); - float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); + float decalAlpha = msg.ReadRangedSingle(0f, 1f, 8); if (decalIndex < 0 || decalIndex >= decals.Count) { return; } if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 8ffc5a751..06ed826ff 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -66,6 +66,9 @@ namespace Barotrauma.Networking txt = msg.ReadString() ?? ""; } + // Sanitize incoming text message from client so they can't use RichString features + txt = txt.Replace('‖', ' '); + if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; } c.LastSentChatMsgID = ID; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 6bab9e4ec..eb0442453 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -244,6 +244,8 @@ namespace Barotrauma Character targetCharacter = inventory.Owner as Character; if (yoinker == null || item == null || thiefCharacter == null || targetCharacter == null || thiefCharacter == targetCharacter) { return; } + + if (thiefCharacter.TeamID != targetCharacter.TeamID) { return; } if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; } @@ -261,7 +263,7 @@ namespace Barotrauma } Item foundItem = null; - if (isValid(item)) + if (IsValid(item)) { foundItem = item; } @@ -269,7 +271,7 @@ namespace Barotrauma { foreach (Item containedItem in item.ContainedItems) { - if (isValid(containedItem)) + if (IsValid(containedItem)) { foundItem = containedItem; break; @@ -277,16 +279,19 @@ namespace Barotrauma } } - static bool isValid(Item item) + static bool IsValid(Item item) { - return item.GetComponent() != null || item.GetComponent() != null || item.GetComponent() != null; + return item.GetComponent() != null || IsWeapon(item); + } + static bool IsWeapon(Item item) + { + //a threshold of 10 excludes things like tools, all "proper weapons" seem to have a priority higher than that + return item.Components.Max(c => c.CombatPriority) > 10.0f || item.HasTag(Tags.Weapon); } if (foundItem == null) { return; } bool isIdCard = foundItem.GetComponent() != null; - bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; - if (isIdCard) { string name = string.Empty; @@ -325,7 +330,7 @@ namespace Barotrauma JobPrefab clientJob = yoinker.CharacterInfo?.Job?.Prefab; // security officers receive less karma penalty - if (clientJob != null && clientJob.Identifier == "securityofficer" && isWeapon) + if (clientJob != null && clientJob.Identifier == "securityofficer" && IsWeapon(foundItem)) { karmaDecrease *= 0.5f; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 79bfedd93..b320a8d6d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -32,6 +32,13 @@ namespace Barotrauma.Networking Port = serverSettings.Port, DualStack = GameSettings.CurrentConfig.UseDualModeSockets }; + if (NetConfig.UseLenientHandshake) + { + // More lenient timeouts for local testing, so the server would start even without perfect conditions + netPeerConfiguration.ConnectionTimeout = 60.0f; + netPeerConfiguration.ResendHandshakeInterval = 5.0f; + netPeerConfiguration.MaximumHandshakeAttempts = 20; + } netPeerConfiguration.DisableMessageType( NetIncomingMessageType.DebugMessage diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index e1e2a6c78..7c78adc98 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -582,6 +582,23 @@ namespace Barotrauma.Networking teamSpecificState.RespawnItems.AddRange(AutoItemPlacer.RegenerateLoot(respawnShuttle, respawnContainer)); } } + else if (character.InWater) + { + if (divingSuitPrefab != null) + { + var divingSuit = new Item(divingSuitPrefab, character.Position, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit)); + character.Inventory.TryPutItem(divingSuit, user: null, allowedSlots: divingSuit.AllowedSlots); + teamSpecificState.RespawnItems.Add(divingSuit); + if (oxyPrefab != null && divingSuit.GetComponent() != null) + { + var oxyTank = new Item(oxyPrefab, character.Position, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); + divingSuit.Combine(oxyTank, user: null); + teamSpecificState.RespawnItems.Add(oxyTank); + } + } + } var characterData = campaign?.GetClientCharacterData(clients[i]); // NOTE: This was where Reaper's tax got applied diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 8e07d5c89..3e65ec3d6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -46,7 +46,7 @@ namespace Barotrauma.Networking .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); private bool IsFlagRequired(Client c, NetFlags flag) - => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate); + => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate) || !c.InitialLobbyUpdateSent; public NetFlags GetRequiredFlags(Client c) => LastUpdateIdForFlag.Keys diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ebbd2c594..828830e30 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.11.5.0 + 1.12.6.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml index f90685787..bb24f0573 100644 --- a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml @@ -45,6 +45,11 @@ + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/Lighting stress (10000 lights).sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/Lighting stress (10000 lights).sub new file mode 100644 index 000000000..282a30b37 Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/Lighting stress (10000 lights).sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/filelist.xml new file mode 100644 index 000000000..f897a3c2b --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/filelist.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/EthanolPowerGenerator.png b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/EthanolPowerGenerator.png new file mode 100644 index 000000000..76d1aaf54 Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/EthanolPowerGenerator.png differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/OxygenDispenserTest.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/OxygenDispenserTest.xml new file mode 100644 index 000000000..1cf6e86ca --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/OxygenDispenserTest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub index 0a09be029..7eafd5c1c 100644 Binary files a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml new file mode 100644 index 000000000..4b58a87c8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml index fb1fd016e..be951b04d 100644 --- a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml @@ -1,4 +1,6 @@  - + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/Events.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/Events.xml new file mode 100644 index 000000000..fecc60223 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/Events.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/[DebugOnlyTest]TestPathFinding.sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/[DebugOnlyTest]TestPathFinding.sub new file mode 100644 index 000000000..b1620c419 Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/[DebugOnlyTest]TestPathFinding.sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/filelist.xml new file mode 100644 index 000000000..bc9dfb0f6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/filelist.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/README.txt b/Barotrauma/BarotraumaShared/README.txt deleted file mode 100644 index 6ba44b3f5..000000000 --- a/Barotrauma/BarotraumaShared/README.txt +++ /dev/null @@ -1,38 +0,0 @@ -BAROTRAUMA - -http://www.barotraumagame.com - -© 2017-2024 FakeFish Ltd. All rights reserved. -© 2019-2024 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. -Privacy policy: http://privacypolicy.daedalic.com - -See the wiki for more detailed info and instructions: -http://barotraumagame.com/wiki - ------------------------------------------------------------------------- - -Port forwarding: -You may try to forward ports on your router using UPnP (Universal Plug and -Play) port forwarding by selecting "Attempt UPnP port forwarding" in the -"Host Server" menu. - -However, UPnP isn't supported by all routers, so you may need to setup port -forwards manually. The exact steps for forwarding a port depend on your -router's model, but you may be able to find a port forwarding guide for -your particular router/application on portforward.com or by practicing -your google-fu skills. - -These are the values that you should use when forwarding a port to your -Barotrauma server: - -Game port (used to communicate with clients) - Service/Application: barotrauma - External Port: The port you have selected for your server (27015 by default) - Internal Port: The port you have selected for your server (27015 by default) - Protocol: UDP - -Query port (used to communicate with Steam) - Service/Application: barotrauma - External Port: The port you have selected for your server (27016 by default) - Internal Port: The port you have selected for your server (27016 by default) - Protocol: UDP \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs index a4feacedd..2e0365317 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -551,9 +551,14 @@ namespace Barotrauma private static void UnlockKillAchievement(Character killer, Character target, Identifier identifier) { - if (killer != null && - target.Params.UnlockKillAchievementForWholeCrew && - GameSession.GetSessionCrewCharacters(CharacterType.Player).Contains(killer)) + bool alwaysUnlockForWholeCrew = false; +#if CLIENT + alwaysUnlockForWholeCrew = GameMain.GameSession?.Campaign is SinglePlayerCampaign; +#endif + + if (killer != null && + (alwaysUnlockForWholeCrew || target.Params.UnlockKillAchievementForWholeCrew) && + GameSession.GetSessionCrewCharacters(CharacterType.Both).Contains(killer)) { UnlockAchievement(identifier, unlockClients: true, characterConditions: c => c != null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 6501c1333..8e005243b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -49,6 +49,13 @@ namespace Barotrauma } } + + /// + /// A multiplier for the sound range for the purposes of displaying the target on sonar. + /// E.g. a value of 10 would mean the sonar can detect the target from x10 further than monsters. + /// + public float SoundRangeOnSonarMultiplier { get; private set; } = 1.0f; + public float SightRange { get { return sightRange; } @@ -206,6 +213,7 @@ namespace Barotrauma MinSoundRange = element.GetAttributeFloat("minsoundrange", 0f); MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange); MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange); + SoundRangeOnSonarMultiplier = element.GetAttributeFloat(nameof(SoundRangeOnSonarMultiplier), 1.0f); FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime); Static = element.GetAttributeBool("static", Static); StaticSight = element.GetAttributeBool("staticsight", StaticSight); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 49389c008..ce74caefa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -244,14 +244,39 @@ namespace Barotrauma /// /// The monster won't try to damage these submarines /// - public HashSet UnattackableSubmarines + private readonly HashSet unattackableSubmarines = new HashSet(); + + public void SetUnattackableSubmarines(Submarine submarine, bool includeOwnSub = true, bool includeConnectedSubs = true, bool clearExisting = true) { - get; - private set; - } = new HashSet(); + if (clearExisting) + { + unattackableSubmarines.Clear(); + } + if (submarine != null) + { + AddSubs(submarine); + } + if (includeOwnSub && Character.Submarine is Submarine ownSub && ownSub != submarine) + { + AddSubs(ownSub); + } + + void AddSubs(Submarine sub) + { + unattackableSubmarines.Add(sub); + if (includeConnectedSubs) + { + foreach (Submarine connectedSub in sub.DockedTo) + { + unattackableSubmarines.Add(connectedSub); + } + } + } + } public static bool IsTargetBeingChasedBy(Character target, Character character) => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && enemyAI.State is AIState.Attack or AIState.Aggressive; + public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c); private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character); @@ -539,26 +564,7 @@ namespace Barotrauma //doesn't do anything usually, but events may sometimes change monsters' (or pets' that use enemy AI) teams Character.UpdateTeam(); - bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); - if (steeringManager == insideSteering) - { - var currPath = PathSteering.CurrentPath; - if (currPath != null && currPath.CurrentNode != null) - { - if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) - { - // Don't allow to jump from too high. - float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; - float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y); - ignorePlatforms = height < allowedJumpHeight; - } - } - if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) - { - Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); - } - } - Character.AnimController.IgnorePlatforms = ignorePlatforms; + HandleLaddersAndPlatforms(deltaTime); if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) @@ -986,6 +992,69 @@ namespace Barotrauma } } + //how often the character can try ragdolling to drop down + private const float MaxDroppingInterval = 5.0f; + + //last time the character tried ragdolling to drop down + private double lastDroppingTime; + + //how long the character can stay ragdolled to drop down + private const float MaxDroppingTime = 1.0f; + + //timer for the duration of the ragdolling + private float droppingTimer; + + private void HandleLaddersAndPlatforms(float deltaTime) + { + bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); + if (steeringManager == insideSteering) + { + var currPath = PathSteering.CurrentPath; + if (currPath is { CurrentNode: WayPoint currentNode }) + { + Vector2 colliderBottom = Character.AnimController.GetColliderBottom(); + if (Character.Submarine != currentNode.Submarine) + { + colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, Character.Submarine); + } + if (currentNode.SimPosition.Y < colliderBottom.Y) + { + // Don't allow to jump from too high. + float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; + Vector2 diff = currentNode.WorldPosition - Character.WorldPosition; + float height = ConvertUnits.ToSimUnits(Math.Abs(diff.Y)); + ignorePlatforms = height < allowedJumpHeight; + + //trying to head down ladders, but can't climb -> periodically try ragdolling to get down + //(may be required by large monsters like mudraptors to fit through hatches) + if (ignorePlatforms && !Character.CanClimb && PathSteering.IsCurrentNodeLadder && + ConvertUnits.ToSimUnits(Math.Abs(diff.X)) < Character.AnimController.Collider.GetMaxExtent()) + { + if (lastDroppingTime < Timing.TotalTime - MaxDroppingInterval) + { + Character.IsRagdolled = true; + Character.SetInput(InputType.Ragdoll, hit: false, held: true); + droppingTimer += deltaTime; + if (droppingTimer > MaxDroppingTime) + { + lastDroppingTime = Timing.TotalTime; + } + } + else + { + droppingTimer = 0.0f; + } + } + } + } + if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) + { + Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); + } + } + Character.AnimController.IgnorePlatforms = ignorePlatforms; + } + #region Idle private void UpdateIdle(float deltaTime, bool followLastTarget = true) @@ -1229,6 +1298,8 @@ namespace Barotrauma return; } + if (Character.IsAttachedToController()) { return; } + attackWorldPos = SelectedAiTarget.WorldPosition; attackSimPos = SelectedAiTarget.SimPosition; @@ -1751,6 +1822,7 @@ namespace Barotrauma { SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority); State = AIState.Attack; + AttackLimb = null; return; } } @@ -1761,12 +1833,20 @@ namespace Barotrauma float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; if ((!canAttack || distance > margin) && !IsTryingToSteerThroughGap) { + bool useManualSteering = false; // Steer towards the target if in the same room and swimming // Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering. if (Character.CurrentHull != null && Character.Submarine != null && !Character.Submarine.Info.IsRuin && (Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + if (CanSeeTarget(targetCharacter)) + { + useManualSteering = true; + } + } + if (useManualSteering) { Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - myPos)); @@ -2311,18 +2391,49 @@ namespace Barotrauma { float prio = 1 + limb.attack.Priority; if (Character.AnimController.SimplePhysicsEnabled) { return prio; } - float dist = Vector2.Distance(limb.WorldPosition, attackPos); - float distanceFactor = 1; + float distance = Vector2.Distance(limb.WorldPosition, attackPos); + float maxDistance = Math.Max(limb.attack.Range * 3, 1000); + if (distance > maxDistance) + { + // Far enough to ignore the attack. + return 0; + } + // Not in range, but relatively close. Let's use the distance factor as a multiplier. + float distanceFactor; if (limb.attack.Ranged) { float min = 100; - distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist)); + if (distance < min) + { + // Too close -> smoothly but steeply reduce the preference (and prefer other attacks, like melee instead) + float t = MathUtils.InverseLerp(0, min, distance); + distanceFactor = MathHelper.Lerp(0.01f, 1, t * t); + } + else + { + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance)); + } } else { - // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. - // We also need a max value that is more than the actual range. - distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + if (distance <= limb.attack.Range) + { + // In range. + if (!Character.InWater) + { + // On dry land vertical distance works a bit differently, as we can't necessarily reach the target above/below us. + float verticalDistance = Math.Abs(limb.WorldPosition.Y - attackPos.Y); + if (verticalDistance > limb.attack.DamageRange) + { + // Most likely can't reach. + return 0; + } + } + // Highly prefer attacks which we can use to hit immediately. + return prio * 10; + } + float min = limb.attack.Range; + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance)); } return prio * distanceFactor; } @@ -2521,6 +2632,7 @@ namespace Barotrauma { SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority); State = AIState.Attack; + AttackLimb = null; return true; } } @@ -2555,14 +2667,10 @@ namespace Barotrauma return true; } } - if (damageTarget != null) - { - Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, user: Character); - } + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, user: Character); } } - if (damageTarget == null) { return true; } //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); if (!ActiveAttack.IsRunning) @@ -2609,10 +2717,24 @@ namespace Barotrauma } return true; } + + private const float VisibilityCheckStep = 0.2f; + private double lastVisibilityCheckTime; + private bool canSeeTarget; + /// + /// This method uses and caches the results. + /// + private bool CanSeeTarget(ISpatialEntity target) + { + if (Timing.TotalTime > lastVisibilityCheckTime + VisibilityCheckStep) + { + canSeeTarget = Character.CanSeeTarget(target); + lastVisibilityCheckTime = Timing.TotalTime; + } + return canSeeTarget; + } private float aimTimer; - private float visibilityCheckTimer; - private bool canSeeTarget; private float sinTime; private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) { @@ -2630,13 +2752,7 @@ namespace Barotrauma { Character.CursorPosition -= Character.Submarine.Position; } - visibilityCheckTimer -= deltaTime; - if (visibilityCheckTimer <= 0.0f) - { - canSeeTarget = Character.CanSeeTarget(target); - visibilityCheckTimer = 0.2f; - } - if (!canSeeTarget) + if (!CanSeeTarget(target)) { SetAimTimer(); return false; @@ -2817,7 +2933,10 @@ namespace Barotrauma } } steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3); - Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); + if (Character.AnimController.OnGround || Character.InWater) + { + Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, maxVelocity: 10.0f); + } } } else @@ -2961,7 +3080,7 @@ namespace Barotrauma { if (aiTarget.Entity.Submarine.Info.IsWreck || aiTarget.Entity.Submarine.Info.IsBeacon || - UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) + unattackableSubmarines.Contains(aiTarget.Entity.Submarine)) { continue; } @@ -3509,13 +3628,16 @@ namespace Barotrauma { if (targetCharacter.Submarine != null) { - // Target is inside -> reduce the priority - valueModifier *= 0.5f; - if (Character.Submarine != null) + if (Character.Submarine != null && !targetCharacter.Submarine.IsConnectedTo(Character.Submarine)) { - // Both inside different submarines -> can ignore safely + // Both inside different, unconnected submarines -> can ignore safely continue; } + else + { + // Target is inside a submarine that we are not -> reduce the priority + valueModifier *= 0.5f; + } } else if (Character.CurrentHull != null) { @@ -4402,6 +4524,7 @@ namespace Barotrauma { SelectTarget(doorAiTarget, CurrentTargetMemory.Priority); State = AIState.Attack; + AttackLimb = null; return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index e84df22f7..320db7e27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1380,7 +1380,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectionType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 60ad0799d..5c7447555 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Linq; using FarseerPhysics; +using System.Diagnostics; namespace Barotrauma { @@ -50,7 +51,7 @@ namespace Barotrauma } /// - /// Returns true if any node in the path is in stairs + /// Returns true if any node in the path is on stairs /// public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null); @@ -285,14 +286,17 @@ namespace Barotrauma } } - Vector2 diff = DiffToCurrentNode(); + Vector2 diff = GetDiffAndAdvance(); if (diff == Vector2.Zero) { return Vector2.Zero; } return Vector2.Normalize(diff) * weight; } protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight); - - private Vector2 DiffToCurrentNode() + + /// + /// Decides whether and when we should skip to the next node. Returns the difference to the current node (after skipping). + /// + private Vector2 GetDiffAndAdvance() { if (currentPath == null || currentPath.Unreachable) { @@ -320,26 +324,37 @@ namespace Barotrauma Reset(); return Vector2.Zero; } - Vector2 pos = host.WorldPosition; - Vector2 diff = currentPath.CurrentNode.WorldPosition - pos; + WayPoint currentNode = currentPath.CurrentNode; + WayPoint nextNode = currentPath.NextNode; + Vector2 diff = currentNode.WorldPosition - host.WorldPosition; + float horizontalDistance = Math.Abs(diff.X); + float verticalDistance = Math.Abs(diff.Y); bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; bool canClimb = character.CanClimb; Ladder currentLadder = GetCurrentLadder(); Ladder nextLadder = GetNextLadder(); - var ladders = currentLadder ?? nextLadder; + Ladder ladders = currentLadder ?? nextLadder; bool useLadders = canClimb && ladders != null; var collider = character.AnimController.Collider; - Vector2 colliderSize = collider.GetSize(); + Vector2 colliderSize = ConvertUnits.ToDisplayUnits(collider.GetSize()); + float colliderHeight = colliderSize.Y; + if (character.AnimController.CurrentAnimationParams is FishGroundedParams fishGrounded) + { + // On monsters, the main collider might be rotated, so we need to take that into account here. + float standAngle = fishGrounded.ColliderStandAngleInRadians * character.AnimController.Dir; + Vector2 transformedColliderSize = PhysicsBody.RotateVector(colliderSize, standAngle); + colliderHeight = Math.Abs(transformedColliderSize.Y); + } if (useLadders) { - if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y)) + if (character.IsClimbing && Math.Abs(diff.X) - colliderSize.X > Math.Abs(diff.Y)) { // If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders. useLadders = false; } - else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null) + else if (!character.IsClimbing && nextNode != null && nextLadder == null) { - Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos; + Vector2 diffToNextNode = nextNode.WorldPosition - host.WorldPosition; if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y)) { // If the next node is horizontally farther from us than vertically, we don't want to start climbing. @@ -356,7 +371,7 @@ namespace Barotrauma { if (currentPath.IsAtEndNode && canClimb && ladders != null) { - // Don't release the ladders when ending a path in ladders. + // Don't release the ladders when ending a path on ladders. useLadders = true; } else @@ -388,20 +403,18 @@ namespace Barotrauma if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item) { // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node. - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } else { bool nextLadderSameAsCurrent = currentLadder == nextLadder; - float colliderHeight = collider.Height / 2 + collider.Radius; - float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; - float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); + float distanceMargin = colliderSize.X; if (currentLadder != null && nextLadder != null) { //climbing ladders -> don't move horizontally diff.X = 0.0f; } - if (Math.Abs(heightDiff) < colliderHeight * 1.25f) + if (verticalDistance < colliderHeight / 2 * 1.25f) { if (nextLadder != null && !nextLadderSameAsCurrent) { @@ -410,7 +423,7 @@ namespace Barotrauma { if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } } } @@ -432,9 +445,9 @@ namespace Barotrauma } if (isAboveFloor) { - if (Math.Abs(diff.Y) < distanceMargin) + if (verticalDistance < distanceMargin) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin))) { @@ -443,14 +456,21 @@ namespace Barotrauma } } } - else if (currentLadder != null && currentPath.NextNode != null) + else if (currentLadder != null && nextNode != null) { - if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) + if (Math.Sign(currentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(nextNode.WorldPosition.Y - character.WorldPosition.Y)) { //if the current node is below the character and the next one is above (or vice versa) //and both are on ladders, we can skip directly to the next one //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above - NextNode(!doorsChecked); + return NextNode(!doorsChecked); + } + //heading towards a ladder waypoint below the character, but the next waypoint is above it on the same ladder + // -> allow skipping to that waypoint. + // Otherwise the character may get stuck trying to move to a waypoint near the floor at the bottom of the ladder, failing to get close enough because they can't move any lower. + else if (nextLadderSameAsCurrent && diff.Y < 0 && nextNode.WorldPosition.Y > currentNode.WorldPosition.Y) + { + return NextNode(!doorsChecked); } } } @@ -458,21 +478,20 @@ namespace Barotrauma else if (character.AnimController.InWater) { // Swimming - var door = currentPath.CurrentNode.ConnectedDoor; + var door = currentNode.ConnectedDoor; if (door == null || door.CanBeTraversed) { - float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); - float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f); - float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); - if (character.CurrentHull != currentPath.CurrentNode.CurrentHull) + float distanceMultiplier = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); + float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * distanceMultiplier, 0.5f); + float modifiedVerticalDist = verticalDistance; + if (character.CurrentHull != currentNode.CurrentHull) { - verticalDistance *= 2; + modifiedVerticalDist *= 2; } - float distance = horizontalDistance + verticalDistance; - if (ConvertUnits.ToSimUnits(distance) < targetDistance) + float distance = horizontalDistance + modifiedVerticalDist; + if (distance < targetDistance) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } } } @@ -480,6 +499,10 @@ namespace Barotrauma { // Walking horizontally Vector2 colliderBottom = character.AnimController.GetColliderBottom(); + if (character.Submarine != currentNode.Submarine) + { + colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, character.Submarine); + } Vector2 velocity = collider.LinearVelocity; // If the character is very short, it would fail to use the waypoint nodes because they are always too high. // If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small. @@ -487,60 +510,113 @@ namespace Barotrauma float minHeight = 1.6125001f; float minWidth = 0.3225f; // Cannot use the head position, because not all characters have head or it can be below the total height of the character - float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight); - float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); - bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight; - bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y; - var door = currentPath.CurrentNode.ConnectedDoor; - float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); - float colliderHeight = collider.Height / 2 + collider.Radius; - if (currentPath.CurrentNode.Stairs == null) + float characterHeight = Math.Max(ConvertUnits.ToSimUnits(colliderHeight) + character.AnimController.ColliderHeightFromFloor, minHeight); + bool isTargetTooHigh = currentNode.SimPosition.Y > colliderBottom.Y + characterHeight; + bool isTargetTooLow = currentNode.SimPosition.Y < colliderBottom.Y; + var door = currentNode.ConnectedDoor; + float targetDistanceMultiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); + if (currentNode.Stairs == null) { - float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; - if (heightDiff < colliderHeight) + // Only attempt dropping if the node is below the collider bottom. + // Using the next node position here, because the current node might be on the top of the ladder, which can be at the same level with the character or even above it. + bool isBelowEnough = (nextNode ?? currentNode).WorldPosition.Y < character.WorldPosition.Y - colliderHeight / 2; + bool drop = false; + if (isBelowEnough) { - // Original comment: - //the waypoint is between the top and bottom of the collider, no need to move vertically. - // Note that the waypoint can be below collider too! This might be incorrect. + if (!canClimb) + { + // Can't climb -> check if we should drop. + Door nextDoor = door ?? nextNode?.ConnectedDoor; + if (nextDoor is Door { IsHorizontal: true, CanBeTraversed: true } openHatch) + { + bool isHatchBelowCharacter = openHatch.LinkedGap.WorldPosition.Y < character.WorldPosition.Y; + if (isHatchBelowCharacter) + { + // Trying to go through an open hatch below us -> drop. + drop = true; + } + } + else if (currentLadder != null && !isTargetTooLow && nextDoor == null) + { + // On ladders -> drop. + drop = true; + } + } + } + if (drop) + { + return NextNode(!doorsChecked); + } + else if (verticalDistance < colliderHeight / 2) + { + // The waypoint is between the top and bottom of the collider, and we don't intend to drop -> no need to move vertically. diff.Y = 0.0f; } } else { - // In stairs - bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs; + // On stairs + bool isNextNodeInSameStairs = nextNode?.Stairs == currentNode.Stairs; if (!isNextNodeInSameStairs) { - margin = 1; - if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f) + targetDistanceMultiplier = 1; + if (currentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f) { isTargetTooLow = true; } + Structure nextStairs = nextNode?.Stairs; + if (character.AnimController.Stairs != null && nextStairs != null) + { + //currently on stairs, and the next node is not in the same stairs + // -> we must get off the current stairs first before we can skip to the next node, otherwise the character + // would attempt to get "through the stairs" to the next ones + if (character.AnimController.Stairs.StairDirection == Direction.Right) + { + //the direction in which the bot should keep moving depends on the direction of the stairs and whether we're going up or down + diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? Vector2.UnitX : -Vector2.UnitX; + } + else + { + diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? -Vector2.UnitX : Vector2.UnitX; + } + } } } - float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); - if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow) + // Walking horizontally, check whether we are close enough to the current node. + float targetDistance = Math.Max(colliderSize.X / 2 * targetDistanceMultiplier, ConvertUnits.ToDisplayUnits(minWidth / 2)); + Debug.Assert(targetDistance < 500, "Target distance too large (a character is trying to skip on their path to a waypoint far away), something is probably off here."); + if (!isTargetTooHigh && !isTargetTooLow && horizontalDistance < targetDistance) { - if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null)) + bool isBlockedByDoor = door is { CanBeTraversed: false }; + // If both the current ladder and the next ladder are not null, we are in the middle of ladders and should let the code above handle advancing the nodes. + // However, if either one is null, and we get here, we are probably walking to or from ladders. + bool notOnLadders = currentLadder == null || nextLadder == null; + if (!isBlockedByDoor && notOnLadders) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } } } - if (currentPath.CurrentNode == null) + return ReturnDiff(); + + Vector2 NextNode(bool checkDoors) { - return Vector2.Zero; + if (checkDoors) + { + CheckDoorsInPath(); + } + currentPath.SkipToNextNode(); + return ReturnDiff(); } - return ConvertUnits.ToSimUnits(diff); - } - - private void NextNode(bool checkDoors) - { - if (checkDoors) + + Vector2 ReturnDiff() { - CheckDoorsInPath(); + if (currentPath.CurrentNode == null) + { + return Vector2.Zero; + } + return ConvertUnits.ToSimUnits(diff); } - currentPath.SkipToNextNode(); } public bool CanAccessDoor(Door door, Func buttonFilter = null) @@ -600,8 +676,6 @@ namespace Barotrauma } } - private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()); - private float GetColliderLength() { Vector2 colliderSize = character.AnimController.Collider.GetSize(); @@ -676,7 +750,7 @@ namespace Barotrauma if (door.LinkedGap.IsHorizontal) { int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X); - float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X; + float size = character.AnimController.InWater ? colliderLength : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()).X; shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size; } else @@ -794,12 +868,17 @@ namespace Barotrauma if (character == null) { return 0.0f; } float? penalty = GetSingleNodePenalty(nextNode); if (penalty == null) { return null; } + Vector2 nextNodePosition = nextNode.Position; + if (nextNode.Waypoint.Submarine != node.Waypoint.Submarine) + { + nextNodePosition = Submarine.GetRelativeSimPosition(nextNodePosition, node.Waypoint.Submarine, nextNode.Waypoint.Submarine); + } bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y; if (!character.CanClimb && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null) { if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) || - (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up - nextNodeAboveWaterLevel)) //upper node not underwater + (nextNodePosition.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up + nextNodeAboveWaterLevel)) //upper node not underwater { return null; } @@ -830,7 +909,7 @@ namespace Barotrauma } } - float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y); + float yDist = Math.Abs(node.Position.Y - nextNodePosition.Y); if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null) { penalty += yDist * 10.0f; @@ -898,18 +977,14 @@ namespace Barotrauma //steer away from edges of the hull bool wander = false; bool inWater = character.AnimController.InWater; - Hull currentHull = character.CurrentHull; - // TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere. - // if (!inWater) - // { - // Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom()); - // if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull) - // { - // // Use the hull found at the collider bottom, if found. - // // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is. - // currentHull = lowestHull; - // } - // } + + //use the hull the legs are in (if one is found), so the character won't walk against the wall when their torso is in a different hull where there'd be room to walk further + //(e.g. if the character is in a shallow pool-type room, like in ResearchModule_01_Colony) + Hull currentHull = + character.AnimController.GetLimb(LimbType.RightLeg)?.Hull ?? + character.AnimController.GetLimb(LimbType.LeftLeg)?.Hull ?? + character.CurrentHull; + if (currentHull != null && !inWater) { float roomWidth = currentHull.Rect.Width; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index e27bf7a04..664813c08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -103,9 +103,19 @@ namespace Barotrauma } } - // For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something. - // The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority. - public bool ForceWalk { get; set; } + /// + /// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something. + /// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority. + /// + public bool ForceWalkTemporarily { get; set; } + + /// + /// Forces the character to walk when executing this objective, even if the priority is above . + /// Unlike , this value is not automatically reset. + /// + public bool ForceWalkPermanently { get; set; } + + public bool ForceWalk => ForceWalkTemporarily || ForceWalkPermanently; public bool IgnoreAtOutpost { get; set; } @@ -313,7 +323,7 @@ namespace Barotrauma /// public float CalculatePriority() { - ForceWalk = false; + ForceWalkTemporarily = false; Priority = GetPriority(); ForceHighestPriority = false; return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index b5304e13c..728645fa6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -40,7 +40,7 @@ namespace Barotrauma if (subObjectives.All(so => so.SubObjectives.None())) { // If none of the subobjectives have subobjectives, no valid container was found. Don't allow running. - ForceWalk = true; + ForceWalkTemporarily = true; } return prio; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 42e35c1b2..093cf34a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -258,13 +258,14 @@ namespace Barotrauma protected override bool CheckObjectiveState() { - if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine) + // In a friendly outpost, and the target is still in the outpost + if (character.Submarine is { Info.IsOutpost: true } && character.IsOnFriendlyTeam(character.Submarine.TeamID) && + character.Submarine == Enemy.Submarine) { - // Target still in the outpost + // Outpost guards shouldn't lose the target in friendly outposts, + // However, if we are not a guard, let's ensure that we allow the cooldown. if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity) { - // Outpost guards shouldn't lose the target in friendly outposts, - // However, if we are not a guard, let's ensure that we allow the cooldown. allowCooldown = true; } } @@ -286,7 +287,8 @@ namespace Barotrauma { allowCooldown = true; // Target not in the outpost anymore. - if (character.CanSeeTarget(Enemy)) + if (character.Submarine.IsConnectedTo(Enemy.Submarine) && + character.CanSeeTarget(Enemy)) { allowCooldown = false; coolDownTimer = DefaultCoolDown; @@ -389,7 +391,7 @@ namespace Barotrauma HumanAIController.AutoFaceMovement = false; if (!gotoObjective.ShouldRun(true)) { - ForceWalk = true; + ForceWalkTemporarily = true; } } } @@ -468,7 +470,7 @@ namespace Barotrauma isMoving = true; if (!IsEnemyClose(MeleeDistance)) { - ForceWalk = true; + ForceWalkTemporarily = true; } HumanAIController.FaceTarget(Enemy); HumanAIController.AutoFaceMovement = false; @@ -1234,7 +1236,7 @@ namespace Barotrauma } if (isAimBlocked) { - ForceWalk = true; + ForceWalkTemporarily = true; } if (!followTargetObjective.IsCloseEnough) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs index 2f0f8ec71..c3fd16668 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -95,7 +95,11 @@ namespace Barotrauma if (potentialDeconstructor?.InputContainer == null) { continue; } if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; } if (!potentialDeconstructor.Item.HasAccess(character)) { continue; } - if (Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem))) { continue; } + if (Item.Prefab.DeconstructItems.Any() && + Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem))) + { + continue; + } float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f); if (distFactor > bestDistFactor) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs index 781ed4746..391bbc4dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -64,7 +64,11 @@ namespace Barotrauma if (target == null || target.Removed) { return false; } //bots can't handle deconstructing items that require another item to deconstruct, let's not try to do that //in the vanilla game, this means unidentified genetic materials, which we don't want to "deconstruct" anyway - if (target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0)) { return false; } + if (target.Prefab.DeconstructItems.Any() && + target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0)) + { + return false; + } // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 53ec38880..0a57628a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -148,7 +148,7 @@ namespace Barotrauma character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } // Prevents running into the flames. - objectiveManager.CurrentObjective.ForceWalk = true; + objectiveManager.CurrentObjective.ForceWalkTemporarily = true; } if (moveCloser) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index af532273c..c5790c08d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -11,6 +11,8 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier(); public override bool ForceRun => true; protected override bool AllowInAnySub => true; + // Periodically clear the ignore list so that fires abandoned when fumbling with finding an extinguisher, navigating etc get reconsidered + protected override float IgnoreListClearInterval => 30; public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 94a876ca7..b2f0ae419 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -49,6 +49,13 @@ namespace Barotrauma public const float DefaultReach = 100; public const float MaxReach = 150; + /// + /// How long it takes for the objective to be abandoned if no suitable item is found. + /// Intended to be an optimization: if the bots are constantly trying to find some item (like a diving suit), + /// it can easily lead to performance issues when e.g. AIObjectiveFindDivingGear constantly starts up new GetItem objectives. + /// + private float abandonDelayIfItemNotFound = 5.0f; + /// /// Is the goal of this objective to get diving gear (i.e. has it been created by )? /// If so, the objective won't attempt to create another objective if the path requires diving gear @@ -213,7 +220,7 @@ namespace Barotrauma { if (isDoneSeeking) { - HandlePotentialItems(); + HandlePotentialItems(deltaTime); } if (objectiveManager.CurrentOrder is not AIObjectiveGoTo) { @@ -389,6 +396,8 @@ namespace Barotrauma // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character), SpeakIfFails = false, + ForceWalkTemporarily = this.ForceWalkTemporarily, + ForceWalkPermanently = this.ForceWalkPermanently, endNodeFilter = CreateEndNodeFilter(moveToTarget) }; }, @@ -598,7 +607,7 @@ namespace Barotrauma } } - private void HandlePotentialItems() + private void HandlePotentialItems(float deltaTime) { Debug.Assert(isDoneSeeking); if (itemCandidates.Any()) @@ -652,10 +661,14 @@ 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); -#endif - Abandon = true; + abandonDelayIfItemNotFound -= deltaTime; + if (abandonDelayIfItemNotFound <= 0.0f) + { + #if DEBUG + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow); + #endif + Abandon = true; + } } } } @@ -718,13 +731,15 @@ namespace Barotrauma private bool CheckItem(Item item) { + bool matchesIdentifiersOrTags = item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); + if (!matchesIdentifiersOrTags) { return false; } if (!item.HasAccess(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; } - return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); + return true; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index fd5ade494..79786bcea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -958,6 +958,7 @@ namespace Barotrauma public bool ShouldRun(bool run) { + if (ForceWalk) { return false; } if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam) { // NPCs with a wait order don't run. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index efa495d6c..1a2d14048 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -267,7 +267,10 @@ namespace Barotrauma if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; } return true; //don't stop at ladders when idling - }, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull))); + }, endNodeFilter: node => + node.Waypoint.Stairs == null && node.Waypoint.CurrentHull == currentTarget && node.Waypoint.Ladders == null && + (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull))); + if (path.Unreachable) { //can't go to this room, remove it from the list and try another room diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs index b8639dd08..6439708bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs @@ -78,9 +78,10 @@ namespace Barotrauma if (item.GetRootInventoryOwner() is Character targetCharacter && AIObjectiveFightIntruders.IsValidTarget(targetCharacter, character, targetCharactersInOtherSubs: false)) { - float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, aiTarget.SoundRange, distanceMultiplierPerClosedDoor: 2); - if (dist * HumanAIController.Hearing > aiTarget.SoundRange) { continue; } - + float range = aiTarget.SoundRange * HumanAIController.Hearing; + float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, range, distanceMultiplierPerClosedDoor: 2); + if (dist > range) { continue; } + character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f); if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index 894f27e60..65d0e110a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -112,7 +112,7 @@ namespace Barotrauma float prio = objectiveManager.GetOrderPriority(this); if (subObjectives.All(so => so.SubObjectives.None() || so.Priority <= 0)) { - ForceWalk = true; + ForceWalkTemporarily = true; } return prio; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index ad38f8cee..04a9bafa4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -257,6 +257,7 @@ namespace Barotrauma { DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget, TargetName = target.Item.Name, + ForceWalkPermanently = ForceWalk, endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item) }, onAbandon: () => Abandon = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 6c1a7a37b..7aa3d1e87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -29,7 +29,7 @@ namespace Barotrauma { if (pump?.Item == null || pump.Item.Removed) { return false; } if (pump.Item.IgnoreByAI(character)) { return false; } - if (!pump.Item.IsInteractable(character)) { return false; } + if (!pump.Item.IsInteractable(character) || !pump.CanBeSelected) { return false; } if (pump.IsAutoControlled) { return false; } if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 42cbd4de6..8963eeb34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -136,7 +136,7 @@ namespace Barotrauma public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds) { ignoredAsMinorWounds = false; - if (target == null || target.IsDead || target.Removed) { return false; } + if (target == null || target.IsDead || target.Removed || target.InvisibleTimer > 0.0f) { return false; } if (target.IsInstigator) { return false; } if (target.IsPet) { return false; } if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 879c4197f..669bc3a0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -42,7 +42,9 @@ namespace Barotrauma { enemyAi.PetBehavior?.Update(deltaTime); } - if (IsDead || IsUnconscious || Stun > 0.0f || IsIncapacitated) + if (IsDead || IsUnconscious || IsIncapacitated || + //only check "real" stuns here, ignoring ragdolling, so the AI can run and decide whether to ragdoll or unragdoll + CharacterHealth.Stun > 0.0f) { //don't enable simple physics on dead/incapacitated characters //the ragdoll controls the movement of incapacitated characters instead of the collider, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 316d3db5d..db9322c49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -685,7 +685,7 @@ namespace Barotrauma { movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f); - if (Collider.BodyType == BodyType.Dynamic) + if (Collider.BodyType == BodyType.Dynamic && onGround) { Collider.LinearVelocity = new Vector2( movement.X, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index ed100e587..8f8a4f87b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -30,6 +30,7 @@ namespace Barotrauma { public Fixture F1, F2; public Vector2 LocalNormal; + public Vector2 WorldNormal; public Vector2 Velocity; public Vector2 ImpactPos; @@ -39,7 +40,7 @@ namespace Barotrauma F2 = f2; Velocity = velocity; LocalNormal = contact.Manifold.LocalNormal; - contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2 points); + contact.GetWorldManifold(out WorldNormal, out FarseerPhysics.Common.FixedArray2 points); ImpactPos = points[0]; } } @@ -826,7 +827,7 @@ namespace Barotrauma return true; } - private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity) + private void ApplyImpact(Fixture f1, Fixture f2, Vector2 worldNormal, Vector2 impactPos, Vector2 velocity) { if (character.DisableImpactDamageTimer > 0.0f) { return; } @@ -838,7 +839,7 @@ namespace Barotrauma return; } - Vector2 normal = localNormal; + Vector2 normal = worldNormal; float impact = Vector2.Dot(velocity, -normal); if (f1.Body == Collider.FarseerBody || !Collider.Enabled) { @@ -1069,9 +1070,12 @@ namespace Barotrauma } Hull newHull = Hull.FindHull(findPos, currentHull); - if (setInWater && newHull == null) + if (setInWater) { - inWater = true; + if (newHull == null || findPos.Y < newHull.WorldSurface) + { + inWater = true; + } } if (newHull == currentHull) { return; } @@ -1114,7 +1118,10 @@ namespace Barotrauma { //don't teleport out yet if the character is going through a gap if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; } - if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; } + if (Limbs.Any(l => !l.IsSevered && Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) + { + return; + } character.MemLocalState?.Clear(); Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity); } @@ -1246,6 +1253,9 @@ namespace Barotrauma private float BodyInRestDelay = 1.0f; + /// + /// Controls the sleeping state of this character + /// public bool BodyInRest { get { return bodyInRestTimer > BodyInRestDelay; } @@ -1269,7 +1279,7 @@ namespace Barotrauma while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); - ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity); + ApplyImpact(impact.F1, impact.F2, impact.WorldNormal, impact.ImpactPos, impact.Velocity); } CheckValidity(); @@ -1312,9 +1322,18 @@ namespace Barotrauma } float MaxVel = NetConfig.MaxPhysicsBodyVelocity; - Collider.LinearVelocity = new Vector2( - NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12), - NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12)); + if (GameMain.NetworkMember != null) + { + Collider.LinearVelocity = new Vector2( + NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12), + NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12)); + } + else + { + Collider.LinearVelocity = new Vector2( + MathHelper.Clamp(Collider.LinearVelocity.X, -MaxVel, MaxVel), + MathHelper.Clamp(Collider.LinearVelocity.Y, -MaxVel, MaxVel)); + } if (forceStanding) { @@ -1368,9 +1387,19 @@ namespace Barotrauma UpdateHullFlowForces(deltaTime); - if (currentHull == null || + bool applyWaterForces = + currentHull == null || currentHull.WaterVolume > currentHull.Volume * 0.95f || - ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y) + ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y; +#if CLIENT + if (Screen.Selected is CharacterEditor.CharacterEditorScreen && + this is AnimController animController) + { + applyWaterForces = animController.CurrentAnimationParams is SwimParams; + } +#endif + + if (applyWaterForces) { Collider.ApplyWaterForces(); } @@ -1460,10 +1489,10 @@ namespace Barotrauma else { // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck. - if (Collider.LinearVelocity == Vector2.Zero && !character.IsRemotePlayer) + if (Collider.LinearVelocity == Vector2.Zero && GameMain.NetworkMember is not { IsClient: true }) { character.IsRagdolled = true; - if (character.IsBot) + if (!character.IsPlayer) { // Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots. character.SetInput(InputType.Ragdoll, hit: false, held: true); @@ -1823,7 +1852,13 @@ namespace Barotrauma { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; - if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f) + + //allow the floor to be just a bit below the bottom of the collider for the character to be "on ground" + //there is some inaccuracy in the physics simulation (and floats), the collider isn't usually precisely ColliderHeightFromFloor above the floor + const float Tolerance = 0.1f; + float standHeight = Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor; + + if (rayStart.Y - standOnFloorY <= standHeight + Tolerance) { onGround = true; if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 6fcd7a33b..8201439ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -187,6 +187,11 @@ namespace Barotrauma set => Params.Health.DoesBleed = value; } + /// + /// Can this character be contained inside a controller? + /// + public bool IsContainable { get; set; } + public readonly Dictionary Properties; public Dictionary SerializableProperties { @@ -683,6 +688,11 @@ namespace Barotrauma get { return AnimController.Mass; } } + /// + /// The position the character was at when we previously set the transforms of the items in the character's inventory. + /// + private Vector2 lastInventoryItemSetTransformPosition; + public CharacterInventory Inventory { get; private set; } /// @@ -788,7 +798,24 @@ namespace Barotrauma set { if (value == selectedCharacter) { return; } - if (selectedCharacter != null) { selectedCharacter.selectedBy = null; } + //deselect the currently selected character + if (selectedCharacter != null) + { + selectedCharacter.selectedBy = null; + //check if some other character has selected the currently selected character too, + //and set selectedBy to that other character (otherwise the currently selected character would be unaware they're still being dragged by someone) + foreach (var otherCharacter in CharacterList) + { + if (otherCharacter != this && otherCharacter.selectedCharacter == selectedCharacter) + { + selectedCharacter.selectedBy = otherCharacter; + break; + } + } + } + + CharacterHUD.RecreateHudTextsIfControlling(this); + selectedCharacter = value; if (selectedCharacter != null) { selectedCharacter.selectedBy = this; } #if CLIENT @@ -1642,8 +1669,10 @@ namespace Barotrauma AnimController.FindHull(setInWater: true); if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; } + IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f); + CharacterList.Add(this); - + Enabled = GameMain.NetworkMember == null; if (info != null) @@ -2268,6 +2297,12 @@ namespace Barotrauma } } + // Try to detach from the controller if we are currently attached to something that is dangerous for our character + if (aiControlled && Stun <= 0f && !IsKnockedDownOrRagdolled && !LockHands && ShouldAvoidStayingAttachedToController()) + { + SelectedItem = null; + } + if (GameMain.NetworkMember != null) { if (GameMain.NetworkMember.IsServer) @@ -2316,7 +2351,7 @@ namespace Barotrauma { attackCoolDown -= deltaTime; } - else if (IsKeyDown(InputType.Attack)) + else if (IsKeyDown(InputType.Attack) && !IsAttachedToController()) { //normally the attack target, where to aim the attack and such is handled by EnemyAIController, //but in the case of player-controlled monsters, we handle it here @@ -2843,14 +2878,14 @@ namespace Barotrauma #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; } #endif - if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; } - Controller controller = item.GetComponent(); if (controller != null && IsAnySelectedItem(item) && controller.IsAttachedUser(this)) { return true; } + if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; } + if (item.ParentInventory != null) { return CanAccessInventory(item.ParentInventory); @@ -2972,7 +3007,9 @@ namespace Barotrauma } } - if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) + //note that the distance to item should be set to 0 above if the character is within the item's bounding box + bool closeEnoughToIgnoreVisibilityCheck = distanceToItem <= 0.1f; + if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger && !closeEnoughToIgnoreVisibilityCheck) { var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); bool itemCenterVisible = CheckBody(body, item); @@ -3001,7 +3038,6 @@ namespace Barotrauma { return itemCenterVisible; } - } return true; @@ -3091,7 +3127,11 @@ namespace Barotrauma if (!CanInteract) { - SelectedItem = SelectedSecondaryItem = null; + if (!IsAttachedToController()) + { + SelectedItem = null; + } + SelectedSecondaryItem = null; focusedItem = null; if (!AllowInput) { @@ -3110,8 +3150,16 @@ namespace Barotrauma { if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld) { - FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; - if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; } + //don't allow focusing on anyone when the health window is open (avoids accidentally selecting someone when closing the window) + if (CharacterHealth.OpenHealthWindow != null) + { + FocusedCharacter = null; + } + else + { + FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; + if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; } + } float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { @@ -3435,7 +3483,7 @@ namespace Barotrauma obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f); - if (Inventory != null) + if (Inventory != null && Vector2.DistanceSquared(lastInventoryItemSetTransformPosition, Position) > 0.1f) { //do not check for duplicates: this is code is called very frequently, and duplicates don't matter here, //so it's better just to avoid the relatively expensive duplicate check @@ -3444,6 +3492,7 @@ namespace Barotrauma if (item.body == null || item.body.Enabled) { continue; } item.SetTransform(SimPosition, 0.0f, forceSubmarine: Submarine); } + lastInventoryItemSetTransformPosition = Position; } HideFace = false; @@ -3570,7 +3619,7 @@ namespace Barotrauma { wasRagdolled = IsRagdolled; IsRagdolled = IsKeyDown(InputType.Ragdoll); - if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true }) + if (IsRagdolled && !IsPlayer && GameMain.NetworkMember is not { IsClient: true }) { ClearInput(InputType.Ragdoll); } @@ -3622,7 +3671,19 @@ namespace Barotrauma AnimController.IgnorePlatforms = true; } AnimController.ResetPullJoints(); - SelectedItem = SelectedSecondaryItem = null; + + // Prevent us from detaching from the controller if we are attached to it OR detach if we + // manually ragdoll, in this case it should be similar to us deselecting the controller + if (!IsAttachedToController() || + (IsKeyDown(InputType.Ragdoll) + // Let only the server do this check since the Ragdoll input for other clients is set to be held + // for stunned characters even if a character isn't manually ragdolling + && (GameMain.NetworkMember == null || GameMain.NetworkMember is { IsServer: true } ))) + { + SelectedItem = null; + } + + SelectedSecondaryItem = null; SelectedCharacter = null; return; } @@ -3651,6 +3712,13 @@ namespace Barotrauma bool MustDeselect(Item item) { if (item == null) { return false; } + + // Prevent creatures from deselecting the controller if they are attached to it + if (IsAIControlled && !CanInteract && IsAttachedToController()) + { + return false; + } + if (!CanInteractWith(item)) { return true; } bool hasSelectableComponent = false; foreach (var component in item.Components) @@ -4376,6 +4444,41 @@ namespace Barotrauma } } + public void ForceSay(LocalizedString messageToSay, bool sayInRadio, bool removeQuotes = false, float delay = 0.0f) + { + if (messageToSay.IsNullOrEmpty() || SpeechImpediment >= 100.0f || IsDead) + { + return; + } + + if (removeQuotes) + { + messageToSay = new TrimLString(messageToSay, + TrimLString.Mode.Both, ['"', '”', '“', ' ']); + } + + ChatMessageType messageType = ChatMessageType.Default; + bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio); + if (canUseRadio && sayInRadio) + { + messageType = ChatMessageType.Radio; + } + + CoroutineManager.Invoke(() => + { +#if SERVER + GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, this); +#elif CLIENT + // no need to create the message when playing as a client, the server will send it to us + if (GameMain.Client == null) + { + AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType); + SendSinglePlayerMessage(message, canUseRadio, radio); + } +#endif + }, delay); + } + public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) { CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount); @@ -4760,6 +4863,10 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } if (Screen.Selected != GameMain.GameScreen) { return; } + //don't allow stunning for less than one frame + //fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun, + //because even a one-frame stun briefly disables the animations and makes the character stop + if (newStun < Timing.Step && Stun <= 0.0f) { return; } if (GodMode) { CharacterHealth.Stun = 0; @@ -4787,7 +4894,12 @@ namespace Barotrauma CharacterHealth.Stun = newStun; if (newStun > 0.0f) { - SelectedItem = SelectedSecondaryItem = null; + if (!IsAttachedToController()) + { + SelectedItem = null; + } + + SelectedSecondaryItem = null; if (SelectedCharacter != null) { DeselectCharacter(); } } HealthUpdateInterval = 0.0f; @@ -4976,6 +5088,37 @@ namespace Barotrauma } } + public bool IsAttachedToController() + { + if (SelectedItem == null) { return false; } + + var controller = SelectedItem.GetComponent(); + if (controller == null) { return false; } + + return controller.IsAttachedUser(this); + } + + public bool ShouldAvoidStayingAttachedToController() + { + if (!IsAttachedToController()) { return false; } + + var deconstructor = SelectedItem.GetComponent(); + if (deconstructor != null) + { + return true; + } + + // Character is being carried by an enemy! + if (IsHuman && + SelectedItem.GetRootInventoryOwner() is Character carryingCharacter && + TeamID != carryingCharacter.TeamID) + { + return true; + } + + return false; + } + public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; } @@ -5113,7 +5256,7 @@ namespace Barotrauma AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; - if (!LockHands) + if (!LockHands && causeOfDeath != CauseOfDeathType.Disconnected) { foreach (Item heldItem in HeldItems.ToList()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 93e4b1bf6..68110740b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index cb36b1196..8008179fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -629,7 +629,7 @@ namespace Barotrauma public static readonly Identifier StunType = "stun".ToIdentifier(); public static readonly Identifier EMPType = "emp".ToIdentifier(); public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); - public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); + public static readonly Identifier AlienInfectionType = "alieninfection".ToIdentifier(); public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); public static readonly Identifier DisguisedAsHuskType = "disguiseashusk".ToIdentifier(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index f72b31c43..ed5415fe6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -827,9 +827,21 @@ namespace Barotrauma } } + float modifiedStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType)); + if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) + { + //don't allow stunning for less than one frame + //fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun, + //because even a one-frame stun briefly disables the animations and makes the character stop + if (modifiedStrength < Timing.Step && Stun <= 0.0f) + { + return; + } + } + if (existingAffliction != null) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType)); + float newStrength = modifiedStrength; if (allowStacking) { // Add the existing strength @@ -851,7 +863,7 @@ namespace Barotrauma //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, limbType))), + Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength), newAffliction.Source); afflictions.Add(copyAffliction, limbHealth); AchievementManager.OnAfflictionReceived(copyAffliction, Character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 67b0141e4..a8ce33b63 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -190,7 +190,7 @@ namespace Barotrauma idleObjective.PreferredOutpostModuleTypes.Add(moduleType); } } - humanAI.ReportRange = Hearing; + humanAI.Hearing = Hearing; humanAI.ReportRange = ReportRange; humanAI.FindWeaponsRange = FindWeaponsRange; humanAI.AimSpeed = AimSpeed; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index d9226ce75..e9f1d2f0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1293,7 +1293,7 @@ namespace Barotrauma if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } foreach (StatusEffect statusEffect in statusEffectList) { - if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { return; } + if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { continue; } statusEffect.sourceBody = body; if (statusEffect.type == ActionType.OnDamaged) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 14c8e83c8..675faa777 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -728,7 +728,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "Should the character target or ignore walls when it's outside the submarine."), Editable] public bool TargetOuterWalls { get; private set; } - [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "If disabled (default), the character selects the limb based on a formula where the parameters are a) the priority of the attack b) the distance to the target, and c) the range of the attack" + + "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random. The distance to the target is in this case ignored." + ), Editable] public bool RandomAttack { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs index e589401bc..99b619886 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -77,8 +77,8 @@ namespace Barotrauma } else { - conn.SetLabel(conn.Connection.DisplayName, this); conn.Connection.DisplayNameOverride = null; + conn.SetLabel(conn.Connection.DisplayName, this); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index e608d06ba..0e67a57ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -106,9 +106,10 @@ namespace Barotrauma void AddTexturePath(string path) { if (string.IsNullOrEmpty(path)) { return; } + var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture); //if the path contains a gender variable, we can't load it yet because we don't know which gender we need - if (path.Contains("[GENDER]")) { return; } - texturePaths.Add(ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture)); + if (contentPath.FullPath.Contains("[GENDER]")) { return; } + texturePaths.Add(contentPath); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 2bee7b3c1..12967f125 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -199,9 +199,16 @@ namespace Barotrauma try { - return success(doc.Root.GetAttributeBool("corepackage", false) + ContentPackage contentPackage = doc.Root.GetAttributeBool("corepackage", false) ? new CorePackage(doc, path) - : new RegularPackage(doc, path)); + : new RegularPackage(doc, path); + + if (System.IO.Path.GetFileNameWithoutExtension(path)?.Any(char.IsUpper) is true) + { + DebugConsole.ThrowError($"Invalid filename casing. Please rename \"filelist.xml\" so it is entirely lowercase.", contentPackage: contentPackage); + } + + return success(contentPackage); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 8dc9bb60d..c70f3a767 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -3019,7 +3019,10 @@ namespace Barotrauma switch (args[argIndex].ToLowerInvariant()) { case "inside": - spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub); + spawnPoint = + WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub) ?? + //try a non-job-specific spawnpoint if a job-specific one can't be found + WayPoint.GetRandom(SpawnType.Human, assignedJob: null, Submarine.MainSub); break; case "outside": spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs index b42f1131c..281022315 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs @@ -34,11 +34,12 @@ namespace Barotrauma get { return Prefab.LifeTime; } } + private float baseAlpha = 1.0f; public float BaseAlpha { - get; - set; - } = 1.0f; + get => baseAlpha; + set => baseAlpha = MathHelper.Clamp(value, 0f, 1f); + } public Color Color { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 79a25913a..3f2cb74c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -131,6 +131,14 @@ namespace Barotrauma /// OnRemoved = 25, /// + /// Executes continuously while the item/character is being deconstructed. + /// + OnDeconstructing = 26, + /// + /// Executed once when the item/character is deconstructed. + /// + OnDeconstructed = 27, + /// /// Executes when the character dies. Only valid for characters. /// OnDeath = OnBroken diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 08abda9ed..1c2cc3a87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -12,7 +12,11 @@ namespace Barotrauma public readonly int RandomSeed; protected readonly EventPrefab prefab; - + +#nullable enable + public Mission? TriggeringMission; +#nullable restore + public EventPrefab Prefab => prefab; public EventSet ParentSet { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index 04d517c2f..b1391bd64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -29,6 +29,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target (or all targets if there's multiple) when the check succeeds.")] public Identifier ApplyTagToTarget { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Should the check fail if no targets matching the specified tag are found?")] + public bool FailIfTargetNotFound { get; set; } + public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (TargetTag.IsEmpty) @@ -79,11 +82,10 @@ namespace Barotrauma if (targets.None()) { - DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventDebugName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.", - contentPackage: ParentEvent.Prefab.ContentPackage); + return !FailIfTargetNotFound; } - if (targets.None() || Conditionals.None()) + if (Conditionals.None()) { foreach (var target in targets) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index f6ff09a03..bc40eab16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -14,6 +14,33 @@ namespace Barotrauma /// partial class ConversationAction : EventAction { + public class OptionActionGroup : SubactionGroup + { + [Serialize("", IsPropertySaveable.Yes, description: "The text to display in the option.")] + public string Text { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should this option end the conversation (closing the conversation prompt?). " + + "By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation. " + + "But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, " + + "and by default leaves it open.")] + public bool EndConversation { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the player will send the {nameof(Text)} in chat when selecting the option, or if {nameof(ForceSayText)} is not empty, will send that instead.")] + public bool ForceSay { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat will be sent in radio chat instead.")] + public bool ForceSayInRadio { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat, if empty, {nameof(Text)} is used instead.")] + public string ForceSayText { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")] + public bool ForceSayRemoveQuotes { get; set; } + + public OptionActionGroup(ScriptedEvent scriptedEvent, ContentXElement element) : base(scriptedEvent, element) + { + } + } public enum DialogTypes { @@ -33,6 +60,18 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")] public string Text { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the speaker will send the {nameof(Text)} in chat, or if {nameof(ForceSayText)} is not empty, will send that instead. Note: requires a valid SpeakerTag to be defined.")] + public bool ForceSay { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat by the speaker will be sent in radio chat instead.")] + public bool ForceSayInRadio { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat by the speaker, if empty, {nameof(Text)} is used instead.")] + public string ForceSayText { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")] + public bool ForceSayRemoveQuotes { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")] public Identifier SpeakerTag { get; set; } @@ -75,7 +114,7 @@ namespace Barotrauma private AIObjective prevIdleObjective, prevGotoObjective; private AIObjective npcWaitObjective; - public List Options { get; private set; } + public List Options { get; private set; } public SubactionGroup Interrupted { get; private set; } @@ -99,12 +138,12 @@ namespace Barotrauma { actionCount++; Identifier = actionCount; - Options = new List(); + Options = new List(); foreach (var elem in element.Elements()) { if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase)) { - Options.Add(new SubactionGroup(ParentEvent, elem)); + Options.Add(new OptionActionGroup(ParentEvent, elem)); } else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase)) { @@ -215,6 +254,10 @@ namespace Barotrauma interrupt = false; dialogOpened = false; Speaker = null; +#if CLIENT + dialogBox?.Close(); + dialogBox = null; +#endif } /// @@ -292,6 +335,7 @@ namespace Barotrauma if (dialogOpened) { lastActiveTime = Timing.TotalTime; + #if CLIENT if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction")) { @@ -350,7 +394,7 @@ namespace Barotrauma } else { - TryStartConversation(null); + TryStartConversation(Speaker); } } else @@ -467,11 +511,26 @@ namespace Barotrauma ParentEvent.AddTarget(InvokerTag, targetCharacter); } - ShowDialog(speaker, targetCharacter); + if (ForceSay) + { + speaker?.ForceSay( + ForceSayText.IsNullOrEmpty() ? TextManager.Get(Text).Fallback(Text) : TextManager.Get(ForceSayText).Fallback(ForceSayText), + ForceSayInRadio, + ForceSayRemoveQuotes, + // Small delay so the speaking character doesn't talk at the same time as the player + delay: 0.7f); + } + + ShowDialog(Speaker, targetCharacter); dialogOpened = true; - if (speaker != null) + if (Speaker != null) { + Speaker = speaker; + + // Set the Speaker of the child conversation actions so they know which character is speaking + Options.SelectMany(static op => op.Actions).OfType().ForEach(action => action.Speaker = speaker); + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; speaker.SetCustomInteract(null, null); #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs index 13d3b6859..ba10a1f7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -99,6 +99,7 @@ namespace Barotrauma else { int compareToTargetCount = ParentEvent.GetTargets(CompareToTarget).Count(); + if (compareToTargetCount == 0) { return false; } float percentage = MathUtils.Percentage(targetCount, compareToTargetCount); if (MinPercentageRelativeToTarget > -1 && percentage < MinPercentageRelativeToTarget) { return false; } if (MaxPercentageRelativeToTarget > -1 && percentage > MaxPercentageRelativeToTarget) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index f1a54e742..bf96cb286 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -9,14 +9,7 @@ namespace Barotrauma { public class SubactionGroup { - public string Text; public List Actions; - /// - /// Should this option end the conversation (closing the conversation prompt?). By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation. - /// But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, and by default leaves it open. - /// - public bool EndConversation; - private int currentSubAction = 0; public EventAction CurrentSubAction @@ -31,17 +24,17 @@ namespace Barotrauma } } - public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem) + public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement element) { - Text = elem.GetAttribute("text")?.Value ?? ""; + SerializableProperty.DeserializeProperties(this, element); + Actions = new List(); - EndConversation = elem.GetAttributeBool("endconversation", false); - foreach (var e in elem.Elements()) + foreach (var e in element.Elements()) { if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { - DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.", - contentPackage: elem.ContentPackage); + DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.", + contentPackage: element.ContentPackage); continue; } var action = Instantiate(scriptedEvent, e); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ForceSayAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ForceSayAction.cs new file mode 100644 index 000000000..1e5b933a2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ForceSayAction.cs @@ -0,0 +1,62 @@ +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Forces a specific character to say a message in chat. + /// + class ForceSayAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character that should say the message.")] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "The message that the character should say. Can be the text as-is, or a tag referring to a line in a text file.")] + public string Message { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should the message that the character says be sent in radio?")] + public bool SayInRadio { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the message be stripped of any quotation mark characters?")] + public bool RemoveQuotes { get; set; } + + public ForceSayAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + var targets = ParentEvent.GetTargets(TargetTag); + + LocalizedString messageToSay = TextManager.Get(Message).Fallback(Message); + foreach (var target in targets) + { + if (target != null && target is Character character) + { + character.ForceSay(messageToSay, SayInRadio, RemoveQuotes); + } + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ForceSayAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + $"Message: {Message})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs index a6dd612be..92ddc1ee3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -1,84 +1,77 @@ -namespace Barotrauma +#nullable enable +namespace Barotrauma; + +/// Changes the state of missions. The way the states are used depends on the type of mission. +internal sealed class MissionStateAction : EventAction { - - /// - /// Changes the state of a specific active mission. The way the states are used depends on the type of mission. - /// - class MissionStateAction : EventAction + /// The operation to perform on missions' states. + public enum OperationType { - [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission whose state to change.")] - public Identifier MissionIdentifier { get; set; } + /// Sets the missions' states to . + Set, + /// Adds to the missions' states. + Add + } - public enum OperationType + [Serialize("", IsPropertySaveable.Yes, "Identifiers of the missions whose states to change. Leave blank to only set the state of the mission that triggered the parent event.")] + public Identifier MissionIdentifier { get; set; } + + [Serialize(OperationType.Set, IsPropertySaveable.Yes, "The operation to perform on missions' states.")] + public OperationType Operation { get; set; } + + [Serialize(0, IsPropertySaveable.Yes, "The value to apply to missions' states.")] + public int State { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, "If set to true, missions are forced to fail without a chance of retrying them.")] + public bool ForceFailure { get; set; } + + public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + State = element.GetAttributeInt("value", State); + if (Operation == OperationType.Add && State == 0 && !ForceFailure) { - Set, - Add + DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to only add 0 to the mission state, which will do nothing.", + contentPackage: element.ContentPackage); } + } - [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Should the value be added to the state of the mission, or should the state be set to the specified value.")] - public OperationType Operation { get; set; } + private bool isFinished; + public override bool IsFinished(ref string goTo) => isFinished; + public override void Reset() => isFinished = false; - [Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")] - public int State { get; set; } + public override void Update(float deltaTime) + { + if (isFinished) { return; } - [Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the mission is forced to fail without a chance of retrying it.")] - public bool ForceFailure { get; set; } - - private bool isFinished; - - public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + if (!MissionIdentifier.IsEmpty) { - State = element.GetAttributeInt("value", State); - if (MissionIdentifier.IsEmpty) - { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.", - contentPackage: element.ContentPackage); - } - if (Operation == OperationType.Add && State == 0 && !ForceFailure) - { - DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.", - contentPackage: element.ContentPackage); - } - } - - public override bool IsFinished(ref string goTo) - { - return isFinished; - } - public override void Reset() - { - isFinished = false; - } - - public override void Update(float deltaTime) - { - if (isFinished) { return; } - foreach (Mission mission in GameMain.GameSession.Missions) { if (mission.Prefab.Identifier != MissionIdentifier) { continue; } - if (ForceFailure) - { - mission.ForceFailure = true; - } - - switch (Operation) - { - case OperationType.Set: - mission.State = State; - break; - case OperationType.Add: - mission.State += State; - break; - } + SetMissionState(mission); } - - isFinished = true; + } + else if (ParentEvent.TriggeringMission != null) + { + SetMissionState(ParentEvent.TriggeringMission); } - public override string ToDebugString() + isFinished = true; + } + + private void SetMissionState(Mission mission) + { + if (ForceFailure) { mission.ForceFailure = true; } + switch (Operation) { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})"; + case OperationType.Set: + mission.State = State; + break; + case OperationType.Add: + mission.State += State; + break; } } + + public override string ToDebugString() => $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})"; } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 968e8c988..ed8fa1590 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -18,6 +18,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop following the target?")] public bool Follow { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be forced to walk towards the target?")] + public bool ForceWalk { get; set; } + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs to target (e.g. you could choose to only make a specific number of security officers follow the player.)")] public int MaxTargets { get; set; } @@ -65,7 +68,8 @@ namespace Barotrauma var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true) { OverridePriority = Priority, - IsFollowOrder = true + IsFollowOrder = true, + ForceWalkPermanently = ForceWalk }; humanAiController.ObjectiveManager.AddObjective(newObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 3b28cd186..6accd385e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -271,6 +271,10 @@ namespace Barotrauma ParentEvent.AddTarget(TargetTag, newCharacter); } spawnedEntity = newCharacter; + if (newCharacter is { AIController: EnemyAIController enemyAi, Submarine: Submarine ownSub }) + { + enemyAi.SetUnattackableSubmarines(ownSub); + } }); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 01009f1db..195ff9fe6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -240,42 +240,45 @@ namespace Barotrauma CreateEvents(eventSet); } - if (level?.LevelData != null) + bool isOutpostLevel = level?.LevelData is { Type: LevelData.LevelType.Outpost } || + (GameMain.GameSession?.GameMode is TestGameMode && Submarine.MainSub?.Info?.Type == SubmarineType.Outpost); + if (isOutpostLevel) { - if (level.LevelData.Type == LevelData.LevelType.Outpost) + //if the outpost is connected to a locked connection, create an event to unlock it + if (level?.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { - //if the outpost is connected to a locked connection, create an event to unlock it - if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) + var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); + if (unlockPathEventPrefab != null) { - var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); - if (unlockPathEventPrefab != null) + var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); + activeEvents.Add(newEvent); + } + else + { + //if no event that unlocks the path can be found, unlock it automatically + level.StartLocation.Connections.ForEach(c => c.Locked = false); + } + } + Submarine outpost = level?.StartOutpost ?? Submarine.MainSub; + if (GameMain.NetworkMember is not { IsClient: true } && outpost != null) + { + foreach (var eventTag in outpost.Info.TriggerOutpostMissionEvents) + { + EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, outpost.ContentPackage); + if (eventPrefab == null) { - var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); - activeEvents.Add(newEvent); + DebugConsole.ThrowError($"Outpost {outpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: outpost.ContentPackage); } else { - //if no event that unlocks the path can be found, unlock it automatically - level.StartLocation.Connections.ForEach(c => c.Locked = false); + var newEvent = eventPrefab.CreateInstance(RandomSeed); + ActivateEvent(newEvent); } } - if (GameMain.NetworkMember is not { IsClient: true } && level.StartOutpost != null) - { - foreach (var eventTag in level.StartOutpost.Info.TriggerOutpostMissionEvents) - { - EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, level.StartOutpost.ContentPackage); - if (eventPrefab == null) - { - DebugConsole.ThrowError($"Outpost {level.StartOutpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: level.StartOutpost.ContentPackage); - } - else - { - var newEvent = eventPrefab.CreateInstance(RandomSeed); - ActivateEvent(newEvent); - } - } - } - } + } + } + if (level?.LevelData != null) + { RegisterNonRepeatableChildEvents(initialEventSet); void RegisterNonRepeatableChildEvents(EventSet eventSet) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 09703cf05..16d635b69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -233,7 +233,7 @@ namespace Barotrauma } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return State > 0 && State != HostagesKilledState; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index f9b102a87..70fc86b00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -171,7 +171,7 @@ namespace Barotrauma #endif } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return level.CheckBeaconActive(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index f62efa2ef..4768696f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -331,7 +331,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 64bf201c7..6b45f0ef9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -204,7 +204,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return Winner != CharacterTeamType.None; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CustomMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CustomMission.cs new file mode 100644 index 000000000..d18349dbd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CustomMission.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace Barotrauma; + +/// +/// Defines a mission where the success and failure are determined solely by its state. +/// Intended to be used alongside . +/// +internal sealed partial class CustomMission(MissionPrefab prefab, Location[] locations, Submarine sub) : Mission(prefab, locations, sub) +{ + public readonly int SuccessState = prefab.ConfigElement.GetAttributeInt(nameof(SuccessState), +1); + public readonly int FailureState = prefab.ConfigElement.GetAttributeInt(nameof(FailureState), -1); + + public bool RequireDestinationReached = prefab.ConfigElement.GetAttributeBool(nameof(RequireDestinationReached), false); + + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => + State == SuccessState && + (!RequireDestinationReached || transitionType is CampaignMode.TransitionType.ProgressToNextLocation or CampaignMode.TransitionType.ProgressToNextEmptyLocation); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs index cc7855701..1bfeaea1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs @@ -1,4 +1,4 @@ -using System; +using System; using Barotrauma.Extensions; using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; @@ -199,7 +199,7 @@ namespace Barotrauma private static bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead; - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index 1ffc09b93..e68af0349 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; @@ -301,7 +301,7 @@ namespace Barotrauma partial void OnStateChangedProjSpecific(); - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return Phase == MissionPhase.BossKilled; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 473006b60..3a770fffa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -343,7 +343,7 @@ namespace Barotrauma return character != null && !character.Removed && !character.IsDead; } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs index a1924db58..c35e7b789 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -17,7 +17,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (Level.Loaded?.Type == LevelData.LevelType.Outpost) { @@ -25,7 +25,7 @@ namespace Barotrauma } else { - return Submarine.MainSub is { AtEndExit: true }; + return transitionType == CampaignMode.TransitionType.ProgressToNextLocation; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 10d174170..d75e61fa6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; @@ -47,6 +47,12 @@ namespace Barotrauma } } + /// + /// Minerals spawned by the mission. Note that minerals that were already present in the level may have also been used as targets. + /// Each list of items represents a separate cluster of minerals. + /// + public IEnumerable> SpawnedResources => spawnedResources.Values; + public override LocalizedString SuccessMessage => ModifyMessage(base.SuccessMessage); public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage); public override LocalizedString Description => ModifyMessage(description); @@ -169,7 +175,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return EnoughHaveBeenCollected(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index e6629022b..6dcfd314f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -401,14 +401,9 @@ namespace Barotrauma { characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); } - if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi) + if (spawnedCharacter.AIController is EnemyAIController enemyAi && submarine != null) { - enemyAi.UnattackableSubmarines.Add(submarine); - enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); - foreach (Submarine sub in Submarine.MainSub.DockedTo) - { - enemyAi.UnattackableSubmarines.Add(sub); - } + enemyAi.SetUnattackableSubmarines(submarine); } InitCharacter(spawnedCharacter, element); return spawnedCharacter; @@ -532,6 +527,7 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null) { var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); + newEvent.TriggeringMission = this; GameMain.GameSession.EventManager.ActivateEvent(newEvent); } } @@ -539,13 +535,13 @@ namespace Barotrauma /// /// End the mission and give a reward if it was completed successfully /// - public void End() + public void End(CampaignMode.TransitionType transitionType) { if (GameMain.NetworkMember is not { IsClient: true }) { completed = !ForceFailure && - DetermineCompleted() && + DetermineCompleted(transitionType) && (completeCheckDataAction == null || completeCheckDataAction.GetSuccess()); } if (completed) @@ -578,7 +574,7 @@ namespace Barotrauma } } - protected abstract bool DetermineCompleted(); + protected abstract bool DetermineCompleted(CampaignMode.TransitionType transitionType); protected virtual void EndMissionSpecific(bool completed) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index cd32310eb..28c5e4a26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -30,7 +30,8 @@ namespace Barotrauma { "GoTo".ToIdentifier(), typeof(GoToMission) }, { "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) }, { "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) }, - { "End".ToIdentifier(), typeof(EndMission) } + { "End".ToIdentifier(), typeof(EndMission) }, + { "Custom".ToIdentifier(), typeof(CustomMission) } }; /// @@ -64,6 +65,7 @@ namespace Barotrauma public Type MissionClass { get; private set; } + public bool CampaignOnly { get; private set; } public bool MultiplayerOnly { get; private set; } public bool SingleplayerOnly { get; private set; } @@ -319,8 +321,9 @@ namespace Barotrauma SonarIconIdentifier = ConfigElement.GetAttributeIdentifier("sonaricon", ""); - MultiplayerOnly = ConfigElement.GetAttributeBool("multiplayeronly", false); - SingleplayerOnly = ConfigElement.GetAttributeBool("singleplayeronly", false); + CampaignOnly = ConfigElement.GetAttributeBool(nameof(CampaignOnly), false); + MultiplayerOnly = ConfigElement.GetAttributeBool(nameof(MultiplayerOnly), false); + SingleplayerOnly = ConfigElement.GetAttributeBool(nameof(SingleplayerOnly), false); AchievementIdentifier = ConfigElement.GetAttributeIdentifier("achievementidentifier", ""); @@ -543,7 +546,7 @@ namespace Barotrauma } /// - /// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission + /// Returns all mission types that can be selected in the server lobby, excluding any special, hidden ones like EndMission /// (the mission at the end of the campaign) /// public static IEnumerable GetAllMultiplayerSelectableMissionTypes() @@ -552,6 +555,7 @@ namespace Barotrauma foreach (var missionPrefab in Prefabs) { if (missionPrefab.Commonness <= 0.0f) { continue; } + if (missionPrefab.CampaignOnly) { continue; } if (missionPrefab.SingleplayerOnly) { continue; } if (HiddenMissionTypes.Contains(missionPrefab.Type)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 941b7c4dd..656764b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -242,7 +242,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return state > 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 3ad3effe1..41dbb356a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -337,7 +337,7 @@ namespace Barotrauma return true; } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return AllItemsDestroyedOrRetrieved(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 6331311af..6d7f895ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -547,7 +547,7 @@ namespace Barotrauma return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated; } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return state == 2; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 602e4dd0d..e0a7aa4af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -715,7 +715,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (requiredDeliveryAmount < 1.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index e388b2908..a16c556fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -1,4 +1,4 @@ -using System; +using System; using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.RuinGeneration; @@ -17,7 +17,7 @@ namespace Barotrauma private readonly Dictionary parentInventoryIDs = new Dictionary(); private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); - private readonly int targetsToScan; + private readonly int totalTargetsToScan; private readonly Dictionary scanTargets = new Dictionary(); private readonly HashSet newTargetsScanned = new HashSet(); private readonly float minTargetDistance; @@ -44,7 +44,7 @@ namespace Barotrauma public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { itemConfig = prefab.ConfigElement.GetChildElement("Items"); - targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); + totalTargetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f); } @@ -77,57 +77,60 @@ namespace Barotrauma var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false); ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null); - if (ruinWaypoints.Count < targetsToScan) + if (ruinWaypoints.Count < totalTargetsToScan) { - DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})", + DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {totalTargetsToScan})", contentPackage: Prefab.ContentPackage); return; } + + //the distance we'll use if we otherwise fail to place the targets far enough from each other + //(smallest extent should be large enough to fit the targets and one extra to be safe) + float guaranteedDistance = Math.Min(TargetRuin.Area.Width, TargetRuin.Area.Height) / (totalTargetsToScan + 1); + var availableWaypoints = new List(); - float minTargetDistanceSquared = minTargetDistance * minTargetDistance; - for (int tries = 0; tries < 15; tries++) + const int MaxTries = 15; + for (int tries = 0; tries < MaxTries; tries++) { + float triesNormalized = tries / (float)(MaxTries - 1); // 0.0 -> 1.0 + float desperationFactor = MathF.Pow(triesNormalized, 2); + //try placing the targets the desired minimum distance apart, gradually lowering the distance requirement on each try + float currentMinDistance = MathHelper.Lerp(minTargetDistance, guaranteedDistance, desperationFactor); + float currentMinDistanceSquared = currentMinDistance * currentMinDistance; + scanTargets.Clear(); availableWaypoints.Clear(); availableWaypoints.AddRange(ruinWaypoints); - for (int i = 0; i < targetsToScan; i++) + for (int i = 0; i < totalTargetsToScan; i++) { var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient); scanTargets.Add(selectedWaypoint, false); availableWaypoints.Remove(selectedWaypoint); - if (i < (targetsToScan - 1)) + if (i < (totalTargetsToScan - 1)) { availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull); - availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared); + availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < currentMinDistanceSquared); if (availableWaypoints.None()) { #if DEBUG - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})", contentPackage: Prefab.ContentPackage); #endif break; } } } - if (scanTargets.Count >= targetsToScan) + if (scanTargets.Count >= totalTargetsToScan) { #if DEBUG DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green); #endif break; } - if ((tries + 1) % 5 == 0) - { - float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance; - minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance; -#if DEBUG - DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow); -#endif - } } - if (scanTargets.Count < targetsToScan) + if (scanTargets.Count < totalTargetsToScan) { - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})", contentPackage: Prefab.ContentPackage); } } @@ -241,9 +244,9 @@ namespace Barotrauma State = Math.Max(State, scanTargets.Count(kvp => kvp.Value)); } - private bool AllTargetsScanned() => State >= targetsToScan; + private bool AllTargetsScanned() => State >= totalTargetsToScan; - protected override bool DetermineCompleted() => AllTargetsScanned(); + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => AllTargetsScanned(); protected override void EndMissionSpecific(bool completed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index 129fa3d25..6edea3673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -104,7 +104,7 @@ namespace Barotrauma return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 } ? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex - : throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics"); + : throw new Exception("Could not retrieve Steam authentication ticket, possibly due to connection issues. GameAnalytics logging will be disabled."); } private static async Task GetEOSAuthTicket() @@ -215,9 +215,8 @@ namespace Barotrauma IRestResponse response; try { - var client = new RestClient(consentServerUrl); - - var request = new RestRequest(consentServerFile, Method.GET); + var client = RestFactory.CreateClient(consentServerUrl); + var request = RestFactory.CreateRequest(consentServerFile); request.AddParameter("authticket", authTicket.Token); if (consent == Consent.Ask) { @@ -321,7 +320,7 @@ namespace Barotrauma RestClient client; try { - client = new RestClient(consentServerUrl); + client = RestFactory.CreateClient(consentServerUrl); } catch (Exception e) { @@ -329,7 +328,7 @@ namespace Barotrauma return Consent.Error; } - var request = new RestRequest(consentServerFile, Method.GET); + var request = RestFactory.CreateRequest(consentServerFile); request.AddParameter("authticket", authTicket.Token); request.AddParameter("action", "getconsent"); request.AddParameter("request_version", RemoteRequestVersion); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index daa20659a..ae18a5a21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1017,7 +1017,7 @@ namespace Barotrauma UpdateStoreStock(); } - GameMain.GameSession.EndMissions(); + GameMain.GameSession.EndMissions(TransitionType.None); GameMain.GameSession.EventManager?.StoreEventDataAtRoundEnd(registerFinishedOnly: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index e00bf540b..d19250e5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -53,6 +53,7 @@ namespace Barotrauma { foreach (MissionPrefab missionPrefab in missionPrefabs) { + if (missionPrefab.CampaignOnly) { continue; } if (!missionClasses.ContainsValue(missionPrefab.MissionClass)) { throw new InvalidOperationException($"Cannot start gamemode with a {missionPrefab.MissionClass} mission."); @@ -68,7 +69,7 @@ namespace Barotrauma { return missionTypes.Where(type => MissionPrefab.Prefabs.OrderBy(missionPrefab => missionPrefab.UintIdentifier) - .Any(missionPrefab => missionPrefab.Type == type && missionClasses.ContainsValue(missionPrefab.MissionClass))); + .Any(missionPrefab => missionPrefab.Type == type && !missionPrefab.CampaignOnly && missionClasses.ContainsValue(missionPrefab.MissionClass))); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 79e38a2f7..5174ef002 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1,16 +1,17 @@ #nullable enable +using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Barotrauma.PerkBehaviors; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; -using Barotrauma.Extensions; -using Barotrauma.PerkBehaviors; namespace Barotrauma { @@ -952,14 +953,6 @@ namespace Barotrauma sub.SetPosition(spawnPos); myPort.Dock(outPostPort); myPort.Lock(isNetworkMessage: true, applyEffects: false); - foreach (var item in sub.GetItems(alsoFromConnectedSubs: true)) - { - //need to refresh position to maintain since the sub was moved to the docking port - if (item.GetComponent() is { MaintainPos: true } steering) - { - steering.RefreshPosToMaintain(); - } - } } else { @@ -982,6 +975,16 @@ namespace Barotrauma sub.EnableMaintainPosition(); } + foreach (var item in sub.GetItems(alsoFromConnectedSubs: true)) + { + // Refresh pos to maintain in all steering components maintaining + // position, including ones in shuttles, since the submarines moved + if (item.GetComponent() is { MaintainPos: true } steering) + { + steering.RefreshPosToMaintain(); + } + } + // Make sure that linked subs which are NOT docked to the main sub // (but still close enough to NOT be considered as 'left behind') // are also moved to keep their relative position to the main sub @@ -1094,7 +1097,7 @@ namespace Barotrauma ImmutableHashSet crewCharacters = GetSessionCrewCharacters(CharacterType.Both); int prevMoney = GetAmountOfMoney(crewCharacters); - EndMissions(); + EndMissions(transitionType); foreach (Character character in crewCharacters) { @@ -1197,12 +1200,12 @@ namespace Barotrauma } } - public void EndMissions() + public void EndMissions(CampaignMode.TransitionType transitionType) { ImmutableHashSet crewCharacters = GetSessionCrewCharacters(CharacterType.Both); foreach (Mission mission in missions) { - mission.End(); + mission.End(transitionType); } if (missions.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 98b9bd60a..146f6b603 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -43,8 +43,12 @@ namespace Barotrauma public InvSlotType[] SlotTypes { get; - private set; } + + /// + /// Optimization for fast access of by . + /// + private readonly Dictionary> slotsByType = []; public static readonly List AnySlot = new List { InvSlotType.Any }; public static readonly List BagSlot = new List { InvSlotType.Bag }; @@ -106,9 +110,20 @@ namespace Barotrauma case InvSlotType.RightHand: slots[i].HideIfEmpty = true; break; - } + } } - + + for (int i = 0; i < capacity; i++) + { + InvSlotType slotType = SlotTypes[i]; + if (!slotsByType.TryGetValue(slotType, out List slotList)) + { + slotList = []; + slotsByType[SlotTypes[i]] = slotList; + } + slotList.Add(slots[i]); + } + InitProjSpecific(element); var itemElements = element.Elements().Where(e => e.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)); @@ -198,39 +213,55 @@ namespace Barotrauma public Item GetItemInLimbSlot(InvSlotType limbSlot) { - for (int i = 0; i < slots.Length; i++) + if (slotsByType.TryGetValue(limbSlot, out List slotList)) { - if (SlotTypes[i] == limbSlot) { return slots[i].FirstOrDefault(); } + return slotList.First().FirstOrDefault(); } return null; } + public IEnumerable GetItemsInLimbSlot(InvSlotType limbSlot) + { + if (slotsByType.TryGetValue(limbSlot, out List slotList)) + { + foreach (var slot in slotList) + { + foreach (Item item in slot.Items) + { + yield return item; + } + } + } + } public bool IsInLimbSlot(Item item, InvSlotType limbSlot) { if (limbSlot == (InvSlotType.LeftHand | InvSlotType.RightHand)) { - int rightHandSlot = FindLimbSlot(InvSlotType.RightHand); - int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand); - if (rightHandSlot > -1 && slots[rightHandSlot].Contains(item) && - leftHandSlot > -1 && slots[leftHandSlot].Contains(item)) + if (GetItemsInLimbSlot(InvSlotType.RightHand).Contains(item) && + GetItemsInLimbSlot(InvSlotType.LeftHand).Contains(item)) { return true; } } - - for (int i = 0; i < slots.Length; i++) + else if (slotsByType.TryGetValue(limbSlot, out List slotList)) { - if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; } + foreach (ItemSlot slot in slotList) + { + if (slot.Contains(item)) { return true; } + } } return false; } public bool IsSlotEmpty(InvSlotType limbSlot) { - for (int i = 0; i < slots.Length; i++) + if (slotsByType.TryGetValue(limbSlot, out List slotList)) { - if (SlotTypes[i] == limbSlot && slots[i].Empty()) { return true; } + foreach (ItemSlot slot in slotList) + { + if (slot.Empty()) { return true; } + } } return false; } @@ -370,7 +401,7 @@ namespace Barotrauma /// /// If there is room, puts the item in the inventory and returns true, otherwise returns false /// - public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { if (allowedSlots == null || !allowedSlots.Any()) { return false; } if (item == null) @@ -494,8 +525,6 @@ namespace Barotrauma return placedInSlot > -1; } - - public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1; private int GetFreeAnySlot(Item item, bool inWrongSlot) @@ -542,7 +571,7 @@ namespace Barotrauma return -1; } - public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { if (index < 0 || index >= slots.Length) { @@ -590,9 +619,9 @@ namespace Barotrauma return TryPutItem(item, user, new List() { placeToSlots }, createNetworkEvent, ignoreCondition); } - protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) + protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true, bool triggerOnInsertedEffects = true) { - base.PutItem(item, i, user, removeItem, createNetworkEvent); + base.PutItem(item, i, user, removeItem, createNetworkEvent, triggerOnInsertedEffects); #if CLIENT CreateSlots(); if (character == Character.Controlled) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index dacf4103a..75155b00c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -739,6 +739,12 @@ namespace Barotrauma.Items.Components { picker.Inventory.FlashAllowedSlots(item, Color.Red); } + else + { + //normally this would be done in the base.OnPicked method, but clients don't call it, + //but instead rely on the server telling them to put the item in the inventory + SoundPlayer.PlayUISound(GUISoundType.PickItem); + } return false; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index d27ca9cd6..a94b6f7ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -347,18 +347,9 @@ namespace Barotrauma.Items.Components } else if (f2.Body.UserData is Character targetCharacter) { - if (targetCharacter == picker || targetCharacter == User) { return false; } - if (targetCharacter.IgnoreMeleeWeapons) { return false; } - if (HitFriendlyTarget(targetCharacter)) { return false; } - if (AllowHitMultiple) - { - if (hitTargets.Contains(targetCharacter)) { return false; } - } - else - { - if (hitTargets.Any(t => t is Character)) { return false; } - } - hitTargets.Add(targetCharacter); + //only allow hitting limbs, not the main collider + //otherwise it's difficult to make certain parts of the ragdoll not take hits by making them ignore collisions or melee weapons + return false; } else if (!HitOnlyCharacters) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 58d4ddc3c..75976e215 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -378,7 +378,7 @@ namespace Barotrauma.Items.Components break; case "requireditem": case "requireditems": - SetRequiredItems(subElement); + SetRequiredItems(subElement, allowEmpty: true); break; case "requiredskill": case "requiredskills": @@ -1100,6 +1100,9 @@ namespace Barotrauma.Items.Components foreach (RelatedItem ri in DisabledRequiredItems) { XElement newElement = new XElement("requireditem"); + //if we have some actual requirements, no need to keep the empty requirement + //as a "placeholder" for the user to add requirements in the sub editor + if (ri.Identifiers.IsEmpty && RequiredItems.Any()) { continue; } ri.Save(newElement); componentElement.Add(newElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index ae7016391..561fa6716 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components } } - [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position where the contained items get drawn at. In the case of items with a physics body, the offset is from the center of the body, on items without one from the top-left corner of the sprite. In pixels.")] public Vector2 ItemPos { get; set; } [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] @@ -425,7 +425,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(ContentXElement element); - public void OnItemContained(Item containedItem) + public void OnItemContained(Item containedItem, bool triggerOnInsertedEffects = true) { int index = Inventory.FindIndex(containedItem); RelatedItem relatedItem = null; @@ -444,14 +444,20 @@ namespace Barotrauma.Items.Components ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath); activeContainedItems.Add(activeContainedItem); - if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems || - containedItem.OnInsertedEffectsApplied) - { - continue; + if (triggerOnInsertedEffects) + { + if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems || + containedItem.OnInsertedEffectsApplied) + { + continue; + } + activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets); } - activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets); } - containedItem.OnInsertedEffectsApplied = true; + if (triggerOnInsertedEffects) + { + containedItem.OnInsertedEffectsApplied = true; + } } } } @@ -1087,21 +1093,25 @@ namespace Barotrauma.Items.Components { if (item.body == null) { + //if the item is a holdable item currently attached to a wall (i.e. normally has a physics body, but the body is now disabled), + //we must position the contained items using the center as the origin since the item positions have been configured with the assumption the item has a body + bool isAttachedHoldable = item.GetComponent() is { Attached: true }; + bool useCenterAsOrigin = isAttachedHoldable; if (flippedX) { transformedItemPos.X = -transformedItemPos.X; - transformedItemPos.X += item.Rect.Width; + if (!useCenterAsOrigin) { transformedItemPos.X += item.Rect.Width; } transformedItemInterval.X = -transformedItemInterval.X; transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; } if (flippedY) { transformedItemPos.Y = -transformedItemPos.Y; - transformedItemPos.Y -= item.Rect.Height; + if (!useCenterAsOrigin) { transformedItemPos.Y -= item.Rect.Height; } transformedItemInterval.Y = -transformedItemInterval.Y; transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y; } - transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); + transformedItemPos += useCenterAsOrigin ? item.Position : new Vector2(item.Rect.X, item.Rect.Y); if (drawPosition) { if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs new file mode 100644 index 000000000..6faa6db36 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs @@ -0,0 +1,121 @@ +#nullable enable + +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + /// + /// Item component used by for keeping a reference to the character that is currently + /// selecting the controller. Also provides functionality for changing the inventory sprite of the item based on the linked character. + /// + partial class LinkedControllerCharacterComponent : ItemComponent, IServerSerializable + { +#if CLIENT + private class SpriteOverride + { + public readonly Sprite? Sprite; + public readonly Identifier SpeciesName; + public readonly Identifier SpeciesGroup; + public SpriteOverride(ContentXElement element) + { + if (element.GetChildElement("Sprite") is ContentXElement spriteElement) + { + Sprite = new Sprite(spriteElement); + } + SpeciesName = element.GetAttributeIdentifier("speciesname", Identifier.Empty); + SpeciesGroup = element.GetAttributeIdentifier("speciesgroup", Identifier.Empty); + } + } + + private readonly ImmutableArray spriteOverrides; +#endif + + [Serialize(0.5f, IsPropertySaveable.No, description: $"Maximum value which {nameof(DeconstructTimeMultiplier)} can be.")] + public float MaxDeconstructTimeMultiplier + { + get; + set; + } + + public Character? Character { get; private set; } + + public bool DoesBleed => Character?.DoesBleed == true; + + public float DeconstructTimeMultiplier { get; private set; } = 1f; + + public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element) + { +#if CLIENT + spriteOverrides = element.Elements() + .Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride") + .Select(static e => new SpriteOverride(e)) + .ToImmutableArray(); +#endif + } + + public void UpdateLinkedCharacter(Character? character) + { + Character = character; + + if (character != null) + { + var animController = character.AnimController; + float totalLimbs = animController.Limbs.Length; + float nonSeveredLimbs = animController.Limbs.Count(static l => !l.IsSevered); + + // Decrease deconstruction time if the character is missing some limbs + DeconstructTimeMultiplier *= MathF.Max(MaxDeconstructTimeMultiplier, nonSeveredLimbs / totalLimbs); + } + +#if CLIENT + if (character != null) + { + SpriteOverride? spriteOverride = + spriteOverrides.Where(s => s.SpeciesName == character.SpeciesName).FirstOrDefault() + ?? spriteOverrides.Where(s => s.SpeciesGroup == character.Group).FirstOrDefault(); + + if (spriteOverride != null) + { + item.OverrideInventorySprite = spriteOverride.Sprite; + } + } + else + { + item.OverrideInventorySprite = null; + } +#elif SERVER + Item.CreateServerEvent(this); +#endif + } + + public void ClientEventRead(IReadMessage msg, float sendingTime) + { + UInt16 characterId = msg.ReadUInt16(); + if (characterId == Entity.NullEntityID) + { + UpdateLinkedCharacter(null); + } + else if (Entity.FindEntityByID(characterId) is Character character) + { + UpdateLinkedCharacter(character); + } + } + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) + { + if (Character != null) + { + msg.WriteUInt16(Character.ID); + } + else + { + msg.WriteUInt16(Entity.NullEntityID); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 899258e4f..b9c3222ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -1,9 +1,12 @@ -using FarseerPhysics; -using Barotrauma.Networking; +using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -45,6 +48,39 @@ namespace Barotrauma.Items.Components private Camera cam; private Character user; + public Character User + { + get { return user; } + private set + { + if (user == value) + { + return; + } + + user = value; + + if (user != null) + { + teleportTransition = 0f; + teleportStartPosition = user.WorldPosition; + } +#if SERVER + item.CreateServerEvent(this); +#endif + +#if CLIENT + UpdateMsg(); + + if (HideAllItemComponentHUDs && Character.Controlled == user) + { + // Prevents any UIs in this item from briefly showing up when you select this controller, since + // activeHUDs would take a single frame to be updated to not contain any other item component HUD + Item.ClearActiveHUDs(); + } +#endif + } + } private Item focusTarget; private float targetRotation; @@ -55,11 +91,6 @@ namespace Barotrauma.Items.Components set { userPos = value; } } - public Character User - { - get { return user; } - } - public IEnumerable LimbPositions { get { return limbPositions; } } [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)] @@ -123,6 +154,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the HUDs of all item components in this item be hidden when a character is using this controller.")] + public bool HideAllItemComponentHUDs + { + get; + set; + } + public enum UseEnvironment { Air, Water, Both @@ -152,6 +190,49 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Can a character put another character into this controller by dragging them and selecting this controller?")] + public bool AllowPuttingInOtherCharacters + { + get; + set; + } + + [Serialize(true, IsPropertySaveable.No, description: "Can a character select this controller by themselves?")] + public bool CanBeSelectedByCharacters + { + get; + set; + } + + [Serialize(false, IsPropertySaveable.No, description: "If a character selects this controller, but another character already has it selected, should it be kicked out?")] + public bool SelectingKicksCharacterOut + { + get; + set; + } + + [Serialize("", IsPropertySaveable.No, description: "Message displayed when there's a character inside this controller.")] + public string KickOutCharacterMsg + { + get; + set; + } + + [Serialize("", IsPropertySaveable.No, description: "Message displayed when you are putting a character into the controller.")] + public string PutOtherCharacterMsg + { + get; + set; + } + + + [Serialize("", IsPropertySaveable.No, description: "Spawns this item in the first available item container slot when a character selects this controller, if the item container is full, the character will not be able to select the controller.")] + public Identifier SpawnItemOnSelected + { + get; + private set; + } + public bool ControlCharacterPose { get { return limbPositions.Count > 0; } @@ -204,6 +285,23 @@ namespace Barotrauma.Items.Components set; } + /// + /// Used to determine how fast the character is teleported + /// to the item when they first select the controller. + /// Only relevant for + /// + private const float TeleportTransitionSpeed = 8f; + private float teleportTransition = 0f; + private Vector2 teleportStartPosition; + + private readonly ItemPrefab spawnItemOnSelectedPrefab; + private readonly ItemContainer containerToSpawnOnSelectedItem; + + /// + /// Item spawned by + /// + private Item spawnedItemOnSelected = null; + public Controller(Item item, ContentXElement element) : base(item, element) { @@ -211,6 +309,18 @@ namespace Barotrauma.Items.Components Enum.TryParse(element.GetAttributeString("direction", "None"), out dir); LoadLimbPositions(element); IsActive = true; + + containerToSpawnOnSelectedItem = item.GetComponent(); + + if (!SpawnItemOnSelected.IsEmpty && !ItemPrefab.Prefabs.TryGet(SpawnItemOnSelected, out spawnItemOnSelectedPrefab)) + { + DebugConsole.ThrowError($"Failed to find item prefab \"{SpawnItemOnSelected}\""); + } + + if (containerToSpawnOnSelectedItem == null && !SpawnItemOnSelected.IsEmpty) + { + DebugConsole.ThrowError($"Error - Controller has a {nameof(SpawnItemOnSelected)} but no ItemContainer defined"); + } } /// @@ -236,58 +346,77 @@ namespace Barotrauma.Items.Components item.SendSignal(signal, "trigger_out"); } - if (forceSelectNextFrame && user != null) + if (forceSelectNextFrame && User != null) { - user.SelectedItem = item; + User.SelectedItem = item; } forceSelectNextFrame = false; userCanInteractCheckTimer -= deltaTime; - if (user == null - || user.Removed - || !user.IsAnySelectedItem(item) - || (item.ParentInventory != null && !IsAttachedUser(user)) - || (UsableIn == UseEnvironment.Water && !user.AnimController.InWater) - || (UsableIn == UseEnvironment.Air && user.AnimController.InWater) - || !CheckUserCanInteract()) + if (User == null + || User.Removed + || (((User.Stun <= 0f && !User.IsKnockedDownOrRagdolled && !User.LockHands) || !ForceUserToStayAttached) && (!User.IsAnySelectedItem(item) || !CheckUserCanInteract())) + || (item.ParentInventory != null && !IsAttachedUser(User)) + || (UsableIn == UseEnvironment.Water && !User.AnimController.InWater) + || (UsableIn == UseEnvironment.Air && User.AnimController.InWater) + || !CheckSpawnItem() + ) { - if (user != null) + if (User != null) { - CancelUsing(user); - user = null; + CancelUsing(User); + User = null; } if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; } return; } - if (ForceUserToStayAttached && Vector2.DistanceSquared(item.WorldPosition, user.WorldPosition) > 0.1f) + if (ForceUserToStayAttached) { - user.TeleportTo(item.WorldPosition); - user.AnimController.Collider.ResetDynamics(); - foreach (var limb in user.AnimController.Limbs) + teleportTransition = MathF.Min(teleportTransition + deltaTime * TeleportTransitionSpeed, 1f); + + if (teleportTransition >= 1f) { - if (limb.Removed || limb.IsSevered) { continue; } - limb.body?.ResetDynamics(); + // Transition is complete, if someone was holding this character, force them to deselect + // so they aren't holding the character that is now attached to the controller + if (User.SelectedBy != null) + { + User.SelectedBy.SelectedCharacter = null; + } + } + + if (User == Character.Controlled + || teleportTransition < 1f + || Vector2.DistanceSquared(item.WorldPosition, User.WorldPosition) > 0.1f) + { + var targetPosition = Vector2.Lerp(teleportStartPosition, item.WorldPosition, teleportTransition); + User.TeleportTo(targetPosition); + User.AnimController.Collider.ResetDynamics(); + foreach (var limb in User.AnimController.Limbs) + { + if (limb.Removed || limb.IsSevered) { continue; } + limb.body?.ResetDynamics(); + } } } - user.AnimController.StartUsingItem(); + User.AnimController.StartUsingItem(); if (userPos != Vector2.Zero) { - Vector2 diff = (item.WorldPosition + userPos) - user.WorldPosition; + Vector2 diff = (item.WorldPosition + userPos) - User.WorldPosition; - if (user.AnimController.InWater) + if (User.AnimController.InWater) { if (diff.LengthSquared() > 30.0f * 30.0f) { - user.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One); - user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; + User.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One); + User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; } else { - user.AnimController.TargetMovement = Vector2.Zero; + User.AnimController.TargetMovement = Vector2.Zero; UserInCorrectPosition = true; } } @@ -295,10 +424,10 @@ namespace Barotrauma.Items.Components { // Secondary items (like ladders or chairs) will control the character position over primary items // Only control the character position if the character doesn't have another secondary item already controlling it - if (!user.HasSelectedAnotherSecondaryItem(Item)) + if (!User.HasSelectedAnotherSecondaryItem(Item)) { diff.Y = 0.0f; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && User != Character.Controlled) { if (Math.Abs(diff.X) > 20.0f) { @@ -308,48 +437,48 @@ namespace Barotrauma.Items.Components else if (Math.Abs(diff.X) > 0.1f) { //aim to keep the collider at the correct position once close enough - user.AnimController.Collider.LinearVelocity = new Vector2( + User.AnimController.Collider.LinearVelocity = new Vector2( diff.X * 0.1f, - user.AnimController.Collider.LinearVelocity.Y); + User.AnimController.Collider.LinearVelocity.Y); } } else if (Math.Abs(diff.X) > 10.0f) { - user.AnimController.TargetMovement = Vector2.Normalize(diff); - user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; + User.AnimController.TargetMovement = Vector2.Normalize(diff); + User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; return; } - user.AnimController.TargetMovement = Vector2.Zero; + User.AnimController.TargetMovement = Vector2.Zero; } UserInCorrectPosition = true; } } - ApplyStatusEffects(ActionType.OnActive, deltaTime, user); + ApplyStatusEffects(ActionType.OnActive, deltaTime, User); if (limbPositions.Count == 0) { return; } - user.AnimController.StartUsingItem(); + User.AnimController.StartUsingItem(); - if (user.SelectedItem != null) + if (User.SelectedItem != null) { - user.AnimController.ResetPullJoints(l => l.IsLowerBody); + User.AnimController.ResetPullJoints(l => l.IsLowerBody); } else { - user.AnimController.ResetPullJoints(); + User.AnimController.ResetPullJoints(); } - if (dir != 0) { user.AnimController.TargetDir = dir; } + if (dir != 0) { User.AnimController.TargetDir = dir; } foreach (LimbPos lb in limbPositions) { - Limb limb = user.AnimController.GetLimb(lb.LimbType); + Limb limb = User.AnimController.GetLimb(lb.LimbType); if (limb == null || !limb.body.Enabled) { continue; } // Don't move lower body limbs if there's another selected secondary item that should control them - if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; } + if (limb.IsLowerBody && User.HasSelectedAnotherSecondaryItem(Item)) { continue; } // Don't move hands if there's a selected primary item that should control them - if (limb.IsArm && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; } + if (limb.IsArm && Item == User.SelectedSecondaryItem && User.SelectedItem != null) { continue; } if (lb.AllowUsingLimb) { switch (lb.LimbType) @@ -357,12 +486,12 @@ namespace Barotrauma.Items.Components case LimbType.RightHand: case LimbType.RightForearm: case LimbType.RightArm: - if (user.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; } + if (User.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; } break; case LimbType.LeftHand: case LimbType.LeftForearm: case LimbType.LeftArm: - if (user.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; } + if (User.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; } break; } } @@ -374,15 +503,77 @@ namespace Barotrauma.Items.Components } } + private bool IsSpawnContainerFull() + { + if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null) + { + return false; + } + + if (containerToSpawnOnSelectedItem.Inventory.IsFull()) + { + return true; + } + + return false; + } + + private bool CheckSpawnItem() + { + if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null) + { + return true; + } + + if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab)) + { + return true; + } + + if (spawnedItemOnSelected != null && !spawnedItemOnSelected.Removed) + { + if (spawnedItemOnSelected.ParentInventory != containerToSpawnOnSelectedItem.Inventory) + { + // Item was moved or dropped, force the user in this controller out + return false; + } + else + { + return true; + } + } + + if (IsSpawnContainerFull()) + { + return false; + } + + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + Entity.Spawner.AddItemToSpawnQueue(spawnItemOnSelectedPrefab, containerToSpawnOnSelectedItem.Inventory, onSpawned: spawnedItem => + { + spawnedItemOnSelected = spawnedItem; + + var linkedCharacterComponent = spawnedItem.GetComponent(); + if (linkedCharacterComponent != null) + { + linkedCharacterComponent.UpdateLinkedCharacter(User); + } + }); + } + + return true; + } + private bool CheckUserCanInteract() { //optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame - if (user != null) + if (User != null) { if (userCanInteractCheckTimer <= 0.0f) { userCanInteractCheckTimer = UserCanInteractCheckInterval; - return user.CanInteractWith(item); + return User.CanInteractWith(item); } } //we only do the actual check every UserCanInteractCheckInterval seconds @@ -394,13 +585,13 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character activator = null) { - if (activator != user) + if (activator != User) { return false; } - if (user == null || user.Removed || !user.IsAnySelectedItem(item) || !user.CanInteractWith(item)) + if (User == null || User.Removed || !User.IsAnySelectedItem(item) || !User.CanInteractWith(item)) { - user = null; + User = null; return false; } @@ -419,7 +610,7 @@ namespace Barotrauma.Items.Components } else if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal(output, sender: user), "trigger_out"); + item.SendSignal(new Signal(output, sender: User), "trigger_out"); } lastUsed = Timing.TotalTime; @@ -428,13 +619,13 @@ namespace Barotrauma.Items.Components public override bool SecondaryUse(float deltaTime, Character character = null) { - if (user != character) + if (User != character) { return false; } - if (user == null || character.Removed || !user.IsAnySelectedItem(item) || !character.CanInteractWith(item)) + if (User == null || character.Removed || !User.IsAnySelectedItem(item) || !character.CanInteractWith(item)) { - user = null; + User = null; return false; } if (character == null) @@ -495,7 +686,7 @@ namespace Barotrauma.Items.Components if (IsOutOfPower()) { return null; } - item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut); + item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: User), positionOut); for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) { @@ -547,8 +738,20 @@ namespace Barotrauma.Items.Components private void CancelUsing(Character character) { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + if (spawnedItemOnSelected != null) + { + Entity.Spawner.AddEntityToRemoveQueue(spawnedItemOnSelected); + spawnedItemOnSelected = null; + } + } + if (character == null || character.Removed) { return; } + // Wake character's colliders so they don't just float in the air when taken out of the controller + character.AnimController.BodyInRest = false; + foreach (LimbPos lb in limbPositions) { Limb limb = character.AnimController.GetLimb(lb.LimbType); @@ -588,31 +791,84 @@ namespace Barotrauma.Items.Components return false; } - //someone already using the item - if (user != null && !user.Removed) + // Someone already using the item + if (User != null && !User.Removed) { - if (user == activator) + // Let the server handle the logic here + if (GameMain.NetworkMember is { IsClient: true }) { - IsActive = false; - CancelUsing(user); - user = null; return false; } - else if (user.IsBot && !activator.IsBot) + + // Prevent user from kicking character out if they are holding another character + if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter)) + { + return false; + } + + if (User == activator || SelectingKicksCharacterOut) + { +#if SERVER + if (User != activator) + { + GameServer.Log($"{GameServer.CharacterLogName(activator)} removed {GameServer.CharacterLogName(User)} from {item.Name}", + ServerLog.MessageType.Attack); + } +#endif + + IsActive = false; + CancelUsing(User); + User = null; + return false; + } + else if (User.IsBot && !activator.IsBot) { if (AllowSelectingWhenSelectedByBot) { - CancelUsing(user); - user = activator; + CancelUsing(User); + User = activator; IsActive = true; return true; } } return AllowSelectingWhenSelectedByOther; } - else + else if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter)) { - user = activator; + // Stun pets longer so they don't immediately get out of the controller + if (activator.SelectedCharacter.IsPet) + { + activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 4f), isNetworkMessage: true); + } + else + { + // Small amount of stun since non-ragdolled characters behave weirdly when syncing the periodic teleportation in multiplayer + activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 1f), isNetworkMessage: true); + } + +#if SERVER + GameServer.Log($"{GameServer.CharacterLogName(activator)} forced {GameServer.CharacterLogName(activator.SelectedCharacter)} into {item.Name}", + ServerLog.MessageType.Attack); +#endif + + User = activator.SelectedCharacter; + User.SelectedItem = this.Item; + IsActive = true; + if (ForceUserToStayAttached && item.Container != null) + { + forceSelectNextFrame = true; + } + return false; + } + else if (CanBeSelectedByCharacters) + { +#if SERVER + GameServer.Log($"{GameServer.CharacterLogName(activator)} entered {item.Name}", ServerLog.MessageType.ItemInteraction); +#endif + + activator.DeselectCharacter(); + + User = activator; IsActive = true; if (ForceUserToStayAttached && item.Container != null) { @@ -621,15 +877,12 @@ namespace Barotrauma.Items.Components } } - //allow the selection logic above to run when out of power, but allow sending signals + //allow the selection logic above to run when out of power, but disallow sending signals if (IsOutOfPower()) { return false; } - -#if SERVER - item.CreateServerEvent(this); -#endif + if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal(output, sender: user), "signal_out"); + item.SendSignal(new Signal(output, sender: User), "signal_out"); } return true; } @@ -639,7 +892,7 @@ namespace Barotrauma.Items.Components /// public bool IsAttachedUser(Character character) { - return character != null && character == user && ForceUserToStayAttached; + return character != null && character == User && ForceUserToStayAttached; } public override void FlipX(bool relativeToSub) @@ -668,12 +921,87 @@ namespace Barotrauma.Items.Components } } + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) + { +#if CLIENT + UpdateMsg(); +#endif + + bool canPutCharacter = AllowPuttingInOtherCharacters && CanPutSelectedCharacter(character.SelectedCharacter, addMessage); + bool canKickCharacter = SelectingKicksCharacterOut && User != null && !User.Removed; + bool canUseController = CanBeSelectedByCharacters; + + // Prevent kicking a character out when the user is holding another character to put into the controller. + // This avoids accidentally taking out a character (e.g. from a deconstructor). + if (canPutCharacter && canKickCharacter) + { +#if CLIENT + if (addMessage) + { + GUI.AddMessage(TextManager.Get("ItemMsgAlreadyHasCharacterFail"), Color.Red, playSound: false); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } +#endif + + return false; + } + + if (!canKickCharacter && !canPutCharacter && !canUseController) + { + return false; + } + + if (IsSpawnContainerFull()) + { +#if CLIENT + if (addMessage) + { + GUI.AddMessage(TextManager.Get("ItemMsgNotEnoughSpaceCharacterFail"), Color.Red, playSound: false); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } +#endif + + return false; + } + + return base.HasRequiredItems(character, addMessage, msg); + } + public override bool HasAccess(Character character) { if (!item.IsInteractable(character)) { return false; } return base.HasAccess(character); } + private bool CanPutSelectedCharacter(Character character, bool showMessage = false) + { + if (character == null) + { + return false; + } + + if (!character.IsContainable) + { +#if CLIENT + if (showMessage) + { + GUI.AddMessage(TextManager.Get("ItemMsgPutCharacterFail"), Color.Red); + } +#endif + + return false; + } + + if (character.IsKnockedDownOrRagdolled) { return true; } + if (character.LockHands) { return true; } + if (character.IsPet) + { + return true; + } + + return false; + } + partial void HideHUDs(bool value); public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 2ab676500..c6cb73978 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -114,7 +114,19 @@ namespace Barotrauma.Items.Components float deconstructTime = 0.0f; foreach (Item targetItem in inputContainer.Inventory.AllItems) { - deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier); + float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost } + ? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime; + float targetDeconstructTime = itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier); + + var linkedCharacter = targetItem.GetComponent(); + if (linkedCharacter != null) + { + targetDeconstructTime *= linkedCharacter.DeconstructTimeMultiplier; + } + + deconstructTime += targetDeconstructTime; + + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime); } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); @@ -143,8 +155,21 @@ namespace Barotrauma.Items.Components var targetItem = inputContainer.Inventory.LastOrDefault(); if (targetItem == null) { return; } + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime); + var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList(); - float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + + float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost } + ? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime; + + float deconstructTime = !targetItem.Prefab.DeconstructItems.Any() || validDeconstructItems.Any() + ? itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + + var linkedCharacter = targetItem.GetComponent(); + if (linkedCharacter != null) + { + deconstructTime *= linkedCharacter.DeconstructTimeMultiplier; + } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) @@ -183,6 +208,8 @@ namespace Barotrauma.Items.Components amountMultiplier = (int)itemCreationMultiplier.Value; } + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f); + if (targetItem.Prefab.RandomDeconstructionOutput) { int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; @@ -297,30 +324,8 @@ namespace Barotrauma.Items.Components { humanAi.HandleRelocation(spawnedItem); } - for (int i = 0; i < outputContainer.Capacity; i++) - { - var containedItem = outputContainer.Inventory.GetItemAt(i); - bool combined = false; - if (containedItem?.OwnInventory != null) - { - foreach (Item subItem in containedItem.ContainedItems.ToList()) - { - if (subItem.Combine(spawnedItem, null)) - { - combined = true; - break; - } - } - } - if (!combined) - { - if (containedItem?.Combine(spawnedItem, null) ?? false) - { - break; - } - } - } - PutItemsToLinkedContainer(); + + TryMoveItemToOutputContainers(spawnedItem); }); } } @@ -347,6 +352,7 @@ namespace Barotrauma.Items.Components } } } + inputContainer.Inventory.RemoveItem(targetItem); Entity.Spawner.AddItemToRemoveQueue(targetItem); MoveInputQueue(); @@ -381,6 +387,34 @@ namespace Barotrauma.Items.Components } } + private void TryMoveItemToOutputContainers(Item spawnedItem) + { + for (int i = 0; i < outputContainer.Capacity; i++) + { + var containedItem = outputContainer.Inventory.GetItemAt(i); + bool combined = false; + if (containedItem?.OwnInventory != null) + { + foreach (Item subItem in containedItem.ContainedItems.ToList()) + { + if (subItem.Combine(spawnedItem, null)) + { + combined = true; + break; + } + } + } + if (!combined) + { + if (containedItem?.Combine(spawnedItem, null) ?? false) + { + break; + } + } + } + PutItemsToLinkedContainer(); + } + private void PutItemsToLinkedContainer() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -399,6 +433,72 @@ namespace Barotrauma.Items.Components } } + private void ApplyDeconstructionStatusEffects(Item targetItem, ActionType type, float deltaTime) + { + var linkedCharacterComponent = targetItem.GetComponent(); + Character character = null; + if (linkedCharacterComponent is { Character.Removed: false }) + { + character = linkedCharacterComponent.Character; + } + + Limb limb = character?.AnimController.Limbs.GetRandomUnsynced(); + + if (user != null) + { + item.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user)); + targetItem.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user)); + } + + // Apply OnDeconstruct/OnDeconstructing to the Deconstructor/item being deconstructed + item.ApplyStatusEffects(type, deltaTime, character, limb, useTarget: targetItem); + targetItem.ApplyStatusEffects(type, deltaTime, character, limb); + + if (character != null) + { + if (type == ActionType.OnDeconstructed) + { + // Move whatever was on the character inventory to free up space for status effects that spawn items + MoveItemsFromCharacterToOutput(); + } + + character.ApplyStatusEffects(type, deltaTime); + + if (type == ActionType.OnDeconstructed) + { + // This needs to run next frame because the status effect might enqueue items to be spawned next frame + CoroutineManager.Invoke(() => + { + if (character.Removed) { return; } + + // Move items again since the status effect could have spawned additional items in the character inventory + MoveItemsFromCharacterToOutput(); + + Entity.Spawner?.AddEntityToRemoveQueue(character); + }, 0.1f); + } + + void MoveItemsFromCharacterToOutput() + { + if (character.Inventory != null) + { + foreach (var item in character.Inventory.AllItemsMod) + { + if (item.Removed) { continue; } + if (!item.IsPlayerTeamInteractable) { continue; } + + if (!outputContainer.Inventory.TryPutItem(item, user: null)) + { + item.Drop(dropper: null); + } + + TryMoveItemToOutputContainers(item); + } + } + } + } + } + /// /// Move items towards the last slot in the inventory if there's free slots /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 52df29696..6676545ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -388,6 +388,13 @@ namespace Barotrauma.Items.Components { Attack.DamageMultiplier = damageMultiplier; } + foreach (var statusEffect in Item.GetStatusEffectsOfType(ActionType.OnImpact)) + { + foreach (var explosion in statusEffect.Explosions) + { + explosion.Attack.DamageMultiplier = damageMultiplier; + } + } // Set user for hitscan projectiles to work properly. User = user; // Need to set null for non-characterusable items. @@ -460,6 +467,7 @@ namespace Barotrauma.Items.Components { initialRotation -= MathHelper.Pi; } + Submarine initialSubmarine = item.Submarine; for (int i = 0; i < HitScanCount; i++) { float launchAngle; @@ -476,6 +484,8 @@ namespace Barotrauma.Items.Components Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); Vector2 prevSimpos = item.SimPosition; item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle); + //when launching multiple projectiles, ensure each raycast starts from the same sub + item.Submarine = initialSubmarine; if (Hitscan) { DoHitscan(launchDir); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 1aebf8187..6ec4fd5a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -448,9 +448,10 @@ namespace Barotrauma.Items.Components UpdateProjSpecific(deltaTime); IsTinkering = false; - if (prevSentConditionValue != (int)item.ConditionPercentage || conditionSignal == null) + int condition = (int)(item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f); + if (prevSentConditionValue != condition || conditionSignal == null) { - prevSentConditionValue = (int)item.ConditionPercentage; + prevSentConditionValue = condition; conditionSignal = prevSentConditionValue.ToString(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index e0475bbd5..c9a3cab15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -27,6 +27,11 @@ namespace Barotrauma.Items.Components private init => _displayName = value; } + /// + /// Display name ignoring + /// + public LocalizedString DefaultDisplayName => _displayName; + public LocalizedString DisplayNameOverride; private readonly HashSet wires; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index e0104a213..bccb65f42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -296,6 +296,7 @@ namespace Barotrauma.Items.Components } #endif CheckIfNeedsUpdate(); + SetLightSourceTransformProjSpecific(); } public void CheckIfNeedsUpdate() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index d5d7678c5..9da0bcfba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -42,6 +42,10 @@ namespace Barotrauma.Items.Components private float currentChargeTime; private bool tryingToCharge; + private const float LineOfSightCheckInterval = 0.5f; + private (Body WorldTarget, Body TransformedTarget, double Time) lastLineOfSightCheck; + private (Character Target, bool CanSee, double Time) lastCanSeeTargetCheck; + private enum ChargingState { Inactive, @@ -1088,6 +1092,7 @@ namespace Barotrauma.Items.Components foreach (Submarine sub in Submarine.Loaded) { if (sub == Item.Submarine) { continue; } + if (sub.IsRespawnShuttle) { continue; } if (item.Submarine != null) { if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; } @@ -1164,15 +1169,28 @@ namespace Barotrauma.Items.Components } Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); + + bool doLineOfSightCheck = lastLineOfSightCheck.Time < Timing.TotalTimeUnpaused - LineOfSightCheckInterval; + if (doLineOfSightCheck) + { + lastLineOfSightCheck.WorldTarget = CheckLineOfSight(start, end); + lastLineOfSightCheck.Time = Timing.TotalTime; + } + // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. - Body worldTarget = CheckLineOfSight(start, end); + Body worldTarget = lastLineOfSightCheck.WorldTarget; bool shoot; if (target.Submarine != null) { - start -= target.Submarine.SimPosition; - end -= target.Submarine.SimPosition; - Body transformedTarget = CheckLineOfSight(start, end); - shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)); + if (doLineOfSightCheck) + { + start -= target.Submarine.SimPosition; + end -= target.Submarine.SimPosition; + lastLineOfSightCheck.TransformedTarget = CheckLineOfSight(start, end); + } + shoot = + (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)) && + CanShoot(lastLineOfSightCheck.TransformedTarget, user: null, friendlyTag, TargetSubmarines); } else { @@ -1437,8 +1455,20 @@ namespace Barotrauma.Items.Components // Adjust the target character position (limb or submarine) if (currentTarget is Character targetCharacter) { + bool enemyInAnotherSub = targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine; + bool canSeeTarget = true; + if (enemyInAnotherSub) + { + if (lastCanSeeTargetCheck.Time < Timing.TotalTime - LineOfSightCheckInterval || + targetCharacter != lastCanSeeTargetCheck.Target) + { + canSeeTarget = targetCharacter.CanSeeTarget(Item); + lastCanSeeTargetCheck = (targetCharacter, canSeeTarget, Timing.TotalTime); + } + } + //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 (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) + if (enemyInAnotherSub && !canSeeTarget) { targetPos = targetCharacter.CurrentHull.WorldPosition; if (closestDistance > maxDistance * maxDistance) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index ce4da3bda..54c9a5f90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -569,16 +569,16 @@ namespace Barotrauma /// /// If there is room, puts the item in the inventory and returns true, otherwise returns false /// - public virtual bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false) + public virtual bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { int slot = FindAllowedSlot(item, ignoreCondition); if (slot < 0) { return false; } - PutItem(item, slot, user, true, createNetworkEvent); + PutItem(item, slot, user, true, createNetworkEvent, triggerOnInsertedEffects); return true; } - public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false) + public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { if (!IsIndexInRange(i)) { @@ -609,14 +609,14 @@ namespace Barotrauma //item in the slot removed as a result of combining -> put this item in the now free slot if (!slots[i].Any()) { - return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition); + return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects); } return true; } } if (CanBePutInSlot(item, i, ignoreCondition)) { - PutItem(item, i, user, true, createNetworkEvent); + PutItem(item, i, user, true, createNetworkEvent, triggerOnInsertedEffects); return true; } else if (slots[i].Any() && item.ParentInventory != null && allowSwapping) @@ -642,7 +642,7 @@ namespace Barotrauma } } - protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) + protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true, bool triggerOnInsertedEffects = true) { if (!IsIndexInRange(i)) { @@ -941,7 +941,8 @@ namespace Barotrauma { foreach (var item in items) { - if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true) && + //don't trigger OnInserted effects: we're not really "inserting" but just putting it back where it was because swapping failed + if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true, triggerOnInsertedEffects: false) && !inventory.GetItemsAt(slotIndex).Contains(item)) { inventory.ForceToSlot(item, slotIndex); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 53d7e0faf..41f69d22f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -262,6 +262,8 @@ namespace Barotrauma /// public Character Equipper; + public Inventory PreviousParentInventory { get; set; } + //the inventory in which the item is contained in public Inventory ParentInventory { @@ -277,9 +279,7 @@ namespace Barotrauma Container = parentInventory.Owner as Item; RemoveFromDroppedStack(allowClientExecute: false); } -#if SERVER PreviousParentInventory = value; -#endif } } @@ -560,6 +560,8 @@ namespace Barotrauma set; } = float.PositiveInfinity; + public Sprite OverrideInventorySprite { get; set; } + protected Color spriteColor; [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)] public Color SpriteColor @@ -1102,7 +1104,7 @@ namespace Barotrauma public override string ToString() { - return Name + " (ID: " + ID + ")"; + return (Name.IsNullOrEmpty() ? Prefab.Identifier : Name) + " (ID: " + ID + ")"; } private readonly List allPropertyObjects = new List(); @@ -1790,7 +1792,8 @@ namespace Barotrauma ic.Move(amount, ignoreContacts); } - if (body != null && (Submarine == null || !Submarine.Loading) || Screen.Selected is { IsEditor: true }) { FindHull(); } + // Refresh items without a body in editors so vents (or other static items that do something with hulls) know which hull they are in after being moved + if ((body != null || Screen.Selected is { IsEditor: true }) && Submarine is not { Loading: true }) { FindHull(); } } public Rectangle TransformTrigger(Rectangle trigger, bool world = false) @@ -2070,6 +2073,12 @@ namespace Barotrauma } } + public IEnumerable GetStatusEffectsOfType(ActionType type) + { + if (!hasStatusEffectsOfType[(int)type]) { return Enumerable.Empty(); } + return statusEffectLists[type]; + } + /// /// Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that should be handled by the code calling the method. /// @@ -3255,7 +3264,9 @@ namespace Barotrauma bool showUiMsg = false; #if CLIENT if (!ic.HasRequiredSkills(user, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); } - showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; + showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen && + // Only show the UI message of the component that we actually want to interact with + (pickHit && ic.CanBePicked || selectHit && ic.CanBeSelected); #endif if (!ignoreRequiredItems && !ic.HasRequiredItems(user, showUiMsg)) { continue; } if ((ic.CanBePicked && pickHit && ic.Pick(user)) || @@ -4354,6 +4365,9 @@ namespace Barotrauma Rotation = Rotation }; + if (FlippedX) { newItem.FlipX(relativeToSub: false); } + if (FlippedY) { newItem.FlipY(relativeToSub: false); } + float scaleRelativeToPrefab = Scale / Prefab.Scale; newItem.Scale *= scaleRelativeToPrefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 1a8cf3cc1..29b914960 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -88,9 +88,9 @@ namespace Barotrauma return true; } - public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { - bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent, ignoreCondition); + bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects); if (wasPut) { @@ -111,9 +111,9 @@ namespace Barotrauma return wasPut; } - public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { - bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition); + bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects); if (wasPut && item.ParentInventory == this) { foreach (Character c in Character.CharacterList) @@ -124,7 +124,7 @@ namespace Barotrauma } container.IsActive = true; - container.OnItemContained(item); + container.OnItemContained(item, triggerOnInsertedEffects); #if SERVER GameMain.Server?.KarmaManager?.OnItemContained(item, container.Item, user); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index c340efab6..a52d1d94c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -548,6 +548,8 @@ namespace Barotrauma public float DeconstructTime { get; private set; } + public float DeconstructTimeInOutposts { get; private set; } + public bool AllowDeconstruct { get; private set; } //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. @@ -1074,6 +1076,7 @@ namespace Barotrauma var storePrices = new Dictionary(); var preferredContainers = new List(); DeconstructTime = 1.0f; + DeconstructTimeInOutposts = DeconstructTime; if (ConfigElement.GetAttribute("allowasextracargo") != null) { @@ -1174,6 +1177,7 @@ namespace Barotrauma break; case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); + DeconstructTimeInOutposts = subElement.GetAttributeFloat("timeinoutposts", DeconstructTime); AllowDeconstruct = true; RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false); RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 2bb9cb587..cad423406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -398,7 +398,7 @@ namespace Barotrauma } foreach (Item contained in container.Inventory.AllItems) { - if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } + if (TargetSlot > -1 && container.Inventory.FindIndex(contained) != TargetSlot) { continue; } if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } if (CheckContained(contained)) { return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 1c193d983..7c4391e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -123,6 +123,8 @@ namespace Barotrauma public const float OxygenDeteriorationSpeed = 0.3f; public const float OxygenConsumptionSpeed = 700.0f; + private const float DecalAlphaRemoveThreshold = 0.001f; + public const int WaveWidth = 32; public static float WaveStiffness = 0.01f; public static float WaveSpread = 0.02f; @@ -913,7 +915,7 @@ namespace Barotrauma for (int i = decals.Count - 1; i >= 0; i--) { var decal = decals[i]; - if (decal.FadeTimer >= decal.LifeTime || decal.BaseAlpha <= 0.001f) + if (decal.FadeTimer >= decal.LifeTime || decal.BaseAlpha <= DecalAlphaRemoveThreshold) { decals.RemoveAt(i); #if SERVER @@ -1159,7 +1161,10 @@ namespace Barotrauma Hull currentHull = current.hull; Vector2 currentPos = current.pos; - if (currentDist > maxDistance) { return float.MaxValue; } + if (currentDist > maxDistance) + { + return float.MaxValue; + } // If we've reached the target, add the final segment from hull to endPos if (currentHull == targetHull) @@ -1167,7 +1172,7 @@ namespace Barotrauma return currentDist + Vector2.Distance(currentPos, endPos); } - foreach (Gap g in ConnectedGaps) + foreach (Gap g in currentHull.ConnectedGaps) { float distanceMultiplier = 1; if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) @@ -1643,9 +1648,18 @@ namespace Barotrauma bool decalsCleaned = false; foreach (Decal decal in decals) { + // Don't attempt to clean the decal if it's already below the remove threshold, since the server + // is already gonna remove the decal for us, sending another decal update event would result in + // us potentially modifying a different decal since the indices can briefly desync. + if (decal.BaseAlpha <= DecalAlphaRemoveThreshold) + { + continue; + } + if (decal.AffectsSection(section)) { decal.Clean(cleanVal); + decalsCleaned = true; #if SERVER decalUpdatePending = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index c5f0d161f..71f4b0752 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -7,6 +7,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -327,6 +328,7 @@ namespace Barotrauma public Submarine BeaconStation { get; private set; } private Sonar beaconSonar; + private ImmutableArray beaconTransducers = ImmutableArray.Empty; /// /// Special wall chunks that aren't part of the normal level geometry: includes things like the ocean floor, floating ice chunks and ice spires. @@ -4398,6 +4400,13 @@ namespace Barotrauma attempts++; } } + + foreach (var wreck in Wrecks) + { + wreck.SetCrushDepth(wreck.RealWorldDepth + 1000); + SetLinkedSubCrushDepth(wreck); + } + totalSW.Stop(); Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)"); } @@ -4782,6 +4791,7 @@ namespace Barotrauma return; } beaconSonar = sonarItem.GetComponent(); + beaconTransducers = sonarItem.GetConnectedComponents().ToImmutableArray(); } public void PrepareBeaconStation() @@ -4908,9 +4918,20 @@ namespace Barotrauma public bool CheckBeaconActive() { if (beaconSonar == null) { return false; } + if (beaconSonar.UseTransducers) + { + var connectedTransducers = beaconSonar.Item.GetConnectedComponents(); + foreach (var beaconTransducer in beaconTransducers) + { + if (!beaconTransducer.HasPower || !connectedTransducers.Contains(beaconTransducer)) { return false; } + } + } return beaconSonar.HasPower && beaconSonar.CurrentMode == Sonar.Mode.Active; } + /// + /// Set the crush depths of the connected subs to match the crush depth of the parent sub. + /// private void SetLinkedSubCrushDepth(Submarine parentSub) { foreach (var connectedSub in parentSub.GetConnectedSubs()) @@ -5149,6 +5170,7 @@ namespace Barotrauma BeaconStation = null; beaconSonar = null; + beaconTransducers = ImmutableArray.Empty; StartOutpost = null; EndOutpost = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 03e37859c..b7460e61c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -413,8 +413,7 @@ namespace Barotrauma new XAttribute("difficulty", Difficulty.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("size", XMLExtensions.PointToString(Size)), new XAttribute("generationparams", GenerationParams.Identifier), - new XAttribute("initialdepth", InitialDepth), - new XAttribute("exhaustedeventsets", allEventsExhausted)); + new XAttribute("initialdepth", InitialDepth)); newElement.Add( new XAttribute(nameof(exhaustedEventSets), string.Join(',', exhaustedEventSets.Select(e => e.Value)))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 8339e6c65..b96b7f1a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -651,7 +651,7 @@ namespace Barotrauma newBody.Friction = 0.8f; newBody.UserData = this; - newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset * Scale; + newBody.Position = ConvertUnits.ToSimUnits(stairPos) + ConvertUnits.ToSimUnits(BodyOffset) * Scale; bodyDimensions.Add(newBody, new Vector2(bodyWidth, bodyHeight)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 1dc9d991c..10c1728ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -583,7 +583,11 @@ namespace Barotrauma { for (int dir = -1; dir <= 1; dir += 2) { - WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2)); + //connect to the closest waypoint, preferring non-stair waypoyints + //(it's easier for characters to fully get off stairs before moving on to the next set of stairs, than to move directly from one set of stairs to another) + WayPoint closest = + stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2), filter: wp => wp.Stairs == null) ?? + stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2)); if (closest == null) { continue; } stairPoints[i].ConnectTo(closest); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 38e335927..efb3d7916 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -39,6 +39,12 @@ namespace Barotrauma.Networking public const int MaxEventPacketsPerUpdate = 4; + /// + /// When enabled, uses more lenient Lidgren handshake timeouts (longer connection timeout, more retry attempts). + /// Useful for local testing when running multiple instances on the same machine under heavy load. + /// + public static bool UseLenientHandshake; + /// /// Interpolates the positional error of a physics body towards zero. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs index e75202086..54f671521 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs @@ -33,13 +33,7 @@ namespace Barotrauma.Networking /// regarding its relation to values other than the input. /// public static ushort GetIdOlderThan(ushort id) -#if DEBUG - // Debug implementation has some RNG to discourage bad assumptions about the return value - => unchecked((ushort)(id - 1 - Rand.Int(500, sync: Rand.RandSync.Unsynced))); -#else - // Release implementation favors performance => unchecked((ushort)(id - 1)); -#endif public static ushort Difference(ushort id1, ushort id2) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs index ff2788158..aa2d46143 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs @@ -15,13 +15,18 @@ sealed class SteamAuthTicketForEosHostAuthenticator : Authenticator { string ticketData = ToolBoxCore.ByteArrayToHexString(ticket.Data); - var client = new RestClient(ServerUrl); - - var request = new RestRequest(ServerFile, Method.GET); + var client = RestFactory.CreateClient(ServerUrl); + var request = RestFactory.CreateRequest(ServerFile); request.AddParameter("authticket", ticketData); request.AddParameter("request_version", RemoteRequestVersion); var response = await client.ExecuteAsync(request, Method.GET); + if (response.ErrorException != null) + { + DebugConsole.AddWarning($"Connection error: Failed to verify Steam auth ticket for EOS host " + + $"({response.ErrorException.Message})."); + return AccountInfo.None; + } if (!response.IsSuccessful) { return AccountInfo.None; } try diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 89a84fbfe..696b70a0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -454,7 +454,7 @@ namespace Barotrauma.Networking private set; } - [Serialize(300.0f, IsPropertySaveable.Yes)] + [Serialize(30.0f, IsPropertySaveable.Yes)] public float RespawnInterval { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 58f0f5280..5434b25a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -534,8 +534,10 @@ namespace Barotrauma default: throw new NotImplementedException(); } - return spritesheetRotation.HasValue ? Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation.Value)) : pos; + return spritesheetRotation.HasValue ? RotateVector(pos, spritesheetRotation.Value) : pos; } + + public static Vector2 RotateVector(Vector2 v, float rotation) => Vector2.Transform(v, Matrix.CreateRotationZ(-rotation)); public float GetMaxExtent() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 47b575be5..f3a0a4842 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -410,7 +410,7 @@ namespace Barotrauma && otherPrefab.UintIdentifier == prefabWithUintIdentifier.UintIdentifier); for (T? collision = findCollision(); collision != null; collision = findCollision()) { - DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})"); + DebugConsole.AddWarning($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})"); prefabWithUintIdentifier.UintIdentifier++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index b04914057..a3a19cddb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -36,6 +36,27 @@ namespace Barotrauma { typeof(Rectangle), (str, defVal) => ParseRect(str, true) } }.ToImmutableDictionary(); + /// + /// Check if the given value equals to the default value of the property. + /// Takes into account that certain default values (e.g. Vectors and other values that aren't compile-time constants) are defined as strings. + /// + public static bool DefaultValueEquals(object defaultValue, object value) + { + //if the value is given as a string, check if there's a converter for the type of the default value and attempt converting + if (defaultValue != null && value is string valueAsString && + Converters.TryGetKey(defaultValue.GetType(), out Type type)) + { + return Equals(Converters[type].Invoke(valueAsString, defaultValue), defaultValue); + } + //other way around: default values is given as a string, check if there's a converter for the type of the value + else if (value != null && defaultValue is string defaultValueAsString && + Converters.TryGetKey(value.GetType(), out Type type2)) + { + return Equals(Converters[type2].Invoke(defaultValueAsString, value), value); + } + return Equals(value, defaultValue); + } + public static string ParseContentPathFromUri(this XObject element) => !string.IsNullOrWhiteSpace(element.BaseUri) ? System.IO.Path.GetRelativePath(Environment.CurrentDirectory, element.BaseUri.CleanUpPath()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index eb44f9d6b..8f22a4da0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -72,6 +72,7 @@ namespace Barotrauma EnableSplashScreen = true, PauseOnFocusLost = true, RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/", + RemoteContentTimeoutSeconds = 15f, AimAssistAmount = DefaultAimAssist, ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, ChatSpeechBubbles = true, @@ -167,6 +168,17 @@ namespace Barotrauma public bool EnableSubmarineAutoSave; public Identifier QuickStartSub; public string RemoteMainMenuContentUrl; + + /// + /// Timeout in seconds for HTTP requests to remote content servers. + /// + public float RemoteContentTimeoutSeconds; + + /// + /// Returns converted to milliseconds needed by eg. RestSharp. + /// + public readonly int RemoteContentTimeoutMs => (int)(RemoteContentTimeoutSeconds * 1000); + #if CLIENT public Eos.EosSteamPrimaryLogin.CrossplayChoice CrossplayChoice; public XElement SavedCampaignSettings; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index bdd8bfac5..2fc581c16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -841,6 +841,12 @@ namespace Barotrauma /// public Vector2 Offset { get; private set; } + /// + /// Should be rotated, flipped and scaled based on the entity that this effect is executed by? + /// Currently only supports status effects in items. + /// + public bool OffsetCopiesEntityTransform { get; private set; } + /// /// An random offset (in a random direction) added to the position of the effect is executed at. Only relevant if the effect does something where position matters, /// for example emitting particles or explosions, spawning something or playing sounds. @@ -901,6 +907,7 @@ namespace Barotrauma Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); + OffsetCopiesEntityTransform = element.GetAttributeBool(nameof(OffsetCopiesEntityTransform), false); RandomOffset = element.GetAttributeFloat("randomoffset", 0.0f); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); if (targetLimbNames != null) @@ -1724,6 +1731,7 @@ namespace Barotrauma protected Vector2 GetPosition(Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition); + if (worldPosition == null) { if (entity is Character character && !character.Removed && targetLimbs != null) @@ -1760,9 +1768,22 @@ namespace Barotrauma } } } - } - position += Offset; + + Vector2 offset = Offset; + + if (OffsetCopiesEntityTransform) + { + if (entity is Item item) + { + offset *= item.Scale; + if (item.FlippedX) { offset.X *= -1; } + if (item.FlippedY) { offset.Y *= -1; } + offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad)); + } + } + + position += offset; position += Rand.Vector(Rand.Range(0.0f, RandomOffset)); return position; } @@ -2060,24 +2081,9 @@ namespace Barotrauma { LocalizedString messageToSay = TextManager.Get(forceSayIdentifier).Fallback(forceSayIdentifier.Value); - if (!messageToSay.IsNullOrEmpty() && target is Character targetCharacter && targetCharacter.SpeechImpediment < 100.0f && !targetCharacter.IsDead) + if (!messageToSay.IsNullOrEmpty() && target is Character targetCharacter) { - ChatMessageType messageType = ChatMessageType.Default; - bool canUseRadio = ChatMessage.CanUseRadio(targetCharacter, out WifiComponent radio); - if (canUseRadio && forceSayInRadio) - { - messageType = ChatMessageType.Radio; - } -#if SERVER - GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, targetCharacter); -#elif CLIENT - //no need to create the message when playing as a client, the server will send it to us - if (isNotClient) - { - AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType); - targetCharacter.SendSinglePlayerMessage(message, canUseRadio, radio); - } -#endif + targetCharacter.ForceSay(messageToSay, forceSayInRadio); } } @@ -2229,7 +2235,10 @@ namespace Barotrauma inheritedTeam = entity switch { Character c => c.TeamID, - Item it => it.GetRootInventoryOwner() is Character owner ? owner.TeamID : GetTeamFromSubmarine(it), + Item it => + (it.GetRootInventoryOwner() as Character ?? it.PreviousParentInventory?.Owner as Character) is { } owner ? + owner.TeamID : + GetTeamFromSubmarine(it), MapEntity e => GetTeamFromSubmarine(e), _ => null // Default to Team1, when we can't deduce the team (for example when spawning outside the sub AND character inventory). diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs index 92c987b9e..4eca59969 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs @@ -9,19 +9,21 @@ namespace Barotrauma public enum Mode { Start = 0x1, End = 0x2, Both=0x3 } private readonly LocalizedString nestedStr; private readonly Mode mode; + private readonly char[]? trimCharacters; - public TrimLString(LocalizedString nestedStr, Mode mode) + public TrimLString(LocalizedString nestedStr, Mode mode, char[]? trimCharacters = null) { this.nestedStr = nestedStr; this.mode = mode; + this.trimCharacters = trimCharacters; } public override bool Loaded => nestedStr.Loaded; public override void RetrieveValue() { cachedValue = nestedStr.Value; - if (mode.HasFlag(Mode.Start)) { cachedValue = cachedValue.TrimStart(); } - if (mode.HasFlag(Mode.End)) { cachedValue = cachedValue.TrimEnd(); } + if (mode.HasFlag(Mode.Start)) { cachedValue = cachedValue.TrimStart(trimCharacters); } + if (mode.HasFlag(Mode.End)) { cachedValue = cachedValue.TrimEnd(trimCharacters); } UpdateLanguage(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs index 19ee41a2f..5ba27c155 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -242,7 +242,10 @@ namespace Barotrauma } - Barotrauma.IO.File.WriteAllText($"csv_{Language.ToString().ToLower()}_{index}.csv", sb.ToString()); + string fileName = $"csv_{Language.ToString().ToLower()}_{index}.csv"; + Barotrauma.IO.File.WriteAllText(fileName, sb.ToString()); + + DebugConsole.NewMessage($"Wrote \"{ContentFile.Path}\" to \"{fileName}\""); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RestFactory.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RestFactory.cs new file mode 100644 index 000000000..1c3c38f97 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RestFactory.cs @@ -0,0 +1,35 @@ +using RestSharp; + +namespace Barotrauma +{ + /// + /// Factory methods for creating RestSharp clients and requests with default timeout + /// settings, to avoid unforeseen connectivity issues hanging the game. + /// The timeout needs to be added to both the client and the request, due to known + /// issues with RestSharp 106.x that we use: https://github.com/restsharp/RestSharp/issues/1900 + /// + public static class RestFactory + { + /// + /// Creates a RestClient with applied. + /// + public static RestClient CreateClient(string baseUrl) + { + return new RestClient(baseUrl) + { + Timeout = GameSettings.CurrentConfig.RemoteContentTimeoutMs + }; + } + + /// + /// Creates a RestRequest with applied. + /// + public static RestRequest CreateRequest(string resource, Method method = Method.GET) + { + return new RestRequest(resource, method) + { + Timeout = GameSettings.CurrentConfig.RemoteContentTimeoutMs + }; + } + } +} diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index fcd1b5730..3c6b19da0 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,94 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.12.6.2 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Updated localization files. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.12.6.1 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed some conversation prompts (such as the one with Artie Dolittle) being misaligned, causing parts the text to be cropped. More specifically, ones that start with some special event sprite but also show the speaker's face in the prompt. +- Fixed pets having trouble moving due to some of the navigation changes in the previous build. Also caused huskified containers to get launched off with enormous speed when they tried to eat something. +- Fixed navigation terminals in shuttles having their maintain position get messed up between level transitions. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.12.6.0 (Spring Update 2026) +------------------------------------------------------------------------------------------------------------------------------------------------- + +Submarine reworks: +- Tier 1 submarines (Barsuk, Dugong, Orca and Camel) have all received a visual polish as well as gameplay polishing. +- Camel and Orca now uses the pipe weakpoints and valves system. +- Various improvements and additions to the vanilla item assemblies. + +Balance: +- Increase health of flare, alienflare and glowstick. Now doesn't get destroyed as quickly by monsters, allowing it be a more useful distraction again. Glowsticks don't aggro monsters from as far as flares. +- Sonar Beacon's sound range is reduced, and when dropped can now be destroyed by monsters (to avoid making flares redundant due to being a better and invulnerable version of monster distraction). + +Changes: +- Characters can now be "deconstructed" by dragging them into a deconstructor, producing small amounts of raw materials. Also a handy way to get rid of monster corpses on the submarine, and perhaps problematic crew mates as well. +- Stationary batteries can charge the battery cells inside them even when the output is disabled. +- A handful of missions in which you can earn a reward for getting through the level fast enough (which also serve as an example of the new custom mission functionality, see the Modding section for more information). +- Minor lighting optimizations. + +Multiplayer: +- Reduced the default respawn interval from 300 seconds to 30. +- Fixed an issue that sometimes caused the list of hidden subs to get out of sync in multiplayer, preventing some subs from being purchased. +- Fixed pickup sound not playing when picking up an item in multiplayer. +- Fixed karma system considering bandages and other medical items "dangerous" and giving a penalty for taking them from other players. +- Characters no longer drop items when the player disconnects (meaning you won't lose the items you were holding). + +Fixes: +- Another attempt to fix reported freezes at 80% in the loading screen, which seems to have been caused by Steam's servers or our master server refusing connection attempts from certain kinds of IPs, causing the game to hang waiting for the connection. +- Fixed conversation/event prompts sometimes getting stuck when you went rapidly pressing E. In particular, this happened with events that allow you to retrigger the same event by interacting with the NPC again. +- Fixed certain monsters (e.g. mudraptors) having trouble dropping through hatches inside the sub. +- Fixed monsters often failing to follow targets from sub to another (e.g. from Remora's drone to the main sub). +- Fixes to pathfinding bugs that sometimes caused bots to get stuck on stairs. +- Fixed closing the health interface while your cursor is on another character opening that character's health interface. +- Fixed an AI bug that often prevented outpost NPCs from putting out fires. +- Fixed projectiles that fire more than one raycast per shot (e.g. shotgun shells) only registering one hit if you're firing from inside to outside. +- Fixed implacable sometimes not triggering in time, causing a 5-second stun when vitality dropped below zero. +- Fixed radio jammer not having the traitormissionitem tag (unlike every other traitor mission item). +- Fixed ruin scan missions sometimes failing to choose all 3 positions to scan, making the mission impossible to complete. Happened with very small ruins in particular. +- Fixed Engineering_G4 module sometimes spawning with a ladder leading nowhere. +- Hide items inside non-interactable containers (e.g. decorative items not accessible to the player) showing up on the item finder. +- The achievement for killing a monster is also awarded if the monster is killed by an bot in single player. +- Fixed some items sometimes teleporting to the origin when saving and loading in the submarine editor. +- Fixed a broken waypoint near Berilia's engine which made bots sometimes get stuck there. +- Fixed shuttles/drones/elevators or other parts of a wreck getting crush depth damage in deep levels. +- Characters that respawn in a flooded hull (in either a submarine or an outpost) now spawn with diving gear. +- Fixed fabrication tooltip being unclear (previously showed "requires recipe to fabricate" in red even when the recipe is already learnt, now shows in green that is has been learned) +- Fixed characters sometimes not taking fall damage if they fall on a mirrored structure. +- Fixed portable pumps getting damaged by explosions, despite not being repairable. +- Fixed pet raptors getting assigned an incorrect team if they hatch in a hostile outpost. +- Fixed Linux systems failing to load content packages whose filelist.xml files aren't all lower-case. +- Fixed NPCs ignoring infected humans attacking them. +- Fixed mirrored items becoming unmirrored when swapped by perk points (e.g. a mirrored periscope base becoming an unmirrored periscope when purchasing a turret with a perk point). +- Fixed inability to rename already-hired bots if you no longer have the required reputation to hire the bot. +- Fixed WeaponDamageModifier (a multiplier in RangedWeapon which seems to be used for buffing the damage of variants of a weapon) not affecting explosion damage. Means that e.g. Harpoon-Coil Rifle or Autoshotgun's modifiers don't actually do much when using explosive ammo. +- Fixed creatures not being able to "properly leave a sub" if any of their severed limbs are still inside the sub. In practice, they'd still be considered to be inside the sub, and they would not collide with anything outside it. +- Fixed toggling layer visibility selecting that layer in the sub editor. +- Fixed pasting entities unhiding all of the layers in the sub editor. +- Fixed condition_out connections not taking into account the multipliers applied by the Tinkerer talent. +- Fixed bots still refusing to deconstruct items that yield nothing, even though you could order them to deconstruct those. + +Modding: +- Support for custom event-based missions. The mission simply triggers a specific event, and that event can control the success/failure of the mission using MissionStateAction. See the "TimeTrial" missions in Missions.xml for an usage example. +- Character, level and particle editors show fields set to the default values as gray. Makes it easier to tell which fields have been modified or are relevant for that specific character/level/particle. +- It's possible to add empty RequiredItem elements to item components to make them not have any requirements by default, but allowing them to be added in the submarine editor. +- Fixed limb's randomcolor attribute not working as expected: every character of the same type would get the same randomly chosen color, instead of a different color being chosen for each character. +- Added ForceSayAction which can be used by scripted events to make characters speak in the chat. ConversationAction can also now be used to display text in the chat in addition to the conversation prompt. +- CheckConditionalAction now fails instead of succeeding if it can't find the specified target. There's also a property called FailIfTargetNotFound to make it succeed instead. +- CountTargetsAction now fails instead of succeeding if it's trying to compare against the amount of some other target (e.g. "number of flooded hulls" is at least 30% of the "number of all hulls") and none of that other target can't be found. +- Fixed light offsets not being handled correctly on flipped items (did not affect any vanilla items). +- Adds a new status effect property called OffsetCopiesEntityTransform that can be used by status effects to configure the offset so it copies the current entity rotation/flipping/scaling. +- Fixed TargetSlot in RequiredItem not working properly on items that have multiple ItemContainers/inventories (only the first one was checked). +- Fixed melee weapons sometimes hitting characters whose limbs have been set to ignore collisions. More specifically, the weapon would still hit the character's "main collider". +- If a beacon station has a sonar transducer connected to the sonar monitor, and the monitor is set to use transducers, the transducer must be powered for the beacon mission to complete. +- Fix ContainedSpriteDepth being tied to the item's index in the container instead of the slot index. +- Fixed OnInserted StatusEffects triggering when you try to swap an item inside some other item but the swap fails. + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.11.5.0 (Winter Update Hotfix 1) ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj b/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj index 43147b2b5..7cc2b89a1 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj +++ b/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj @@ -1,28 +1,28 @@ - - - - net8.0 - Barotrauma - disable - enable - - - - full - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - full - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - - - - + + + + net8.0 + Barotrauma + disable + enable + + + + full + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + full + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + + + + diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs index d4c7f54f2..e22177450 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs @@ -47,11 +47,17 @@ public static class ToolBoxCore byte[] inputBytes = Encoding.UTF8.GetBytes(str); byte[] hash = md5.ComputeHash(inputBytes); - UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead? - key |= (UInt32)(hash[hash.Length - 3] << 16); - key |= (UInt32)(hash[hash.Length - 2] << 8); - key |= (UInt32)(hash[hash.Length - 1]); - + //xor all of the bits of the hash together + UInt32 key = 0; + foreach (byte b in hash) + { + // Rotate the 32-bit value left by 5 bits: + // (key << 5) moves everything left, + // (key >> 27) brings the 5 bits that overflowed back around (32 - 5 = 27), + // OR'ing them together completes the rotate. + key = (key << 5) | (key >> 27); + key ^= b; + } return key; } diff --git a/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj b/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj index e366bb52f..1976acfca 100644 --- a/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj +++ b/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj @@ -1,26 +1,26 @@ - - - - net8.0 - disable - enable - Barotrauma - - - - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - - - - + + + + net8.0 + disable + enable + Barotrauma + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + + + + diff --git a/Libraries/Concentus/.gitignore b/Libraries/Concentus/.gitignore index 629fe1100..fcd9a1be9 100644 --- a/Libraries/Concentus/.gitignore +++ b/Libraries/Concentus/.gitignore @@ -1,240 +1,240 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory -AppPackages/ -BundleArtifacts/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ -/Java/Concentus/target/ -/Java/ContentusTestConsole/ContentusTestConsole/target/ -/Java/ContentusTestConsole/ConcentusTestConsole/target/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ +/Java/Concentus/target/ +/Java/ContentusTestConsole/ContentusTestConsole/target/ +/Java/ContentusTestConsole/ConcentusTestConsole/target/ /Java/ConcentusTestConsole/target/ \ No newline at end of file diff --git a/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj b/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj index 725dda808..99073a079 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/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj b/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj index 96e8ca92c..69fbafa2f 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 6b99d030a..1a989ea0f 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/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj index a4477adaa..6f2368424 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 57fd8b083..b3b90d794 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 03cd45b0c..6c757d8b7 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 fc2241116..ae420d508 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 47185c67d..97eb46551 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 fcd971bb6..7ad4b5e21 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 dbcc8ae92..2eb113ac8 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 e428bd3f7..4ba7c8ae5 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 070c8ab01..383d19f71 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 cbf562183..8e4640094 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 588637e83..3036a4e70 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 5a313c31d..6804918a3 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 f04776380..4ed3bb9e7 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 aea557393..1def7460b 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 5a3253ee6..6e90faa56 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