diff --git a/Barotrauma/BarotraumaClient/ClientCode.projitems b/Barotrauma/BarotraumaClient/ClientCode.projitems index 7059daf8b..489043880 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.projitems +++ b/Barotrauma/BarotraumaClient/ClientCode.projitems @@ -18,14 +18,13 @@ - + - diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb index d40fbb220..35b0ae81b 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb index 8d926c6ea..15bed2a78 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 2a79e12b8..9f61fb02c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -31,6 +31,7 @@ true + ..\BarotraumaShared\IconBmp.bmp ..\BarotraumaShared\Icon.ico @@ -191,6 +192,11 @@ + + + Icon.bmp + + diff --git a/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs index def51c503..bd0597493 100644 --- a/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.8.9.6")] -[assembly: AssemblyFileVersion("0.8.9.6")] +[assembly: AssemblyVersion("0.8.9.7")] +[assembly: AssemblyFileVersion("0.8.9.7")] diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx index bf621efe0..dcf54addd 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx @@ -14,6 +14,7 @@ float2 uvTopLeft; float2 uvBottomRight; float4 tintColor; +float4 solidColor; struct VertexShaderInput { @@ -74,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidColorPS(VertexShaderOutput input) : COLOR +{ + return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + technique DeformShader { pass Pass1 @@ -81,4 +87,13 @@ technique DeformShader VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 mainPS(); } +} + +technique DeformShaderSolidColor +{ + pass Pass1 + { + VertexShader = compile vs_4_0_level_9_1 mainVS(); + PixelShader = compile ps_4_0_level_9_1 solidColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx index fbf640ede..c77904ffe 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx @@ -14,6 +14,7 @@ float2 uvTopLeft; float2 uvBottomRight; float4 tintColor; +float4 solidColor; struct VertexShaderInput { @@ -74,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidColorPS(VertexShaderOutput input) : COLOR +{ + return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + technique DeformShader { pass Pass1 @@ -81,4 +87,13 @@ technique DeformShader VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 mainPS(); } +} + +technique DeformShaderSolidColor +{ + pass Pass1 + { + VertexShader = compile vs_3_0 mainVS(); + PixelShader = compile ps_3_0 solidColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Source/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaClient/Source/Characters/AI/AITarget.cs index 6a7d9ec27..318d1d048 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/AITarget.cs @@ -13,13 +13,39 @@ namespace Barotrauma var pos = new Vector2(WorldPosition.X, -WorldPosition.Y); if (soundRange > 0.0f) { - Color color = Entity is Character ? Color.Yellow : Color.Orange; + Color color; + if (Entity is Character) + { + color = Color.Yellow; + } + else if (Entity is Item) + { + color = Color.Orange; + } + else + { + color = Color.OrangeRed; + } ShapeExtensions.DrawCircle(spriteBatch, pos, SoundRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); + ShapeExtensions.DrawCircle(spriteBatch, pos, 3, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); } if (sightRange > 0.0f) { - Color color = Entity is Character ? Color.CornflowerBlue : Color.CadetBlue; + Color color; + if (Entity is Character) + { + color = Color.CornflowerBlue; + } + else if (Entity is Item) + { + color = Color.CadetBlue; + } + else + { + color = Color.WhiteSmoke; + } ShapeExtensions.DrawCircle(spriteBatch, pos, SightRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); + ShapeExtensions.DrawCircle(spriteBatch, pos, 6, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs index e5ddec5c8..c18e14dee 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs @@ -21,13 +21,11 @@ namespace Barotrauma if (wallTarget != null) { Vector2 wallTargetPos = wallTarget.Position; - if (wallTarget.Structure.Submarine != null) wallTargetPos += wallTarget.Structure.Submarine.Position; wallTargetPos.Y = -wallTargetPos.Y; GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Red, false); - GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); + GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } - - GUI.Font.DrawString(spriteBatch, $"{SelectedAiTarget.Entity.ToString()} ({targetValue.ToString()})", pos - Vector2.UnitY * 20.0f, Color.Red); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity.ToString()} ({targetValue.FormatZeroDecimal()})", Color.Red, Color.Black); } /*GUI.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, Color.Red); @@ -70,29 +68,34 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3); - if (latchOntoAI.WallAttachPos.HasValue) - { - GUI.DrawLine(spriteBatch, pos, - ConvertUnits.ToDisplayUnits(new Vector2(latchOntoAI.WallAttachPos.Value.X, -latchOntoAI.WallAttachPos.Value.Y)), Color.Green, 0, 3); - } - } - - GUI.DrawLine(spriteBatch, - new Vector2(Character.DrawPosition.X, -Character.DrawPosition.Y), - new Vector2(pathSteering.CurrentPath.CurrentNode.DrawPosition.X, -pathSteering.CurrentPath.CurrentNode.DrawPosition.Y), - Color.Orange * 0.6f, 0, 3); - - for (int i = 1; i < pathSteering.CurrentPath.Nodes.Count; i++) + if (steeringManager is IndoorsSteeringManager pathSteering) { - GUI.DrawLine(spriteBatch, - new Vector2(pathSteering.CurrentPath.Nodes[i].DrawPosition.X, -pathSteering.CurrentPath.Nodes[i].DrawPosition.Y), - new Vector2(pathSteering.CurrentPath.Nodes[i - 1].DrawPosition.X, -pathSteering.CurrentPath.Nodes[i - 1].DrawPosition.Y), - Color.Orange * 0.6f, 0, 3); + var path = pathSteering.CurrentPath; + if (path != null) + { + if (path.CurrentNode != null) + { + GUI.DrawLine(spriteBatch, pos, + new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y), + Color.DarkViolet, 0, 3); - GUI.SmallFont.DrawString(spriteBatch, - pathSteering.CurrentPath.Nodes[i].ID.ToString(), - new Vector2(pathSteering.CurrentPath.Nodes[i].DrawPosition.X, -pathSteering.CurrentPath.Nodes[i].DrawPosition.Y - 10), - Color.LightGreen); + GUI.DrawString(spriteBatch, pos - new Vector2(0, 100), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); + } + for (int i = 1; i < path.Nodes.Count; i++) + { + var previousNode = path.Nodes[i - 1]; + var currentNode = path.Nodes[i]; + GUI.DrawLine(spriteBatch, + new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y), + new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y), + Color.Red * 0.5f, 0, 3); + + GUI.SmallFont.DrawString(spriteBatch, + currentNode.ID.ToString(), + new Vector2(currentNode.DrawPosition.X + 20, -currentNode.DrawPosition.Y - 20), + Color.Red); + } + } } GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Character.AnimController.TargetMovement.X, -Character.AnimController.TargetMovement.Y)), Color.SteelBlue, width: 2); GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs index b588b93a4..28686501e 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs @@ -75,11 +75,12 @@ namespace Barotrauma GUI.SmallFont.DrawString(spriteBatch, currentNode.ID.ToString(), - new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y - 10), - Color.LightGreen); + new Vector2(currentNode.DrawPosition.X + 20, -currentNode.DrawPosition.Y - 20), + Color.SkyBlue); } } } + GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Character.AnimController.TargetMovement.X, -Character.AnimController.TargetMovement.Y)), Color.SteelBlue, width: 2); GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3); //if (Character.IsKeyDown(InputType.Aim)) diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs index 5c3dd9749..c6f95eda7 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs @@ -210,7 +210,9 @@ namespace Barotrauma else { Vector2 positionError = serverPos.Position - localPos.Position; - float rotationError = serverPos.Rotation - localPos.Rotation; + float rotationError = serverPos.Rotation.HasValue && localPos.Rotation.HasValue ? + serverPos.Rotation.Value - localPos.Rotation.Value : + 0.0f; for (int i = localPosIndex; i < character.MemLocalState.Count; i++) { diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs index 54acd801e..c53b261fa 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs @@ -214,8 +214,8 @@ namespace Barotrauma msg.ReadRangedSingle(-MaxVel, MaxVel, 12)); bool fixedRotation = msg.ReadBoolean(); - float rotation = AnimController.Collider.Rotation; - float angularVelocity = AnimController.Collider.AngularVelocity; + float? rotation = null; + float? angularVelocity = null; if (!fixedRotation) { rotation = msg.ReadFloat(); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs index a43ee6069..d649f7b1a 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs @@ -1,7 +1,1408 @@ -namespace Barotrauma +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Lidgren.Network; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma { partial class CharacterHealth { + private static bool toggledThisFrame; + + private static Sprite damageOverlay; + + private static string[] strengthTexts; + + private GUIButton cprButton; + + private Point screenResolution; + + private float uiScale, inventoryScale; + + private Alignment alignment = Alignment.Left; + public Alignment Alignment + { + get { return alignment; } + set + { + if (alignment == value) return; + alignment = value; + UpdateAlignment(); + } + } + + private GUIButton suicideButton; + + // healthbars + private GUIProgressBar healthBar; + private GUIProgressBar healthBarShadow; + private GUIProgressBar healthWindowHealthBar; + private GUIProgressBar healthWindowHealthBarShadow; + private float healthShadowSize; + private float healthShadowDelay; + private float healthBarPulsateTimer; + private float healthBarPulsatePhase; + + private GUITextBlock characterName; + private GUIFrame afflictionInfoFrame; + private GUIListBox afflictionInfoContainer; + private GUIListBox recommendedTreatmentContainer; + + private float bloodParticleTimer; + + private GUIFrame healthWindow; + + private GUIComponent deadIndicator; + + private GUIComponent lowSkillIndicator; + + private SpriteSheet limbIndicatorOverlay; + private float limbIndicatorOverlayAnimState; + + private GUIFrame dropItemArea; + + private float dropItemAnimDuration = 0.5f; + private float dropItemAnimTimer; + + public Item DroppedItem + { + get + { + return droppedItem; + } + } + private Item droppedItem; + + private GUIComponent draggingMed; + + private int highlightedLimbIndex = -1; + private int selectedLimbIndex = -1; + private LimbHealth currentDisplayedLimb; + + private float distortTimer; + + // 0-1 + private float damageIntensity; + private float damageIntensityDropdownRate = 0.1f; + + public float DamageOverlayTimer { get; private set; } + + private float updateDisplayedAfflictionsTimer; + private const float UpdateDisplayedAfflictionsInterval = 0.5f; + private List currentDisplayedAfflictions = new List(); + + private static CharacterHealth openHealthWindow; + public static CharacterHealth OpenHealthWindow + { + get + { + return openHealthWindow; + } + set + { + if (openHealthWindow == value) return; + if (value != null && !value.UseHealthWindow) return; + + openHealthWindow = value; + toggledThisFrame = true; + if (Character.Controlled == null) { return; } + + if (value == null && + Character.Controlled?.SelectedCharacter?.CharacterHealth != null && + Character.Controlled.SelectedCharacter.CharacterHealth == openHealthWindow && + !Character.Controlled.SelectedCharacter.CanInventoryBeAccessed) + { + Character.Controlled.DeselectCharacter(); + } + + Character.Controlled.ResetInteract = true; + if (openHealthWindow != null) + { + openHealthWindow.characterName.Text = value.Character.Name; + Character.Controlled.SelectedConstruction = null; + } + } + } + + static CharacterHealth() + { + damageOverlay = new Sprite("Content/UI/damageOverlay.png", Vector2.Zero); + } + + partial void InitProjSpecific(XElement element, Character character) + { + if (strengthTexts == null) + { + strengthTexts = new string[] + { + TextManager.Get("AfflictionStrengthLow"), + TextManager.Get("AfflictionStrengthMedium"), + TextManager.Get("AfflictionStrengthHigh") + }; + } + + character.OnAttacked += OnAttacked; + + bool horizontal = HUDLayoutSettings.HealthBarAreaLeft.Width > HUDLayoutSettings.HealthBarAreaLeft.Height; + healthBar = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: horizontal ? "GUIProgressBar" : "GUIProgressBarVertical") + { + IsHorizontal = horizontal + }; + healthBarShadow = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: horizontal ? "GUIProgressBar" : "GUIProgressBarVertical") + { + IsHorizontal = horizontal + }; + healthShadowSize = 1.0f; + + afflictionInfoFrame = new GUIFrame(new RectTransform(new Point(HUDLayoutSettings.HealthWindowAreaLeft.Width / 2, 200), GUI.Canvas)); + var paddedInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), afflictionInfoFrame.RectTransform, Anchor.Center), style: null); + new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.08f), paddedInfoFrame.RectTransform), "", font: GUI.LargeFont) + { + UserData = "selectedlimbname" + }; + + afflictionInfoContainer = new GUIListBox(new RectTransform(new Vector2(0.7f, 0.85f), paddedInfoFrame.RectTransform, Anchor.BottomLeft)); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.08f), paddedInfoFrame.RectTransform), TextManager.Get("SuitableTreatments"), textAlignment: Alignment.TopRight); + lowSkillIndicator = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.07f), paddedInfoFrame.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(0.0f, 0.08f) }, + TextManager.Get("LowMedicalSkillWarning"), Color.Orange, textAlignment: Alignment.Center, font: GUI.SmallFont, wrap: true) + { + Visible = false + }; + recommendedTreatmentContainer = new GUIListBox(new RectTransform(new Vector2(0.28f, 0.5f), paddedInfoFrame.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(0.0f, 0.15f) }) + { + Spacing = 10 + }; + dropItemArea = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.3f), paddedInfoFrame.RectTransform, Anchor.BottomRight) + { RelativeOffset = new Vector2(0.0f, 0.0f) }, style: null) + { + ToolTip = TextManager.Get("HealthItemUseTip") + }; + dropItemArea.RectTransform.NonScaledSize = new Point(dropItemArea.Rect.Width); + + string[] healthCircleStyles = new string[] { "HealthCircleInner", "HealthCircleMid", "HealthCircleOuter" }; + foreach (string healthCircleStyle in healthCircleStyles) + { + for (int i = 1; i < 4; i++) + { + var style = GUI.Style.GetComponentStyle(healthCircleStyle + i); + if (style != null) + { + new GUIImage(new RectTransform(Vector2.One, dropItemArea.RectTransform), healthCircleStyle + i) + { + CanBeFocused = false + }; + } + } + } + + new GUIImage(new RectTransform(Vector2.One * 0.2f, dropItemArea.RectTransform, Anchor.Center), "HealthCross") + { + CanBeFocused = false + }; + + healthWindow = new GUIFrame(new RectTransform(new Point(100, 200), GUI.Canvas)); + var paddedHealthWindow = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), healthWindow.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.03f + }; + + var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedHealthWindow.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) + { + Stretch = true + }; + + characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) + { + AutoScale = true + }; + new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), nameContainer.RectTransform), + onDraw: (spriteBatch, component) => + { + character.Info.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), component.Rect.Width); + }); + + new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.9f), paddedHealthWindow.RectTransform), + (spriteBatch, component) => + { + DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true, false); + }, + (deltaTime, component) => + { + UpdateLimbIndicators(deltaTime, component.RectTransform.Rect); + } + ); + deadIndicator = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.1f), healthWindow.RectTransform, Anchor.Center), + text: TextManager.Get("Deceased"), font: GUI.LargeFont, textAlignment: Alignment.Center, wrap: true, style: "GUIToolTip") + { + Visible = false, + CanBeFocused = false + }; + + healthWindowHealthBar = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: "GUIProgressBarVertical") + { + IsHorizontal = false + }; + healthWindowHealthBarShadow = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: "GUIProgressBarVertical") + { + IsHorizontal = false + }; + cprButton = new GUIButton(new RectTransform(new Point(80, 80), GUI.Canvas), text: "", style: "CPRButton") + { + OnClicked = (button, userData) => + { + Character selectedCharacter = Character.Controlled?.SelectedCharacter; + if (selectedCharacter == null || (!selectedCharacter.IsUnconscious && selectedCharacter.Stun <= 0.0f)) return false; + + Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? + AnimController.Animation.None : AnimController.Animation.CPR; + + selectedCharacter.AnimController.ResetPullJoints(); + + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); + } + + return true; + }, + Visible = false + }; + + UpdateAlignment(); + + suicideButton = new GUIButton(new RectTransform(new Vector2(0.06f, 0.02f), GUI.Canvas, Anchor.TopCenter) + { MinSize = new Point(120, 20), RelativeOffset = new Vector2(0.0f, 0.01f) }, + TextManager.Get("GiveInButton")) + { + ToolTip = TextManager.Get(GameMain.NetworkMember == null ? "GiveInHelpSingleplayer" : "GiveInHelpMultiplayer"), + OnClicked = (button, userData) => + { + GUI.ForceMouseOn(null); + if (Character.Controlled != null) + { + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Status }); + } + else + { + var causeOfDeath = GetCauseOfDeath(); + Character.Controlled.Kill(causeOfDeath.First, causeOfDeath.Second); + Character.Controlled = null; + } + } + return true; + } + }; + + if (element != null) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "sprite": + limbIndicatorOverlay = new SpriteSheet(subElement); + break; + } + } + } + } + + private void OnAttacked(Character attacker, AttackResult attackResult) + { + if (Math.Abs(attackResult.Damage) < 0.01f && attackResult.Afflictions.Count == 0) return; + DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f); + if (healthShadowDelay <= 0.0f) healthShadowDelay = 1.0f; + + if (healthBarPulsateTimer <= 0.0f) healthBarPulsatePhase = 0.0f; + healthBarPulsateTimer = 1.0f; + + float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality)); + damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1); + } + + private void UpdateAlignment() + { + healthBar.RectTransform.RelativeOffset = healthBarShadow.RectTransform.RelativeOffset = Vector2.Zero; + healthWindowHealthBar.RectTransform.RelativeOffset = healthWindowHealthBarShadow.RectTransform.RelativeOffset = Vector2.Zero; + + int healthWindowHealthBarWidth = (int)(40 * GUI.Scale); + + if (alignment == Alignment.Left) + { + healthBar.RectTransform.SetPosition(Anchor.BottomLeft); + healthBarShadow.RectTransform.SetPosition(Anchor.BottomLeft); + healthBar.RectTransform.AbsoluteOffset = healthBarShadow.RectTransform.AbsoluteOffset = + new Point(HUDLayoutSettings.HealthBarAreaLeft.X, GameMain.GraphicsHeight - HUDLayoutSettings.HealthBarAreaLeft.Bottom); + healthBar.RectTransform.NonScaledSize = healthBarShadow.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarAreaLeft.Size; + + healthWindow.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthWindowAreaLeft.Location + new Point(healthWindowHealthBarWidth, 0); + healthWindow.RectTransform.NonScaledSize = new Point( + HUDLayoutSettings.HealthWindowAreaLeft.Width / 3 - healthWindowHealthBarWidth, + HUDLayoutSettings.HealthWindowAreaLeft.Height); + + afflictionInfoFrame.RectTransform.AbsoluteOffset = new Point( + healthWindow.Rect.Right, + HUDLayoutSettings.HealthWindowAreaLeft.Y); + afflictionInfoFrame.RectTransform.NonScaledSize = new Point( + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Width * 0.66f), + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Height)); + + healthWindowHealthBar.RectTransform.NonScaledSize = healthWindowHealthBarShadow.RectTransform.NonScaledSize = + new Point(healthWindowHealthBarWidth, healthWindow.Rect.Height); + healthWindowHealthBar.RectTransform.AbsoluteOffset = healthWindowHealthBarShadow.RectTransform.AbsoluteOffset = + HUDLayoutSettings.HealthWindowAreaLeft.Location; + + int cprButtonSize = (int)(100 * GUI.Scale); + cprButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.HealthWindowAreaLeft.Right, dropItemArea.Rect.Center.Y - cprButtonSize / 2); + cprButton.RectTransform.NonScaledSize = new Point(cprButtonSize); + } + else + { + healthBar.RectTransform.SetPosition(Anchor.TopLeft); + healthBarShadow.RectTransform.SetPosition(Anchor.TopLeft); + healthBar.RectTransform.AbsoluteOffset = healthBarShadow.RectTransform.AbsoluteOffset = + HUDLayoutSettings.HealthBarAreaRight.Location; + healthBar.RectTransform.NonScaledSize = healthBarShadow.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarAreaRight.Size; + + healthWindow.RectTransform.AbsoluteOffset = new Point( + HUDLayoutSettings.HealthWindowAreaRight.X + HUDLayoutSettings.HealthWindowAreaRight.Width / 3 * 2, + HUDLayoutSettings.HealthWindowAreaRight.Y); + healthWindow.RectTransform.NonScaledSize = new Point( + HUDLayoutSettings.HealthWindowAreaRight.Width / 3 - healthWindowHealthBarWidth, + HUDLayoutSettings.HealthWindowAreaRight.Height); + + afflictionInfoFrame.RectTransform.AbsoluteOffset = new Point( + HUDLayoutSettings.HealthWindowAreaRight.X, + HUDLayoutSettings.HealthWindowAreaLeft.Y); + afflictionInfoFrame.RectTransform.NonScaledSize = new Point( + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Width * 0.66f), + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Height)); + + healthWindowHealthBar.RectTransform.NonScaledSize = healthWindowHealthBarShadow.RectTransform.NonScaledSize = + new Point(healthWindowHealthBarWidth, healthWindow.Rect.Height); + healthWindowHealthBar.RectTransform.AbsoluteOffset = healthWindowHealthBarShadow.RectTransform.AbsoluteOffset = + new Point(HUDLayoutSettings.HealthWindowAreaRight.Right - healthWindowHealthBarWidth, HUDLayoutSettings.HealthWindowAreaRight.Y); + + int cprButtonSize = (int)(100 * GUI.Scale); + cprButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.HealthWindowAreaRight.X - cprButtonSize, dropItemArea.Rect.Center.Y - cprButtonSize / 2); + cprButton.RectTransform.NonScaledSize = new Point(cprButtonSize); + } + + dropItemArea.RectTransform.NonScaledSize = new Point(dropItemArea.Rect.Width); + + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + inventoryScale = Inventory.UIScale; + uiScale = GUI.Scale; + } + + partial void UpdateOxygenProjSpecific(float prevOxygen) + { + if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && + Character.Controlled == Character) + { + SoundPlayer.PlaySound(Character.Info != null && Character.Info.Gender == Gender.Female ? "drownfemale" : "drownmale"); + } + } + + partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime) + { + bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); + if (bloodParticleTimer <= 0.0f) + { + float bloodParticleSize = MathHelper.Lerp(0.5f, 1.0f, affliction.Strength / 100.0f); + if (!Character.AnimController.InWater) bloodParticleSize *= 2.0f; + var blood = GameMain.ParticleManager.CreateParticle( + Character.AnimController.InWater ? "waterblood" : "blooddrop", + targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); + + if (blood != null) + { + blood.Size *= bloodParticleSize; + } + bloodParticleTimer = 1.0f; + } + } + + public void UpdateHUD(float deltaTime) + { + if (GUI.DisableHUD) return; + if (openHealthWindow != null) + { + if (openHealthWindow != Character.Controlled?.CharacterHealth && openHealthWindow != Character.Controlled?.SelectedCharacter?.CharacterHealth) + { + openHealthWindow = null; + return; + } + } + + bool forceAfflictionContainerUpdate = false; + if (updateDisplayedAfflictionsTimer > 0.0f) + { + updateDisplayedAfflictionsTimer -= deltaTime; + } + else + { + forceAfflictionContainerUpdate = true; + currentDisplayedAfflictions = GetAllAfflictions(mergeSameAfflictions: true) + .FindAll(a => a.Strength >= a.Prefab.ShowIconThreshold && a.Prefab.Icon != null); + currentDisplayedAfflictions.Sort((a1, a2) => + { + int dmgPerSecond = Math.Sign(a2.DamagePerSecond - a1.DamagePerSecond); + return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(a1.Strength - a1.Strength); + }); + updateDisplayedAfflictionsTimer = UpdateDisplayedAfflictionsInterval; + } + + if (DamageOverlayTimer > 0.0f) + { + DamageOverlayTimer -= deltaTime; + } + if (damageIntensity > 0) + { + damageIntensity -= deltaTime * damageIntensityDropdownRate; + if (damageIntensity < 0) + { + damageIntensity = 0; + } + } + + if (healthShadowDelay > 0.0f) + { + healthShadowDelay -= deltaTime; + } + else + { + healthShadowSize = healthBar.BarSize > healthShadowSize ? + Math.Min(healthShadowSize + deltaTime, healthBar.BarSize) : + Math.Max(healthShadowSize - deltaTime, healthBar.BarSize); + } + + dropItemArea.Visible = !Character.IsDead; + + float blurStrength = 0.0f; + float distortStrength = 0.0f; + float distortSpeed = 0.0f; + float radialDistortStrength = 0.0f; + float chromaticAberrationStrength = 0.0f; + + if (Character.IsUnconscious) + { + blurStrength = 1.0f; + distortSpeed = 1.0f; + } + else if (OxygenAmount < 100.0f) + { + blurStrength = MathHelper.Lerp(0.5f, 1.0f, 1.0f - Vitality / MaxVitality); + distortStrength = blurStrength; + distortSpeed = (blurStrength + 1.0f); + distortSpeed *= distortSpeed * distortSpeed * distortSpeed; + } + + foreach (Affliction affliction in afflictions) + { + distortStrength = Math.Max(distortStrength, affliction.GetScreenDistortStrength()); + blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); + radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); + chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + } + foreach (LimbHealth limbHealth in limbHealths) + { + foreach (Affliction affliction in limbHealth.Afflictions) + { + distortStrength = Math.Max(distortStrength, affliction.GetScreenDistortStrength()); + blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); + radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); + chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + } + } + + Character.RadialDistortStrength = radialDistortStrength; + Character.ChromaticAberrationStrength = chromaticAberrationStrength; + if (blurStrength > 0.0f) + { + distortTimer = (distortTimer + deltaTime * distortSpeed) % MathHelper.TwoPi; + Character.BlurStrength = (float)(Math.Sin(distortTimer) + 1.5f) * 0.25f * blurStrength; + Character.DistortStrength = (float)(Math.Sin(distortTimer) + 1.0f) * 0.1f * distortStrength; + } + else + { + Character.BlurStrength = 0.0f; + Character.DistortStrength = 0.0f; + distortTimer = 0.0f; + } + + if (PlayerInput.KeyHit(InputType.Health) && GUI.KeyboardDispatcher.Subscriber == null && + Character.AllowInput && Character.FocusedCharacter == null && !toggledThisFrame) + { + if (openHealthWindow != null) + OpenHealthWindow = null; + else + { + OpenHealthWindow = this; + forceAfflictionContainerUpdate = true; + } + } + else if (openHealthWindow == this) + { + if (Alignment == Alignment.Right ? + HUD.CloseHUD(HUDLayoutSettings.HealthWindowAreaRight) : + HUD.CloseHUD(HUDLayoutSettings.HealthWindowAreaLeft)) + { + //emulate a Health input to get the character to deselect the item server-side + Character.Keys[(int)InputType.Health].Hit = true; + OpenHealthWindow = null; + } + } + toggledThisFrame = false; + + if (OpenHealthWindow == this) + { + var highlightedLimb = highlightedLimbIndex < 0 ? null : limbHealths[highlightedLimbIndex]; + if (highlightedLimbIndex < 0 && selectedLimbIndex < 0) + { + // If no limb is selected or highlighted, select the one with the most critical afflictions. + var affliction = GetAllAfflictions(a => a.Prefab.IndicatorLimb != LimbType.None) + .OrderByDescending(a => a.DamagePerSecond) + .ThenByDescending(a => a.Strength).FirstOrDefault(); + var limbHealth = GetMathingLimbHealth(affliction); + if (limbHealth != null) + { + selectedLimbIndex = limbHealths.IndexOf(limbHealth); + } + } + LimbHealth selectedLimb = selectedLimbIndex < 0 ? highlightedLimb : limbHealths[selectedLimbIndex]; + if (selectedLimb != currentDisplayedLimb || forceAfflictionContainerUpdate) + { + UpdateAfflictionContainer(selectedLimb); + currentDisplayedLimb = selectedLimb; + } + } + + if (Character.IsDead) + { + healthBar.Color = healthWindowHealthBar.Color = Color.Black; + healthBar.BarSize = healthWindowHealthBar.BarSize = 1.0f; + } + else + { + healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(Vitality / MaxVitality, Color.Red, Color.Orange, Color.Green); + healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; + healthBar.BarSize = healthWindowHealthBar.BarSize = + (Vitality > 0.0f) ? + (MaxVitality > 0.0f ? Vitality / MaxVitality : 0.0f) : + (Math.Abs(MinVitality) > 0.0f ? 1.0f - Vitality / MinVitality : 0.0f); + + if (healthBarPulsateTimer > 0.0f) + { + //0-1 + float pulsateAmount = (float)(Math.Sin(healthBarPulsatePhase) + 1.0f) / 2.0f; + + healthBar.RectTransform.LocalScale = healthBarShadow.RectTransform.LocalScale = new Vector2(1.0f, (1.0f + pulsateAmount * healthBarPulsateTimer * 0.5f)); + healthBarPulsatePhase += deltaTime * 5.0f; + healthBarPulsateTimer -= deltaTime; + } + else + { + healthBar.RectTransform.LocalScale = Vector2.One; + } + } + + if (OpenHealthWindow == this) + { + if (Character == Character.Controlled && !Character.AllowInput) + { + openHealthWindow = null; + } + + lowSkillIndicator.Visible = Timing.TotalTime % 1.0f < 0.8f && Character.Controlled != null && Character.Controlled.GetSkillLevel("medical") < 50.0f; + + float rotationSpeed = 0.25f; + int i = 0; + foreach (GUIComponent dropItemIndicator in dropItemArea.Children) + { + GUIImage img = dropItemIndicator as GUIImage; + if (img == null) continue; + + img.State = GUI.MouseOn == dropItemArea ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; + + byte alpha = img.Color.A; + byte hoverAlpha = img.HoverColor.A; + img.Color = ToolBox.GradientLerp(Vitality / MaxVitality, Color.Red, Color.Orange, Color.Green); + img.Color = new Color(img.Color.R, img.Color.G, img.Color.B, alpha); + img.HoverColor = new Color(img.Color.R, img.Color.G, img.Color.B, hoverAlpha); + img.HoverColor = Color.Lerp(img.HoverColor, Color.White, 0.5f); + + if (img.State == GUIComponent.ComponentState.Hover && droppedItem == null) + { + dropItemAnimTimer = Math.Min(0.3f, dropItemAnimTimer + deltaTime * 0.5f); + } + + if (i < 4) + { + img.Scale = 1.0f - (float)Math.Sin(dropItemAnimTimer / dropItemAnimDuration * MathHelper.TwoPi) * 0.3f; + } + + if (dropItemIndicator == dropItemArea.Children.Last()) break; + img.Rotation = (img.Rotation + (rotationSpeed + dropItemAnimTimer * 10.0f) * deltaTime) % MathHelper.TwoPi; + rotationSpeed = (rotationSpeed + 0.3f) % 1.0f; + + i++; + } + + if (Inventory.draggingItem != null) + { + if (highlightedLimbIndex > -1) + { + selectedLimbIndex = highlightedLimbIndex; + } + } + + if (draggingMed != null) + { + if (!PlayerInput.LeftButtonHeld()) + { + OnItemDropped(draggingMed.UserData as Item, ignoreMousePos: false); + draggingMed = null; + } + } + + /*if (GUI.MouseOn?.UserData is Affliction affliction) + { + ShowAfflictionInfo(affliction, afflictionInfoContainer); + }*/ + + if (dropItemAnimTimer > 0.0f) + { + dropItemAnimTimer -= deltaTime; + if (dropItemAnimTimer <= 0.0f) droppedItem = null; + } + } + else + { + if (openHealthWindow != null && Character != Character.Controlled && Character != Character.Controlled?.SelectedCharacter) + { + openHealthWindow = null; + } + highlightedLimbIndex = -1; + } + + Rectangle hoverArea = alignment == Alignment.Left ? + Rectangle.Union(HUDLayoutSettings.AfflictionAreaLeft, HUDLayoutSettings.HealthBarAreaLeft) : + Rectangle.Union(HUDLayoutSettings.AfflictionAreaRight, HUDLayoutSettings.HealthBarAreaRight); + + if (Character.AllowInput && UseHealthWindow && hoverArea.Contains(PlayerInput.MousePosition) && Inventory.SelectedSlot == null) + { + healthBar.State = GUIComponent.ComponentState.Hover; + if (PlayerInput.LeftButtonClicked()) + { + OpenHealthWindow = openHealthWindow == this ? null : this; + } + } + else + { + healthBar.State = GUIComponent.ComponentState.None; + } + + suicideButton.Visible = Character == Character.Controlled && Character.IsUnconscious && !Character.IsDead; + + cprButton.Visible = + Character == Character.Controlled?.SelectedCharacter + && (Character.IsUnconscious || Character.Stun > 0.0f) + && !Character.IsDead + && openHealthWindow == this; + + deadIndicator.Visible = Character.IsDead; + } + + public void AddToGUIUpdateList() + { + if (GUI.DisableHUD) return; + if (OpenHealthWindow == this) + { + //afflictionContainer.AddToGUIUpdateList(); + afflictionInfoFrame.AddToGUIUpdateList(); + healthWindow.AddToGUIUpdateList(); + healthWindowHealthBarShadow.AddToGUIUpdateList(); + healthWindowHealthBar.AddToGUIUpdateList(); + } + else if (Character.Controlled == Character) + { + healthBarShadow.AddToGUIUpdateList(); + healthBar.AddToGUIUpdateList(); + } + if (suicideButton.Visible && Character == Character.Controlled) suicideButton.AddToGUIUpdateList(); + if (cprButton != null && cprButton.Visible) cprButton.AddToGUIUpdateList(); + } + + public void DrawHUD(SpriteBatch spriteBatch) + { + if (GUI.DisableHUD) return; + if (GameMain.GraphicsWidth != screenResolution.X || + GameMain.GraphicsHeight != screenResolution.Y || + Math.Abs(inventoryScale - Inventory.UIScale) > 0.01f || + Math.Abs(uiScale - GUI.Scale) > 0.01f) + { + UpdateAlignment(); + } + + float damageOverlayAlpha = DamageOverlayTimer; + if (Vitality < MaxVitality * 0.1f) + { + damageOverlayAlpha = Math.Max(1.0f - (Vitality / maxVitality * 10.0f), damageOverlayAlpha); + } + else + { + float pulsateAmount = (float)(Math.Sin(healthBarPulsatePhase) + 1.0f) / 2.0f; + damageOverlayAlpha = pulsateAmount * healthBarPulsateTimer * damageIntensity; + } + + if (damageOverlayAlpha > 0.0f) + { + damageOverlay.Draw(spriteBatch, Vector2.Zero, Color.White * damageOverlayAlpha, Vector2.Zero, 0.0f, + new Vector2(GameMain.GraphicsWidth / damageOverlay.size.X, GameMain.GraphicsHeight / damageOverlay.size.Y)); + } + + if (Character.Inventory != null) + { + if (Character.Inventory.CurrentLayout == CharacterInventory.Layout.Right) + { + //move the healthbar on top of the inventory slots + healthBar.RectTransform.ScreenSpaceOffset = new Point( + (GameMain.GraphicsWidth - HUDLayoutSettings.Padding) - HUDLayoutSettings.HealthBarAreaRight.Right, + HUDLayoutSettings.HealthBarAreaRight.Y - (int)(Character.Inventory.SlotPositions.Max(s => s.Y) + Inventory.EquipIndicator.size.Y * Inventory.UIScale * 2) - HUDLayoutSettings.HealthBarAreaRight.Height); + healthBarShadow.RectTransform.ScreenSpaceOffset = healthBar.RectTransform.ScreenSpaceOffset; + } + else + { + healthBar.RectTransform.ScreenSpaceOffset = healthBarShadow.RectTransform.ScreenSpaceOffset = Point.Zero; + } + } + + DrawStatusHUD(spriteBatch); + } + + public void DrawStatusHUD(SpriteBatch spriteBatch) + { + //Rectangle interactArea = healthBar.Rect; + if (openHealthWindow != this) + { + List> statusIcons = new List>(); + if (Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 5.0f) + statusIcons.Add(new Pair(pressureAffliction, TextManager.Get("PressureHUDWarning"))); + if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) + statusIcons.Add(new Pair(oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + + foreach (Affliction affliction in currentDisplayedAfflictions) + { + statusIcons.Add(new Pair(affliction, affliction.Prefab.Name)); + } + + Pair highlightedIcon = null; + Vector2 highlightedIconPos = Vector2.Zero; + Rectangle afflictionArea = alignment == Alignment.Left ? HUDLayoutSettings.AfflictionAreaLeft : HUDLayoutSettings.AfflictionAreaRight; + Point pos = afflictionArea.Location + healthBar.RectTransform.ScreenSpaceOffset; + + bool horizontal = afflictionArea.Width > afflictionArea.Height; + int iconSize = horizontal ? afflictionArea.Height : afflictionArea.Width; + + foreach (Pair statusIcon in statusIcons) + { + Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); + if (afflictionIconRect.Contains(PlayerInput.MousePosition)) + { + highlightedIcon = statusIcon; + highlightedIconPos = afflictionIconRect.Center.ToVector2(); + } + + if (statusIcon.First.DamagePerSecond > 1.0f) + { + Rectangle glowRect = afflictionIconRect; + glowRect.Inflate((int)(25 * GUI.Scale), (int)(25 * GUI.Scale)); + var glow = GUI.Style.GetComponentStyle("OuterGlow"); + glow.Sprites[GUIComponent.ComponentState.None][0].Draw( + spriteBatch, glowRect, + Color.Red * (float)((Math.Sin(statusIcon.First.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); + } + + var slot = GUI.Style.GetComponentStyle("AfflictionIconSlot"); + slot.Sprites[highlightedIcon == statusIcon ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None][0].Draw( + spriteBatch, afflictionIconRect, + highlightedIcon == statusIcon ? slot.HoverColor : slot.Color); + + + statusIcon.First.Prefab.Icon?.Draw(spriteBatch, + pos.ToVector2(), + highlightedIcon == statusIcon ? statusIcon.First.Prefab.IconColor : statusIcon.First.Prefab.IconColor * 0.8f, + rotate: 0, + scale: iconSize / statusIcon.First.Prefab.Icon.size.X); + + if (horizontal) + pos.X += iconSize + (int)(5 * GUI.Scale); + else + pos.Y += iconSize + (int)(5 * GUI.Scale); + } + + if (highlightedIcon != null) + { + GUI.DrawString(spriteBatch, + alignment == Alignment.Left ? highlightedIconPos + new Vector2(60 * GUI.Scale, 5) : highlightedIconPos + new Vector2(-10.0f - GUI.Font.MeasureString(highlightedIcon.Second).X, 5), + highlightedIcon.Second, + Color.White * 0.8f, Color.Black * 0.5f); + } + + if (Vitality > 0.0f) + { + float currHealth = healthBar.BarSize; + Color prevColor = healthBar.Color; + healthBarShadow.BarSize = healthShadowSize; + healthBarShadow.Color = Color.Red; + healthBarShadow.Visible = true; + healthBar.BarSize = currHealth; + healthBar.Color = prevColor; + } + else + { + healthBarShadow.Visible = false; + } + } + else + { + if (Vitality > 0.0f) + { + float currHealth = healthWindowHealthBar.BarSize; + Color prevColor = healthWindowHealthBar.Color; + healthWindowHealthBarShadow.BarSize = healthShadowSize; + healthWindowHealthBarShadow.Color = Color.Red; + healthWindowHealthBarShadow.Visible = true; + healthWindowHealthBar.BarSize = currHealth; + healthWindowHealthBar.Color = prevColor; + } + else + { + healthWindowHealthBarShadow.Visible = false; + } + } + } + + private void UpdateAfflictionContainer(LimbHealth selectedLimb) + { + ((GUITextBlock)afflictionInfoContainer.Parent.GetChildByUserData("selectedlimbname")).Text = selectedLimb == null ? "" : selectedLimb.Name; + + if (selectedLimb == null) + { + afflictionInfoContainer.Content.ClearChildren(); + return; + } + var currentAfflictions = GetMatchingAfflictions(selectedLimb, a => a.Strength >= a.Prefab.ShowIconThreshold); + var displayedAfflictions = afflictionInfoContainer.Content.Children.Select(c => c.UserData as Affliction); + if (currentAfflictions.Any(a => !displayedAfflictions.Contains(a)) || + displayedAfflictions.Any(a => !currentAfflictions.Contains(a))) + { + CreateAfflictionInfos(currentAfflictions); + } + + UpdateAfflictionInfos(displayedAfflictions); + } + + private void CreateAfflictionInfos(IEnumerable afflictions) + { + afflictionInfoContainer.Content.ClearChildren(); + recommendedTreatmentContainer.Content.ClearChildren(); + + float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical"); + + //random variance is 200% when the skill is 0 + //no random variance if the skill is 50 or more + float randomVariance = MathHelper.Lerp(2.0f, 0.0f, characterSkillLevel / 50.0f); + + //key = item identifier + //float = suitability + Dictionary treatmentSuitability = new Dictionary(); + float minSuitability = -10, maxSuitability = 10; + foreach (Affliction affliction in afflictions) + { + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) + { + if (!treatmentSuitability.ContainsKey(treatment.Key)) + { + treatmentSuitability[treatment.Key] = treatment.Value * affliction.Strength; + } + else + { + treatmentSuitability[treatment.Key] += treatment.Value * affliction.Strength; + } + minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability); + maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability); + } + } + //normalize the suitabilities to a range of 0 to 1 + foreach (string treatment in treatmentSuitability.Keys.ToList()) + { + treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); + //lerp towards a random value if the medical skill is low + treatmentSuitability[treatment] = MathHelper.Lerp(treatmentSuitability[treatment], Rand.Range(0.0f, 1.0f), randomVariance); + } + + foreach (Affliction affliction in afflictions) + { + var child = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, afflictionInfoContainer.Content.RectTransform, Anchor.TopCenter)) + { + Stretch = true, + RelativeSpacing = 0.02f, + UserData = affliction + }; + + var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), child.RectTransform), isHorizontal: true) + { + Stretch = true, + UserData = "header" + }; + + new GUIImage(new RectTransform(new Vector2(0.15f, 1.0f), headerContainer.RectTransform), affliction.Prefab.Icon, scaleToFit: true) + { + Color = affliction.Prefab.IconColor + }; + + var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), headerContainer.RectTransform), isHorizontal: true) + { + Stretch = true, + AbsoluteSpacing = 10, + UserData = "label" + }; + var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont); + var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUI.LargeFont) + { + Padding = Vector4.Zero, + UserData = "strength" + }; + var vitality = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), labelContainer.RectTransform, Anchor.BottomRight), "", textAlignment: Alignment.BottomRight) + { + IgnoreLayoutGroups = true, + UserData = "vitality" + }; + + var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), child.RectTransform), + affliction.Prefab.Description, textAlignment: Alignment.TopLeft, wrap: true); + if (description.Font.MeasureString(description.WrappedText).Y > description.Rect.Height) + { + description.Font = GUI.SmallFont; + } + description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); + child.RectTransform.Resize(new Point(child.Rect.Width, child.Children.Sum(c => c.Rect.Height))); + child.Recalculate(); + headerContainer.Recalculate(); + labelContainer.Recalculate(); + afflictionStrength.AutoScale = true; + afflictionName.AutoScale = true; + vitality.AutoDraw = true; + } + + List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); + + foreach (KeyValuePair treatment in treatmentSuitabilities) + { + ItemPrefab item = MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) as ItemPrefab; + if (item == null) continue; + int slotSize = (int)(recommendedTreatmentContainer.Content.Rect.Width * 0.8f); + + var itemSlot = new GUIButton(new RectTransform(new Point(slotSize), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopCenter), + text: "", style: "InventorySlotSmall") + { + UserData = item + }; + itemSlot.Color = ToolBox.GradientLerp(treatment.Value, Color.Red, Color.White, Color.LightGreen); + + Sprite itemSprite = item.InventoryIcon ?? item.sprite; + Color itemColor = itemSprite == item.sprite ? item.SpriteColor : item.InventoryIconColor; + var itemIcon = new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), itemSlot.RectTransform, Anchor.Center), + itemSprite, scaleToFit: true) + { + CanBeFocused = false, + Color = itemColor, + HoverColor = itemColor, + SelectedColor = itemColor + }; + itemSlot.ToolTip = item.Name + "\n" + item.Description; + } + + afflictionInfoContainer.Content.RectTransform.SortChildren((r1, r2) => + { + var first = r1.GUIComponent.UserData as Affliction; + var second = r2.GUIComponent.UserData as Affliction; + int dmgPerSecond = Math.Sign(second.DamagePerSecond - first.DamagePerSecond); + return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(second.Strength - first.Strength); + }); + + //afflictionInfoContainer.Content.RectTransform.SortChildren((r1, r2) => + //{ + // return Math.Sign(((Affliction)r2.GUIComponent.UserData).GetVitalityDecrease(this) - ((Affliction)r1.GUIComponent.UserData).GetVitalityDecrease(this)); + //}); + } + + private void UpdateAfflictionInfos(IEnumerable afflictions) + { + foreach (Affliction affliction in afflictions) + { + var child = afflictionInfoContainer.Content.FindChild(affliction); + var headerContainer = child.GetChildByUserData("header"); + var labelContainer = headerContainer.GetChildByUserData("label"); + var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock; + + strengthText.Text = strengthTexts[ + MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + + strengthText.TextColor = ToolBox.GradientLerp( + affliction.Strength / affliction.Prefab.MaxStrength, + Color.Yellow, Color.Orange, Color.Red); + + var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock; + int vitalityDecrease = (int)affliction.GetVitalityDecrease(this); + if (vitalityDecrease == 0) + { + vitalityText.Visible = false; + } + else + { + vitalityText.Visible = true; + vitalityText.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; + vitalityText.TextColor = vitalityDecrease <= 0 ? Color.LightGreen : + Color.Lerp(Color.Orange, Color.Red, affliction.Strength / affliction.Prefab.MaxStrength); + } + } + } + + public bool OnItemDropped(Item item, bool ignoreMousePos) + { + //items can be dropped outside the health window + if (!ignoreMousePos && + !healthWindow.Rect.Contains(PlayerInput.MousePosition) && + !afflictionInfoFrame.Rect.Contains(PlayerInput.MousePosition)) + { + return false; + } + + //can't apply treatment to dead characters + if (Character.IsDead) return true; + if (item == null || !item.UseInHealthInterface) return true; + if (!ignoreMousePos) + { + if (highlightedLimbIndex > -1) + { + selectedLimbIndex = highlightedLimbIndex; + } + else if (!dropItemArea.Rect.Contains(PlayerInput.MousePosition)) + { + return true; + } + } + + Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); + + item.ApplyTreatment(Character.Controlled, Character, targetLimb); + + dropItemAnimTimer = dropItemAnimDuration; + droppedItem = item; + return true; + } + + private List GetAvailableMedicalItems() + { + List allInventoryItems = new List(); + allInventoryItems.AddRange(Character.Inventory.Items); + if (Character.SelectedCharacter?.Inventory != null && Character.CanAccessInventory(Character.SelectedCharacter.Inventory)) + { + allInventoryItems.AddRange(Character.SelectedCharacter.Inventory.Items); + } + if (Character.SelectedBy?.Inventory != null) + { + allInventoryItems.AddRange(Character.SelectedBy.Inventory.Items); + } + + List medicalItems = new List(); + foreach (Item item in allInventoryItems) + { + if (item == null) continue; + + var containedItems = item.ContainedItems; + if (containedItems != null) + { + foreach (Item containedItem in containedItems) + { + if (containedItem == null) continue; + if (!containedItem.HasTag("medical") && !containedItem.HasTag("chem")) continue; + medicalItems.Add(containedItem); + } + } + + if (!item.HasTag("medical") && !item.HasTag("chem")) continue; + medicalItems.Add(item); + } + + return medicalItems.Distinct().ToList(); + } + + private void UpdateLimbIndicators(float deltaTime, Rectangle drawArea) + { + limbIndicatorOverlayAnimState += deltaTime * 8.0f; + + highlightedLimbIndex = -1; + int i = 0; + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite == null) continue; + + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + + Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea); + + if (highlightArea.Contains(PlayerInput.MousePosition)) + { + highlightedLimbIndex = i; + } + i++; + } + + if (PlayerInput.LeftButtonClicked() && highlightedLimbIndex > -1) + { + selectedLimbIndex = highlightedLimbIndex; + //afflictionContainer.ClearChildren(); + afflictionInfoContainer.ClearChildren(); + } + } + + private void DrawHealthWindow(SpriteBatch spriteBatch, Rectangle drawArea, bool allowHighlight, bool highlightAll) + { + if (Character.Removed) { return; } + + int i = 0; + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite == null) continue; + + float damageLerp = limbHealth.TotalDamage > 0.0f ? MathHelper.Lerp(0.2f, 1.0f, limbHealth.TotalDamage / 100.0f) : 0.0f; + Color color = Character.IsDead ? + Color.Lerp(Color.Black, new Color(150, 100, 100), damageLerp) : + ToolBox.GradientLerp(damageLerp, Color.Green, Color.Orange, Color.Red); + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + + if (((i == highlightedLimbIndex || i == selectedLimbIndex) && allowHighlight) || highlightAll) + { + color = Color.Lerp(color, Color.White, 0.5f); + } + + limbHealth.IndicatorSprite.Draw(spriteBatch, + drawArea.Center.ToVector2(), color, + limbHealth.IndicatorSprite.Origin, + 0, scale); + i++; + } + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative); + + float overlayScale = Math.Min( + drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X, + drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y); + + int frame = 0; + int frameCount = 17; + if (limbIndicatorOverlayAnimState >= frameCount * 2) limbIndicatorOverlayAnimState = 0.0f; + if (limbIndicatorOverlayAnimState < frameCount) + { + frame = (int)limbIndicatorOverlayAnimState; + } + else + { + frame = frameCount - (int)(limbIndicatorOverlayAnimState - (frameCount - 1)); + } + + limbIndicatorOverlay.Draw(spriteBatch, frame, drawArea.Center.ToVector2(), Color.Gray, origin: limbIndicatorOverlay.FrameSize.ToVector2() / 2, rotate: 0.0f, + scale: Vector2.One * overlayScale); + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, blendState: BlendState.AlphaBlend, rasterizerState: GameMain.ScissorTestEnable); + + i = 0; + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite == null) continue; + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + + Rectangle highlightArea = new Rectangle( + (int)(drawArea.Center.X - (limbHealth.IndicatorSprite.Texture.Width / 2 - limbHealth.HighlightArea.X) * scale), + (int)(drawArea.Center.Y - (limbHealth.IndicatorSprite.Texture.Height / 2 - limbHealth.HighlightArea.Y) * scale), + (int)(limbHealth.HighlightArea.Width * scale), + (int)(limbHealth.HighlightArea.Height * scale)); + + if (selectedLimbIndex == i) + { + if (alignment == Alignment.Left) + { + GUI.DrawLine(spriteBatch, + highlightArea.Center.ToVector2(), + afflictionInfoContainer.Parent.Rect.Location.ToVector2() + Vector2.UnitY * 20, + Color.LightBlue * 0.3f, 0, 4); + } + else + { + GUI.DrawLine(spriteBatch, + highlightArea.Center.ToVector2(), + new Vector2(afflictionInfoContainer.Parent.Rect.Right, afflictionInfoContainer.Parent.Rect.Y + 20), + Color.LightBlue * 0.3f, 0, 4); + } + } + + var slot = GUI.Style.GetComponentStyle("AfflictionIconSlot"); + + float iconScale = 0.3f * scale; + Vector2 iconPos = highlightArea.Center.ToVector2(); + foreach (Affliction affliction in limbHealth.Afflictions) + { + DrawLimbAfflictionIcon(spriteBatch, affliction, slot, iconScale, ref iconPos); + } + + foreach (Affliction affliction in afflictions) + { + Limb indicatorLimb = Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb); + if (indicatorLimb != null && indicatorLimb.HealthIndex == i) + { + DrawLimbAfflictionIcon(spriteBatch, affliction, slot, iconScale, ref iconPos); + } + } + i++; + } + + if (draggingMed != null) + { + GUIImage itemImage = draggingMed.GetChild(); + float scale = Math.Min(40.0f / itemImage.Sprite.size.X, 40.0f / itemImage.Sprite.size.Y); + itemImage.Sprite.Draw(spriteBatch, PlayerInput.MousePosition, itemImage.Color, 0, scale); + } + + if (dropItemAnimTimer > 0.0f && droppedItem?.Prefab.InventoryIcon != null) + { + var droppedItemSprite = droppedItem.Prefab.InventoryIcon ?? droppedItem.Sprite; + droppedItemSprite.Draw(spriteBatch, dropItemArea.Rect.Center.ToVector2(), + droppedItemSprite == droppedItem.Sprite ? droppedItem.GetSpriteColor() : droppedItem.GetInventoryIconColor(), + origin: droppedItemSprite.size / 2, + scale: MathHelper.SmoothStep(0.0f, 100.0f / droppedItemSprite.size.Length(), dropItemAnimTimer / dropItemAnimDuration)); + } + } + + private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, GUIComponentStyle slotStyle, float iconScale, ref Vector2 iconPos) + { + if (affliction.Strength < affliction.Prefab.ShowIconThreshold) return; + Vector2 iconSize = (affliction.Prefab.Icon.size * iconScale); + + //afflictions that have a strength of less than 10 are faded out slightly + float alpha = MathHelper.Lerp(0.3f, 1.0f, + (affliction.Strength - affliction.Prefab.ShowIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - affliction.Prefab.ShowIconThreshold, 10.0f)); + + slotStyle.Sprites[GUIComponent.ComponentState.None][0].Draw( + spriteBatch, + new Rectangle((iconPos - iconSize / 2.0f).ToPoint(), iconSize.ToPoint()), + slotStyle.Color * alpha); + affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, affliction.Prefab.IconColor * alpha, 0, iconScale); + iconPos += new Vector2(10.0f, 20.0f) * iconScale; + } + + private Rectangle GetLimbHighlightArea(LimbHealth limbHealth, Rectangle drawArea) + { + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + return new Rectangle( + (int)(drawArea.Center.X - (limbHealth.IndicatorSprite.Texture.Width / 2 - limbHealth.HighlightArea.X) * scale), + (int)(drawArea.Center.Y - (limbHealth.IndicatorSprite.Texture.Height / 2 - limbHealth.HighlightArea.Y) * scale), + (int)(limbHealth.HighlightArea.Width * scale), + (int)(limbHealth.HighlightArea.Height * scale)); + } + + public void ClientRead(NetBuffer inc) + { + List> newAfflictions = new List>(); + + byte afflictionCount = inc.ReadByte(); + for (int i = 0; i < afflictionCount; i++) + { + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List[inc.ReadRangedInteger(0, AfflictionPrefab.List.Count - 1)]; + float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); + + newAfflictions.Add(new Pair(afflictionPrefab, afflictionStrength)); + } + + foreach (Affliction affliction in afflictions) + { + //deactivate afflictions that weren't included in the network message + if (!newAfflictions.Any(a => a.First == affliction.Prefab)) + { + affliction.Strength = 0.0f; + } + } + + foreach (Pair newAffliction in newAfflictions) + { + Affliction existingAffliction = afflictions.Find(a => a.Prefab == newAffliction.First); + if (existingAffliction == null) + { + afflictions.Add(newAffliction.First.Instantiate(newAffliction.Second)); + } + else + { + existingAffliction.Strength = newAffliction.Second; + if (existingAffliction == stunAffliction) Character.SetStun(existingAffliction.Strength, true, true); + } + } + + List> newLimbAfflictions = new List>(); + byte limbAfflictionCount = inc.ReadByte(); + for (int i = 0; i < limbAfflictionCount; i++) + { + int limbIndex = inc.ReadRangedInteger(0, limbHealths.Count - 1); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List[inc.ReadRangedInteger(0, AfflictionPrefab.List.Count - 1)]; + float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); + + newLimbAfflictions.Add(new Triplet(limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); + } + + foreach (LimbHealth limbHealth in limbHealths) + { + foreach (Affliction affliction in limbHealth.Afflictions) + { + //deactivate afflictions that weren't included in the network message + if (!newLimbAfflictions.Any(a => a.First == limbHealth && a.Second == affliction.Prefab)) + { + affliction.Strength = 0.0f; + } + } + + foreach (Triplet newAffliction in newLimbAfflictions) + { + if (newAffliction.First != limbHealth) continue; + Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == newAffliction.Second); + if (existingAffliction == null) + { + limbHealth.Afflictions.Add(newAffliction.Second.Instantiate(newAffliction.Third)); + } + else + { + existingAffliction.Strength = newAffliction.Third; + } + } + } + } + partial void UpdateLimbAfflictionOverlays() { foreach (Limb limb in Character.AnimController.Limbs) @@ -18,5 +1419,20 @@ limb.DamageOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count; } } + + partial void RemoveProjSpecific() + { + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite != null) + { + limbHealth.IndicatorSprite.Remove(); + limbHealth.IndicatorSprite = null; + } + } + + limbIndicatorOverlay?.Remove(); + limbIndicatorOverlay = null; + } } } diff --git a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs index db0bce447..8f40088a7 100644 --- a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs @@ -403,6 +403,11 @@ namespace Barotrauma AssignRelayToServer("help", false); AssignRelayToServer("verboselogging", false); AssignRelayToServer("freecam", false); +#if DEBUG + AssignRelayToServer("simulatedlatency", false); + AssignRelayToServer("simulatedloss", false); + AssignRelayToServer("simulatedduplicateschance", false); +#endif commands.Add(new Command("clientlist", "", (string[] args) => { })); AssignRelayToServer("clientlist", true); @@ -439,7 +444,7 @@ namespace Barotrauma AssignOnExecute("ambientlight", (string[] args) => { - Color color = XMLExtensions.ParseColor(string.Join("", args)); + Color color = XMLExtensions.ParseColor(string.Join(",", args)); if (Level.Loaded != null) { Level.Loaded.GenerationParams.AmbientLightColor = color; @@ -528,6 +533,28 @@ namespace Barotrauma } }, isCheat: true)); + commands.Add(new Command("resetall", "Reset all items and structures to prefabs. Only applicable in the subeditor.", args => + { + if (Screen.Selected == GameMain.SubEditorScreen) + { + Item.ItemList.ForEach(i => i.Reset()); + Structure.WallList.ForEach(s => s.Reset()); + foreach (MapEntity entity in MapEntity.SelectedList) + { + if (entity is Item item) + { + item.CreateEditingHUD(); + break; + } + else if (entity is Structure structure) + { + structure.CreateEditingHUD(); + break; + } + } + } + })); + commands.Add(new Command("alpha", "Change the alpha (as bytes from 0 to 255) of the selected item/structure instances. Applied only in the subeditor.", (string[] args) => { if (Screen.Selected == GameMain.SubEditorScreen) @@ -3786,6 +3813,201 @@ namespace Barotrauma NPCConversation.WriteToCSV(); })); + commands.Add(new Command("csvtoxml", "csvtoxml [language] -> Converts .csv localization files in Content/NPCConversations & Content/Texts to .xml for use in-game.", (string[] args) => + { + if (args.Length == 0) return; + LocalizationCSVtoXML.Convert(args[0]); + })); +#endif + + commands.Add(new Command("cleanbuild", "", (string[] args) => + { + GameMain.Config.MusicVolume = 0.5f; + GameMain.Config.SoundVolume = 0.5f; + NewMessage("Music and sound volume set to 0.5", Color.Green); + + commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) => + { + float defaultZoom = Screen.Selected.Cam.DefaultZoom; + if (args.Length > 0) float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out defaultZoom); + + float zoomSmoothness = Screen.Selected.Cam.ZoomSmoothness; + if (args.Length > 1) float.TryParse(args[1], NumberStyles.Number, CultureInfo.InvariantCulture, out zoomSmoothness); + float moveSmoothness = Screen.Selected.Cam.MoveSmoothness; + if (args.Length > 2) float.TryParse(args[2], NumberStyles.Number, CultureInfo.InvariantCulture, out moveSmoothness); + + float minZoom = Screen.Selected.Cam.MinZoom; + if (args.Length > 3) float.TryParse(args[3], NumberStyles.Number, CultureInfo.InvariantCulture, out minZoom); + float maxZoom = Screen.Selected.Cam.MaxZoom; + if (args.Length > 4) float.TryParse(args[4], NumberStyles.Number, CultureInfo.InvariantCulture, out maxZoom); + + Screen.Selected.Cam.DefaultZoom = defaultZoom; + Screen.Selected.Cam.ZoomSmoothness = zoomSmoothness; + Screen.Selected.Cam.MoveSmoothness = moveSmoothness; + Screen.Selected.Cam.MinZoom = minZoom; + Screen.Selected.Cam.MaxZoom = maxZoom; + })); + + GameMain.Config.SaveNewPlayerConfig(); + + + commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) => + { + //TODO: maybe do this automatically during loading when possible? + if (Screen.Selected == GameMain.SubEditorScreen) + { + if (!MapEntity.SelectedAny) + { + ThrowError("You have to select item(s) first!"); + } + else + { + foreach (var mapEntity in MapEntity.SelectedList) + { + if (mapEntity is Item item) + { + item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, + (int)(item.Prefab.sprite.size.X * item.Prefab.Scale), + (int)(item.Prefab.sprite.size.Y * item.Prefab.Scale)); + } + else if (mapEntity is Structure structure) + { + if (!structure.ResizeHorizontal) + { + structure.Rect = new Rectangle(structure.Rect.X, structure.Rect.Y, + (int)structure.Prefab.ScaledSize.X, + structure.Rect.Height); + } + if (!structure.ResizeVertical) + { + structure.Rect = new Rectangle(structure.Rect.X, structure.Rect.Y, + structure.Rect.Width, + (int)structure.Prefab.ScaledSize.Y); + } + } + } + } + } + }, isCheat: false)); +#endif + + commands.Add(new Command("dumptexts", "dumptexts [filepath]: Extracts all the texts from the given text xml and writes them into a file (using the same filename, but with the .txt extension). If the filepath is omitted, the EnglishVanilla.xml file is used.", (string[] args) => + { + string filePath = args.Length > 0 ? args[0] : "Content/Texts/EnglishVanilla.xml"; + var doc = XMLExtensions.TryLoadXml(filePath); + if (doc?.Root == null) return; + List lines = new List(); + foreach (XElement element in doc.Root.Elements()) + { + lines.Add(element.ElementInnerText()); + } + File.WriteAllLines(Path.GetFileNameWithoutExtension(filePath) + ".txt", lines); + }, + () => + { + var files = TextManager.GetTextFiles().Select(f => f.Replace("\\", "/")); + return new string[][] + { + TextManager.GetTextFiles().Where(f => Path.GetExtension(f)==".xml").ToArray() + }; + })); + + commands.Add(new Command("loadtexts", "loadtexts [sourcefile] [destinationfile]: Loads all lines of text from a given .txt file and inserts them sequientially into the elements of an xml file. If the file paths are omitted, EnglishVanilla.txt and EnglishVanilla.xml are used.", (string[] args) => + { + string sourcePath = args.Length > 0 ? args[0] : "Content/Texts/EnglishVanilla.txt"; + string destinationPath = args.Length > 1 ? args[1] : "Content/Texts/EnglishVanilla.xml"; + + string[] lines; + try + { + lines = File.ReadAllLines(sourcePath); + } + catch (Exception e) + { + ThrowError("Reading the file \"" + sourcePath + "\" failed.", e); + return; + } + var doc = XMLExtensions.TryLoadXml(destinationPath); + int i = 0; + foreach (XElement element in doc.Root.Elements()) + { + if (i >= lines.Length) + { + ThrowError("Error while loading texts to the xml file. The xml has more elements than the number of lines in the text file."); + return; + } + element.Value = lines[i]; + i++; + } + doc.Save(destinationPath); + }, + () => + { + var files = TextManager.GetTextFiles().Select(f => f.Replace("\\", "/")); + return new string[][] + { + files.Where(f => Path.GetExtension(f)==".txt").ToArray(), + files.Where(f => Path.GetExtension(f)==".xml").ToArray() + }; + })); + + commands.Add(new Command("updatetextfile", "updatetextfile [sourcefile] [destinationfile]: Inserts all the xml elements that are only present in the source file into the destination file. Can be used to update outdated translation files more easily.", (string[] args) => + { + if (args.Length < 2) return; + string sourcePath = args[0]; + string destinationPath = args[1]; + + var sourceDoc = XMLExtensions.TryLoadXml(sourcePath); + var destinationDoc = XMLExtensions.TryLoadXml(destinationPath); + + XElement destinationElement = destinationDoc.Root.Elements().First(); + foreach (XElement element in sourceDoc.Root.Elements()) + { + if (destinationDoc.Root.Element(element.Name) == null) + { + element.Value = "!!!!!!!!!!!!!" + element.Value; + destinationElement.AddAfterSelf(element); + } + XNode nextNode = destinationElement.NextNode; + while ((!(nextNode is XElement) || nextNode == element) && nextNode != null) nextNode = nextNode.NextNode; + destinationElement = nextNode as XElement; + } + destinationDoc.Save(destinationPath); + }, + () => + { + var files = TextManager.GetTextFiles().Where(f => Path.GetExtension(f) == ".xml").Select(f => f.Replace("\\", "/")).ToArray(); + return new string[][] + { + files, + files + }; + })); + + commands.Add(new Command("dumpentitytexts", "dumpentitytexts [filepath]: gets the names and descriptions of all entity prefabs and writes them into a file along with xml tags that can be used in translation files. If the filepath is omitted, the file is written to Content/Texts/EntityTexts.txt", (string[] args) => + { + string filePath = args.Length > 0 ? args[0] : "Content/Texts/EntityTexts.txt"; + List lines = new List(); + foreach (MapEntityPrefab me in MapEntityPrefab.List) + { + lines.Add("" + me.Name + ""); + lines.Add("" + me.Description + ""); + } + File.WriteAllLines(filePath, lines); + })); +#if DEBUG + commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => + { + if (args.Length != 1) return; + TextManager.CheckForDuplicates(args[0]); + })); + + commands.Add(new Command("writetocsv", "Writes the default language (English) to a .csv file.", (string[] args) => + { + TextManager.WriteToCSV(); + NPCConversation.WriteToCSV(); + })); + commands.Add(new Command("csvtoxml", "csvtoxml [language] -> Converts .csv localization files in Content/NPCConversations & Content/Texts to .xml for use in-game.", (string[] args) => { if (args.Length == 0) return; diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs index 180dccbc3..916ebef8a 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs @@ -663,20 +663,6 @@ namespace Barotrauma msg.Timer -= deltaTime; msg.Pos += msg.Velocity * deltaTime; } - - foreach (GUIMessage msg in messages) - { - if (!msg.WorldSpace) continue; - msg.Timer -= deltaTime; - msg.Pos += msg.Velocity * deltaTime; - } - - foreach (GUIMessage msg in messages) - { - if (!msg.WorldSpace) continue; - msg.Timer -= deltaTime; - msg.Pos += msg.Velocity * deltaTime; - } } messages.RemoveAll(m => m.Timer <= 0.0f); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 0ab86bef6..96ea38e4b 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -12,7 +12,7 @@ namespace Barotrauma { private UInt16 startWatchmanID, endWatchmanID; - public static GUIComponent StartCampaignSetup(IEnumerable saveFiles) + public static GUIComponent StartCampaignSetup( IEnumerable submarines, IEnumerable saveFiles) { GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); @@ -35,7 +35,7 @@ namespace Barotrauma var newCampaignContainer = new GUIFrame(new RectTransform(Vector2.One, campaignContainer.RectTransform, Anchor.BottomLeft), style: null); var loadCampaignContainer = new GUIFrame(new RectTransform(Vector2.One, campaignContainer.RectTransform, Anchor.BottomLeft), style: null); - var campaignSetupUI = new CampaignSetupUI(true, newCampaignContainer, loadCampaignContainer, saveFiles); + var campaignSetupUI = new CampaignSetupUI(true, newCampaignContainer, loadCampaignContainer, submarines, saveFiles); var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), TextManager.Get("NewCampaign")) @@ -92,8 +92,7 @@ namespace Barotrauma if (endWatchman != null) { InitializeWatchman(endWatchman); } } } - - + protected override void WatchmanInteract(Character watchman, Character interactor) { if ((watchman.Submarine == Level.Loaded.StartOutpost && !Submarine.MainSub.AtStartPosition) || @@ -107,8 +106,7 @@ namespace Barotrauma return; } - if (GameMain.Client != null && interactor == Character.Controlled && - (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign))) + if (GameMain.Client != null && interactor == Character.Controlled) { var msgBox = new GUIMessageBox("", TextManager.Get("CampaignEnterOutpostPrompt") .Replace("[locationname]", Submarine.MainSub.AtStartPosition ? Map.CurrentLocation.Name : Map.SelectedLocation.Name), @@ -122,8 +120,7 @@ namespace Barotrauma return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; - + msgBox.Buttons[1].OnClicked += msgBox.Close; } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSettings.cs b/Barotrauma/BarotraumaClient/Source/GameSettings.cs index 9542ef2b8..4ee4ab87e 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSettings.cs @@ -47,7 +47,7 @@ namespace Barotrauma //if "%" is found if (index > 0) { - while (true) + while (index > 0) { //search for end of label index -= 1; @@ -439,7 +439,6 @@ namespace Barotrauma } }; #endif - var radioButtonFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), voiceSettings.RectTransform)) { Stretch = true, @@ -458,6 +457,24 @@ namespace Barotrauma voiceMode.AddRadioButton((VoiceMode)i, tick); } + var micVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.18f), voiceSettings.RectTransform), TextManager.Get("MicrophoneVolume")); + var micVolumeSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.18f), voiceSettings.RectTransform), + barSize: 0.1f) + { + UserData = micVolumeText, + BarScroll = (float)Math.Sqrt(MathUtils.InverseLerp(0.2f, 5.0f, MicrophoneVolume)), + OnMoved = (scrollBar, scroll) => + { + MicrophoneVolume = MathHelper.Lerp(0.2f, 5.0f, scroll * scroll); + MicrophoneVolume = (float)Math.Round(MicrophoneVolume, 1); + ChangeSliderText(scrollBar, MicrophoneVolume); + scrollBar.Step = 0.05f; + return true; + }, + Step = 0.05f + }; + micVolumeSlider.OnMoved(micVolumeSlider, micVolumeSlider.BarScroll); + var voiceInputContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.2f), voiceSettings.RectTransform, Anchor.BottomCenter)); new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice") + ": "); var voiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), voiceInputContainer.RectTransform, Anchor.TopRight), @@ -469,7 +486,6 @@ namespace Barotrauma voiceKeyBox.SelectedColor = Color.Gold * 0.3f; var voiceActivityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), voiceSettings.RectTransform)); - GUITextBlock noiseGateText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), voiceActivityGroup.RectTransform), TextManager.Get("NoiseGateThreshold")) { TextGetter = () => @@ -509,13 +525,13 @@ namespace Barotrauma if (GameMain.Client == null && VoipCapture.Instance == null) { VoipCapture.Create(GameMain.Config.VoiceCaptureDevice); - } - if (VoipCapture.Instance == null) - { - VoiceSetting = vMode = VoiceMode.Disabled; - voiceInputContainer.Visible = false; - voiceActivityGroup.Visible = false; - return; + if (VoipCapture.Instance == null) + { + VoiceSetting = vMode = VoiceMode.Disabled; + voiceInputContainer.Visible = false; + voiceActivityGroup.Visible = false; + return; + } } } else diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs index d536e9fe5..8a1858095 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Door.cs @@ -93,7 +93,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing) { - Color color = Color.White; + Color color = item.SpriteColor; if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components weldSpritePos.Y = -weldSpritePos.Y; weldedSprite.Draw(spriteBatch, - weldSpritePos, Color.White * (stuck / 100.0f), scale: item.Scale); + weldSpritePos, item.SpriteColor * (stuck / 100.0f), scale: item.Scale); } if (openState == 1.0f) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs index 681bcad91..d9927245e 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemComponent.cs @@ -313,7 +313,7 @@ namespace Barotrauma.Items.Components if (sound == null) { return 0.0f; } if (sound.VolumeProperty == "") { return sound.VolumeMultiplier; } - if (properties.TryGetValue(sound.VolumeProperty, out SerializableProperty property)) + if (SerializableProperties.TryGetValue(sound.VolumeProperty, out SerializableProperty property)) { float newVolume = 0.0f; try diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs index 68d2a9b90..d0e98ead9 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs @@ -331,8 +331,18 @@ namespace Barotrauma.Items.Components spriteBatch.GraphicsDevice.ScissorRectangle = container.Rect; spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + //make the pointer jitter a bit if it's at the upper limit of the fission rate + float jitter = 0.0f; + if (FissionRate > allowedFissionRate.Y - 5.0f) + { + float jitterAmount = Math.Min(targetFissionRate - allowedFissionRate.Y, 10.0f); + float t = graphTimer / updateGraphInterval; + + jitter = (PerlinNoise.GetPerlin(t * 0.5f, t * 0.1f) - 0.5f) * jitterAmount; + } + DrawMeter(spriteBatch, container.Rect, - fissionRateMeter, FissionRate, new Vector2(0.0f, 100.0f), optimalFissionRate, allowedFissionRate); + fissionRateMeter, FissionRate + jitter, new Vector2(0.0f, 100.0f), optimalFissionRate, allowedFissionRate); spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; @@ -392,12 +402,12 @@ namespace Barotrauma.Items.Components Vector2 pos = new Vector2(rect.Center.X, rect.Y + meterSprite.Origin.Y * scale); Vector2 optimalRangeNormalized = new Vector2( - (optimalRange.X - range.X) / (range.Y - range.X), - (optimalRange.Y - range.X) / (range.Y - range.X)); + MathHelper.Clamp((optimalRange.X - range.X) / (range.Y - range.X), 0.0f, 0.95f), + MathHelper.Clamp((optimalRange.Y - range.X) / (range.Y - range.X), 0.0f, 1.0f)); Vector2 allowedRangeNormalized = new Vector2( - (allowedRange.X - range.X) / (range.Y - range.X), - (allowedRange.Y - range.X) / (range.Y - range.X)); + MathHelper.Clamp((allowedRange.X - range.X) / (range.Y - range.X), 0.0f, 0.95f), + MathHelper.Clamp((allowedRange.Y - range.X) / (range.Y - range.X), 0.0f, 1.0f)); Vector2 sectorRad = new Vector2(-1.57f, 1.57f); @@ -417,10 +427,10 @@ namespace Barotrauma.Items.Components { spriteBatch.End(); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; - spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0,0,GameMain.GraphicsWidth, (int)(pos.Y + (meterSprite.size.Y - meterSprite.Origin.Y) * scale)); + spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, 0, GameMain.GraphicsWidth, (int)(pos.Y + (meterSprite.size.Y - meterSprite.Origin.Y) * scale) - 3); spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); - sectorSprite.Draw(spriteBatch, pos, Color.LightGreen, MathHelper.PiOver2, scale); + sectorSprite.Draw(spriteBatch, pos, Color.LightGreen, MathHelper.PiOver2 + (allowedSectorRad.X + allowedSectorRad.Y) / 2.0f, scale); sectorSprite.Draw(spriteBatch, pos, Color.Orange, optimalSectorRad.X, scale); sectorSprite.Draw(spriteBatch, pos, Color.Red, allowedSectorRad.X, scale); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs index da6fe39d0..a4935fd62 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs @@ -785,8 +785,9 @@ namespace Barotrauma.Items.Components foreach (Limb limb in c.AnimController.Limbs) { - float pointDist = ((limb.WorldPosition - pingSource) * displayScale).LengthSquared(); + if (!limb.body.Enabled) { continue; } + float pointDist = ((limb.WorldPosition - pingSource) * displayScale).LengthSquared(); if (limb.SimPosition == Vector2.Zero || pointDist > DisplayRadius * DisplayRadius) continue; if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs index b1871cfe0..7e8fdc706 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs @@ -1,4 +1,6 @@ -using Barotrauma.Particles; +using Barotrauma.Networking; +using Barotrauma.Particles; +using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; @@ -112,8 +114,7 @@ namespace Barotrauma.Items.Components System.Diagnostics.Debug.Assert(GuiFrame.GetChild(0) is GUILayoutGroup, "Repair UI hierarchy has changed, could not find skill texts"); foreach (GUIComponent c in GuiFrame.GetChild(0).Children) { - Skill skill = c.UserData as Skill; - if (skill == null) continue; + if (!(c.UserData is Skill skill)) continue; GUITextBlock textBlock = (GUITextBlock)c; if (character.GetSkillLevel(skill.Identifier) < skill.Level) @@ -126,5 +127,15 @@ namespace Barotrauma.Items.Components } } } + + public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) + { + deteriorationTimer = msg.ReadSingle(); + } + + public void ClientWrite(NetBuffer msg, object[] extraData = null) + { + //no need to write anything, just letting the server know we started repairing + } } } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs index 8b6b5ea03..b56dd8629 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/StatusHUD.cs @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold || affliction.Strength <= 0.0f) continue; + if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) continue; if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs index 77f1b1a68..6e42a2949 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Turret.cs @@ -237,13 +237,13 @@ namespace Barotrauma.Items.Components railSprite?.Draw(spriteBatch, drawPos, - Color.White, + item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); barrelSprite?.Draw(spriteBatch, - drawPos - new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset * item.Scale, - Color.White, + drawPos - new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset * item.Scale, + item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); @@ -288,7 +288,10 @@ namespace Barotrauma.Items.Components availableAmmo.AddRange(itemContainer.Inventory.Items); } - float chargeRate = powerConsumption <= 0.0f ? 1.0f : batteryCharge / batteryCapacity; + float chargeRate = + powerConsumption <= 0.0f ? + 1.0f : + batteryCapacity > 0.0f ? batteryCharge / batteryCapacity : 0.0f; bool charged = batteryCharge * 3600.0f > powerConsumption; bool readyToFire = reload <= 0.0f && charged && availableAmmo.Any(p => p != null); if (ShowChargeIndicator && PowerConsumption > 0.0f) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Item.cs b/Barotrauma/BarotraumaClient/Source/Items/Item.cs index 40af52153..f006a8df1 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Item.cs @@ -448,7 +448,7 @@ namespace Barotrauma } } - private GUIComponent CreateEditingHUD(bool inGame = false) + public GUIComponent CreateEditingHUD(bool inGame = false) { editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null) @@ -457,40 +457,42 @@ namespace Barotrauma }; var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true); - - if (!inGame && Linkable) - { - var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, 20)), TextManager.Get("HoldToLink"), font: GUI.SmallFont); - var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, 20)), TextManager.Get("AllowedLinks") + ": ", font: GUI.SmallFont); - if (AllowedLinks.None()) - { - itemsText.Text += TextManager.Get("None"); - } - else - { - for (int i = 0; i < AllowedLinks.Count; i++) - { - itemsText.Text += AllowedLinks[i]; - if (i < AllowedLinks.Count - 1) - { - itemsText.Text += ", "; - } - } - } - itemEditor.AddCustomContent(linkText, 1); - itemEditor.AddCustomContent(itemsText, 2); - linkText.TextColor = Color.Yellow; - itemsText.TextColor = Color.Yellow; - } - if (!inGame) { + if (Linkable) + { + var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, 20)), TextManager.Get("HoldToLink"), font: GUI.SmallFont); + var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, 20)), TextManager.Get("AllowedLinks") + ": ", font: GUI.SmallFont); + if (AllowedLinks.None()) + { + itemsText.Text += TextManager.Get("None"); + } + }; + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY")) + { + ToolTip = TextManager.Get("MirrorEntityYToolTip"), + OnClicked = (button, data) => + { + for (int i = 0; i < AllowedLinks.Count; i++) + { + itemsText.Text += AllowedLinks[i]; + if (i < AllowedLinks.Count - 1) + { + itemsText.Text += ", "; + } + } + } + itemEditor.AddCustomContent(linkText, 1); + itemEditor.AddCustomContent(itemsText, 2); + linkText.TextColor = Color.Yellow; + itemsText.TextColor = Color.Yellow; + } var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, 20)), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; - new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX")) { ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => @@ -499,7 +501,7 @@ namespace Barotrauma return true; } }; - new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY")) { ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => @@ -510,7 +512,7 @@ namespace Barotrauma }; if (Sprite != null) { - var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite")); + var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite")); reloadTextureButton.OnClicked += (button, data) => { Sprite.ReloadXML(); @@ -518,6 +520,15 @@ namespace Barotrauma return true; }; } + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ResetToPrefab")) + { + OnClicked = (button, data) => + { + Reset(); + CreateEditingHUD(); + return true; + } + }; itemEditor.AddCustomContent(buttonContainer, itemEditor.ContentCount); } diff --git a/Barotrauma/BarotraumaClient/Source/Map/Hull.cs b/Barotrauma/BarotraumaClient/Source/Map/Hull.cs index f295160c6..8dc7700f0 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Hull.cs @@ -1,14 +1,13 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; +using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using Microsoft.Xna.Framework.Input; using System.Linq; -using Lidgren.Network; namespace Barotrauma { @@ -19,6 +18,8 @@ namespace Barotrauma private List decals = new List(); private float serverUpdateDelay; + private float remoteWaterVolume, remoteOxygenPercentage; + private List remoteFireSources; private bool networkUpdatePending; private float networkUpdateTimer; @@ -139,6 +140,10 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam) { serverUpdateDelay -= deltaTime; + if (serverUpdateDelay <= 0.0f) + { + ApplyRemoteState(); + } if (networkUpdatePending) { @@ -547,18 +552,18 @@ namespace Barotrauma public void ClientRead(ServerNetObject type, NetBuffer message, float sendingTime) { - float newWaterVolume = message.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; - float newOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); + remoteWaterVolume = message.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; + remoteOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); bool hasFireSources = message.ReadBoolean(); int fireSourceCount = 0; - List newFireSources = new List(); + remoteFireSources = new List(); if (hasFireSources) { fireSourceCount = message.ReadRangedInteger(0, 16); for (int i = 0; i < fireSourceCount; i++) { - newFireSources.Add(new Vector3( + remoteFireSources.Add(new Vector3( MathHelper.Clamp(message.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), MathHelper.Clamp(message.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), message.ReadRangedSingle(0.0f, 1.0f, 8))); @@ -567,41 +572,6 @@ namespace Barotrauma if (serverUpdateDelay > 0.0f) { return; } - WaterVolume = newWaterVolume; - OxygenPercentage = newOxygenPercentage; - - for (int i = 0; i < fireSourceCount; i++) - { - Vector2 pos = new Vector2( - rect.X + rect.Width * newFireSources[i].X, - rect.Y - rect.Height + (rect.Height * newFireSources[i].Y)); - float size = newFireSources[i].Z * rect.Width; - - var newFire = i < FireSources.Count ? - FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); - newFire.Position = pos; - newFire.Size = new Vector2(size, newFire.Size.Y); - - //ignore if the fire wasn't added to this room (invalid position)? - if (!FireSources.Contains(newFire)) - { - newFire.Remove(); - continue; - } - } - - for (int i = FireSources.Count - 1; i >= fireSourceCount; i--) - { - FireSources[i].Remove(); - if (i < FireSources.Count) - { - FireSources.RemoveAt(i); - } - } - - if (serverUpdateDelay > 0.0f) { return; } - ApplyRemoteState(); } diff --git a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs index 49f40f3dd..c61e3821d 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs @@ -404,12 +404,15 @@ namespace Barotrauma.Lights } } if (highlightedEntities.Count == 0) { return false; } - - //draw characters in solid white first + + //draw characters in light blue first graphics.SetRenderTarget(HighlightMap); SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; SolidColorEffect.Parameters["color"].SetValue(Color.LightBlue.ToVector4()); SolidColorEffect.CurrentTechnique.Passes[0].Apply(); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidColor"]; + DeformableSprite.Effect.Parameters["solidColor"].SetValue(Color.LightBlue.ToVector4()); + DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, samplerState: SamplerState.LinearWrap, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); foreach (Entity highlighted in highlightedEntities) { @@ -426,9 +429,12 @@ namespace Barotrauma.Lights //draw characters in black with a bit of blur, leaving the white edges visible float phase = (float)(Math.Sin(Timing.TotalTime * 3.0f) + 1.0f) / 2.0f; //phase oscillates between 0 and 1 - SolidColorEffect.Parameters["color"].SetValue(Color.Black.ToVector4() * MathHelper.Lerp(0.5f, 0.9f, phase)); + Vector4 overlayColor = Color.Black.ToVector4() * MathHelper.Lerp(0.5f, 0.9f, phase); + SolidColorEffect.Parameters["color"].SetValue(overlayColor); SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColorBlur"]; SolidColorEffect.CurrentTechnique.Passes[0].Apply(); + DeformableSprite.Effect.Parameters["solidColor"].SetValue(overlayColor); + DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, samplerState: SamplerState.LinearWrap, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); foreach (Entity highlighted in highlightedEntities) { @@ -450,6 +456,8 @@ namespace Barotrauma.Lights spriteBatch.Draw(highlightRaster, new Rectangle(0, 0, HighlightMap.Width, HighlightMap.Height), new Rectangle(0, 0, HighlightMap.Width, HighlightMap.Height), Color.White * 0.5f); spriteBatch.End(); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"]; + return true; } @@ -471,6 +479,10 @@ namespace Barotrauma.Lights Level.Loaded?.Renderer?.RenderWalls(graphics, cam, specular: true); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidColor"]; + DeformableSprite.Effect.Parameters["solidColor"].SetValue(Color.Gray.ToVector4()); + DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); + //obstruct specular maps behind the sub and characters by drawing them on the map in solid gray SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; SolidColorEffect.Parameters["color"].SetValue(Color.Gray.ToVector4()); @@ -487,6 +499,9 @@ namespace Barotrauma.Lights } spriteBatch.End(); + + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"]; + graphics.SetRenderTarget(null); graphics.BlendState = BlendState.AlphaBlend; } diff --git a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs index 2edd1425c..7631b14c5 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs @@ -98,7 +98,7 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.02f }; - new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX")) { ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => @@ -107,7 +107,7 @@ namespace Barotrauma return true; } }; - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY")) { ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => @@ -116,7 +116,7 @@ namespace Barotrauma return true; } }; - var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite")) + var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite")) { OnClicked = (button, data) => { @@ -125,6 +125,15 @@ namespace Barotrauma return true; } }; + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ResetToPrefab")) + { + OnClicked = (button, data) => + { + Reset(); + CreateEditingHUD(); + return true; + } + }; editor.AddCustomContent(buttonContainer, editor.ContentCount); PositionEditingHUD(); @@ -351,6 +360,7 @@ namespace Barotrauma -Bodies[i].Rotation, Color.White); } } + AiTarget?.Draw(spriteBatch); } } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs index 85d8ee1fe..9e4fb0f6b 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs @@ -132,6 +132,7 @@ namespace Barotrauma.Networking var buttonContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ButtonAreaTop, inGameHUD.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight) { + AbsoluteSpacing = 5, CanBeFocused = false }; @@ -147,9 +148,10 @@ namespace Barotrauma.Networking Visible = false }; - EndVoteTickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.6f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) }, + EndVoteTickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.4f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) }, TextManager.Get("EndRound")) { + UserData = TextManager.Get("EndRound"), OnSelected = ToggleEndRoundVote, Visible = false }; @@ -323,18 +325,17 @@ namespace Barotrauma.Networking // Loop until we are approved //TODO: show the name of the server instead of IP when connecting through the server list (more streamer-friendly) - string connectingText = TextManager.Get("ConnectingTo").Replace("[serverip]", serverIP); + string connectingText = TextManager.Get("Connecting"); while (!CanStart && !connectCancelled) { if (reconnectBox == null) { - reconnectBox = new GUIMessageBox(TextManager.Get("Connecting"), connectingText, new string[] { TextManager.Get("Cancel") }); - + reconnectBox = new GUIMessageBox(connectingText, TextManager.Get("ConnectingTo").Replace("[serverip]", serverIP), new string[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += CancelConnect; reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; } - reconnectBox.Text.Text = connectingText + new string('.', ((int)Timing.TotalTime % 3 + 1)); + reconnectBox.Header.Text = connectingText + new string('.', ((int)Timing.TotalTime % 3 + 1)); if (DateTime.Now > reqAuthTime) { @@ -621,7 +622,7 @@ namespace Barotrauma.Networking if (gameStarted) SetRadioButtonColor(); - if (ShowNetStats) + if (ShowNetStats && client?.ServerConnection != null) { netStats.AddValue(NetStats.NetStatType.ReceivedBytes, client.ServerConnection.Statistics.ReceivedBytes); netStats.AddValue(NetStats.NetStatType.SentBytes, client.ServerConnection.Statistics.SentBytes); @@ -782,7 +783,7 @@ namespace Barotrauma.Networking saveFiles.Add(inc.ReadString()); } - GameMain.NetLobbyScreen.CampaignSetupUI = MultiPlayerCampaign.StartCampaignSetup(saveFiles); + GameMain.NetLobbyScreen.CampaignSetupUI = MultiPlayerCampaign.StartCampaignSetup(serverSubmarines, saveFiles); break; case ServerPacketHeader.PERMISSIONS: ReadPermissions(inc); @@ -835,6 +836,8 @@ namespace Barotrauma.Networking DisconnectReason disconnectReason = DisconnectReason.Unknown; if (splitMsg.Length > 0) Enum.TryParse(splitMsg[0], out disconnectReason); + DebugConsole.NewMessage("Received a disconnect message (" + disconnectMsg + ")"); + if (disconnectReason == DisconnectReason.ServerFull) { //already waiting for a slot to free up, do nothing @@ -864,22 +867,19 @@ namespace Barotrauma.Networking waitInServerQueueBox = null; CoroutineManager.StopCoroutines("WaitInServerQueue"); } - else - { - string msg = ""; - if (disconnectReason == DisconnectReason.Unknown) - { - msg = disconnectMsg; - } - else - { - msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()); if (allowReconnect && disconnectReason == DisconnectReason.Unknown) { + DebugConsole.NewMessage("Attempting to reconnect..."); + + string msg = TextManager.GetServerMessage(disconnectMsg); + msg = string.IsNullOrWhiteSpace(msg) ? + TextManager.Get("ConnectionLostReconnecting") : + msg + '\n' + TextManager.Get("ConnectionLostReconnecting"); + reconnectBox = new GUIMessageBox( TextManager.Get("ConnectionLost"), - TextManager.Get("ConnectionLostReconnecting"), new string[0]); + msg, new string[0]); connected = false; ConnectToServer(serverIP); } @@ -888,10 +888,12 @@ namespace Barotrauma.Networking string msg = ""; if (disconnectReason == DisconnectReason.Unknown) { + DebugConsole.NewMessage("Do not attempt reconnect (not allowed)."); msg = disconnectMsg; } else { + DebugConsole.NewMessage("Do not attempt to reconnect (DisconnectReason doesn't allow reconnection)."); msg = TextManager.Get("DisconnectReason." + disconnectReason.ToString()); for (int i = 1; i < splitMsg.Length; i++) @@ -1031,9 +1033,10 @@ namespace Barotrauma.Networking EndVoteTickBox.Selected = false; - int seed = inc.ReadInt32(); - string levelSeed = inc.ReadString(); - float levelDifficulty = inc.ReadFloat(); + int seed = inc.ReadInt32(); + string levelSeed = inc.ReadString(); + int levelEqualityCheckVal = inc.ReadInt32(); + float levelDifficulty = inc.ReadFloat(); byte losMode = inc.ReadByte(); @@ -1102,6 +1105,15 @@ namespace Barotrauma.Networking mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); } + if (Level.Loaded.EqualityCheckVal != levelEqualityCheckVal) + { + string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server (seed " + Level.Loaded.Seed + ")."; + DebugConsole.ThrowError(errorMsg, createMessageBox: true); + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + levelSeed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + CoroutineManager.StartCoroutine(EndGame("")); + yield return CoroutineStatus.Failure; + } + if (respawnAllowed) respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle ? GameMain.NetLobbyScreen.SelectedShuttle : null); gameStarted = true; @@ -1171,8 +1183,8 @@ namespace Barotrauma.Networking } } - GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, submarines); - GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, submarines); + GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, serverSubmarines); + GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.ShuttleList.ListBox, serverSubmarines); gameStarted = inc.ReadBoolean(); bool allowSpectating = inc.ReadBoolean(); @@ -1458,6 +1470,9 @@ namespace Barotrauma.Networking case ServerNetObject.CLIENT_LIST: ReadClientList(inc); break; + case ServerNetObject.CLIENT_LIST: + ReadClientList(inc); + break; case ServerNetObject.CHAT_MESSAGE: ChatMessage.ClientRead(inc); break; @@ -1781,6 +1796,8 @@ namespace Barotrauma.Networking SaveUtil.LoadGame(GameMain.GameSession.SavePath, GameMain.GameSession); campaign.LastSaveID = campaign.PendingSaveID; + + DebugConsole.Log("Campaign save received, save ID " + campaign.LastSaveID); //decrement campaign update ID so the server will send us the latest data //(as there may have been campaign updates after the save file was created) campaign.LastUpdateID--; @@ -1798,7 +1815,7 @@ namespace Barotrauma.Networking public override void CreateEntityEvent(INetSerializable entity, object[] extraData) { - if (!(entity is IClientSerializable)) throw new InvalidCastException("entity is not IClientSerializable"); + if (!(entity is IClientSerializable)) throw new InvalidCastException("Entity is not IClientSerializable"); entityEventManager.CreateEvent(entity as IClientSerializable, extraData); } @@ -2067,7 +2084,8 @@ namespace Barotrauma.Networking msg.Write(true); msg.WritePadBits(); msg.Write(savePath); msg.Write(mapSeed); - msg.Write(sub.FilePath); + msg.Write(sub.Name); + msg.Write(sub.MD5Hash.Hash); client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); @@ -2305,13 +2323,25 @@ namespace Barotrauma.Networking if (EndVoteCount > 0) { - string endVoteText = TextManager.Get("EndRoundVotes") - .Replace("[y]", EndVoteCount.ToString()) - .Replace("[n]", (EndVoteMax - EndVoteCount).ToString()); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 10.0f - GUI.SmallFont.MeasureString(endVoteText).X, 40), - endVoteText, - Color.White, - font: GUI.SmallFont); + if (EndVoteTickBox.Visible) + { + EndVoteTickBox.Text = + (EndVoteTickBox.UserData as string) + " " + EndVoteCount + "/" + EndVoteMax; + } + else + { + string endVoteText = TextManager.Get("EndRoundVotes") + .Replace("[votes]", EndVoteCount.ToString()) + .Replace("[max]", EndVoteMax.ToString()); + GUI.DrawString(spriteBatch, EndVoteTickBox.Rect.Center.ToVector2() - GUI.SmallFont.MeasureString(endVoteText) / 2, + endVoteText, + Color.White, + font: GUI.SmallFont); + } + } + else + { + EndVoteTickBox.Text = EndVoteTickBox.UserData as string; } if (respawnManager != null) diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs index 2f85beaf6..bd1df6c87 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs @@ -1,4 +1,5 @@ using Lidgren.Network; +using Microsoft.Xna.Framework; using OpenTK.Audio.OpenAL; using System; using System.Linq; @@ -26,6 +27,11 @@ namespace Barotrauma.Networking get; private set; } + + public float Gain + { + get { return GameMain.Config?.MicrophoneVolume ?? 1.0f; } + } public DateTime LastEnqueueAudio; @@ -69,6 +75,7 @@ namespace Barotrauma.Networking { if (!GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "capturedevicenotfound")) { + GUI.SettingsMenuOpen = false; new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("VoipCaptureDeviceNotFound")) { UserData = "capturedevicenotfound" @@ -160,8 +167,9 @@ namespace Barotrauma.Networking double maxAmplitude = 0.0f; for (int i = 0; i < VoipConfig.BUFFER_SIZE; i++) { - double sampleVal = (double)uncompressedBuffer[i] / (double)short.MaxValue; - maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal)); + uncompressedBuffer[i] = (short)MathHelper.Clamp((uncompressedBuffer[i] * Gain), -short.MaxValue, short.MaxValue); + double sampleVal = uncompressedBuffer[i] / (double)short.MaxValue; + maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal)); } double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0); diff --git a/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs index 1fc27d949..f231c83a0 100644 --- a/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/Source/Physics/PhysicsBody.cs @@ -150,10 +150,10 @@ namespace Barotrauma float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; Vector2 newPosition = SimPosition; - float newRotation = Rotation; + float? newRotation = null; bool awake = body.Awake; Vector2 newVelocity = LinearVelocity; - float newAngularVelocity = AngularVelocity; + float? newAngularVelocity = null; newPosition = new Vector2( msg.ReadFloat(), @@ -179,12 +179,12 @@ namespace Barotrauma msg.ReadPadBits(); if (!MathUtils.IsValid(newPosition) || - !MathUtils.IsValid(newRotation) || !MathUtils.IsValid(newVelocity) || - !MathUtils.IsValid(newAngularVelocity)) + (newRotation.HasValue && !MathUtils.IsValid(newRotation.Value)) || + (newAngularVelocity.HasValue && !MathUtils.IsValid(newAngularVelocity.Value))) { string errorMsg = "Received invalid position data for \"" + parentDebugName - + "\" (position: " + newPosition + ", rotation: " + newRotation + ", velocity: " + newVelocity + ", angular velocity: " + newAngularVelocity + ")"; + + "\" (position: " + newPosition + ", rotation: " + (newRotation ?? 0) + ", velocity: " + newVelocity + ", angular velocity: " + (newAngularVelocity ?? 0) + ")"; #if DEBUG DebugConsole.ThrowError(errorMsg); #endif diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs index 99ce9bda9..8cfeef04b 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs @@ -33,7 +33,7 @@ namespace Barotrauma private readonly bool isMultiplayer; - public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable saveFiles=null) + public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) { this.isMultiplayer = isMultiplayer; this.newGameContainer = newGameContainer; @@ -45,6 +45,12 @@ namespace Barotrauma RelativeSpacing = 0.05f }; + var columnContainer = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + var leftColumn = new GUILayoutGroup(new RectTransform(Vector2.One, columnContainer.RectTransform)) { Stretch = true, @@ -63,7 +69,7 @@ namespace Barotrauma OnSelected = CheckForPax }; - UpdateSubList(); + UpdateSubList(submarines); // New game right sideon new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), rightColumn.RectTransform), TextManager.Get("SaveName") + ":", textAlignment: Alignment.BottomLeft); @@ -90,9 +96,8 @@ namespace Barotrauma return false; } - Submarine selectedSub = subList.SelectedData as Submarine; - if (selectedSub == null) return false; - + if (!(subList.SelectedData is Submarine selectedSub)) { return false; } + if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) { ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; @@ -199,9 +204,9 @@ namespace Barotrauma public void UpdateSubList(IEnumerable submarines) { #if DEBUG - var subsToShow = Submarine.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)); + var subsToShow = submarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)); #else - var subsToShow = Submarine.SavedSubmarines; + var subsToShow = submarines; #endif subList.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs index 12d87cf51..3c322d12d 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs @@ -459,6 +459,12 @@ namespace Barotrauma OnClicked = (GUIButton btn, object obj) => { StartRound?.Invoke(); return true; }, Enabled = true }; + if (GameMain.Client != null) + { + startButton.Visible = !GameMain.Client.GameStarted && + (GameMain.Client.HasPermission(Networking.ClientPermissions.ManageRound) || + GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); + } } OnLocationSelected?.Invoke(location, connection); @@ -502,7 +508,13 @@ namespace Barotrauma CanBeFocused = false }; - if (startButton != null) { startButton.Enabled = true; } + if (startButton != null) + { + startButton.Enabled = true; + startButton.Visible = GameMain.Client == null || + GameMain.Client.HasPermission(Networking.ClientPermissions.ManageRound) || + GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign); + } } private void CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIListBox listBox, int width) diff --git a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs index 091909577..c46668705 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs @@ -282,7 +282,7 @@ namespace Barotrauma menuTabs[(int)Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize)); var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center), style: null); - campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame) + campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame, Submarine.SavedSubmarines) { LoadGame = LoadGame, StartNewGame = StartGame @@ -336,7 +336,7 @@ namespace Barotrauma UpdateTutorialList(); - campaignSetupUI.UpdateSubList(); + campaignSetupUI.UpdateSubList(Submarine.SavedSubmarines); ResetButtonStates(null); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs index c8882034b..02f6ba21c 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs @@ -632,6 +632,13 @@ namespace Barotrauma Visible = false }; + campaignViewButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), rightInfoColumn.RectTransform), + TextManager.Get("CampaignView"), style: "GUIButtonLarge") + { + OnClicked = (btn, obj) => { ToggleCampaignView(true); return true; }, + Visible = false + }; + StartButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.1f), infoFrameContent.RectTransform, Anchor.BottomRight), TextManager.Get("StartGameButton"), style: "GUIButtonLarge") { @@ -644,13 +651,6 @@ namespace Barotrauma }; clientHiddenElements.Add(StartButton); - campaignViewButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.1f), infoFrameContent.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.0f, 0.06f) }, - TextManager.Get("CampaignView"), style: "GUIButtonLarge") - { - OnClicked = (btn, obj) => { ToggleCampaignView(true); return true; }, - Visible = false - }; - spectateButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.1f), infoFrameContent.RectTransform, Anchor.BottomRight), TextManager.Get("SpectateButton"), style: "GUIButtonLarge"); } @@ -726,6 +726,12 @@ namespace Barotrauma spectateButton.Visible = GameMain.Client.GameStarted; ReadyToStartBox.Visible = !GameMain.Client.GameStarted; ReadyToStartBox.Selected = false; + if (campaignUI?.StartButton != null) + { + campaignUI.StartButton.Visible = !GameMain.Client.GameStarted && + (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || + GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); + } GameMain.Client.SetReadyToStart(ReadyToStartBox); } else @@ -844,6 +850,13 @@ namespace Barotrauma GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.EndRoundButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound); + + if (campaignUI?.StartButton != null) + { + campaignUI.StartButton.Visible = !GameMain.Client.GameStarted && + (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || + GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); + } } public void ShowSpectateButton() @@ -1191,7 +1204,7 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.Shuttle)) { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.1f, 0.0f) }, TextManager.Get("Shuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, diff --git a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs index aca7905bb..3fa7dcc26 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs @@ -716,7 +716,7 @@ namespace Barotrauma catch (PingException ex) { string errorMsg = "Failed to ping a server (" + serverInfo.ServerName + ", " + serverInfo.IP + ") - " + (ex?.InnerException?.Message ?? ex.Message); - GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + serverInfo.IP, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + serverInfo.IP, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, errorMsg); #if DEBUG DebugConsole.NewMessage(errorMsg, Color.Red); #endif diff --git a/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs index 860522283..54f33dbb6 100644 --- a/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/Source/Sprite/DeformableSprite.cs @@ -26,6 +26,10 @@ namespace Barotrauma private int subDivX, subDivY; private static Effect effect; + public static Effect Effect + { + get { return effect; } + } private Point spritePos; private Point spriteSize; diff --git a/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs index eb0a60828..1a54bc6aa 100644 --- a/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs +++ b/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.8.9.6")] -[assembly: AssemblyFileVersion("0.8.9.6")] +[assembly: AssemblyVersion("0.8.9.7")] +[assembly: AssemblyFileVersion("0.8.9.7")] diff --git a/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs index a7e0d681b..29f5bade5 100644 --- a/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs @@ -4,11 +4,6 @@ namespace Barotrauma { partial class CharacterInfo { - partial void SpawnInventoryItemProjSpecific(Item item) - { - Entity.Spawner.CreateNetworkEvent(item, false); - } - public void ServerWrite(NetBuffer msg) { msg.Write(ID); diff --git a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs index 18a8733c4..1dd05bae8 100644 --- a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs @@ -81,11 +81,12 @@ namespace Barotrauma //dequeue messages lock (queuedMessages) { - if (queuedMessages.Count>0) + if (queuedMessages.Count > 0) { + int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)Console.WindowWidth), 1); Console.CursorLeft = 0; Console.Write(new string(' ', consoleWidth)); - Console.CursorTop--; Console.CursorLeft = 0; + Console.CursorTop -= inputLines; Console.CursorLeft = 0; while (queuedMessages.Count > 0) { ColoredText msg = queuedMessages.Dequeue(); @@ -177,11 +178,11 @@ namespace Barotrauma private static void RewriteInputToCommandLine(string input) { - Console.WriteLine(""); Console.CursorTop--; - int consoleWidth = Console.WindowWidth; - if (consoleWidth < 5) consoleWidth = 5; - int consoleHeight = Console.WindowHeight; - if (consoleHeight < 5) consoleHeight = 5; + int consoleWidth = Math.Max(Console.WindowWidth, 5); + int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)consoleWidth), 1); + int cursorLine = Math.Max((int)Math.Ceiling((input.Length + 1) / (float)consoleWidth), 1); + + Console.WriteLine(""); Console.CursorTop -= inputLines; string ln = input.Length > 0 ? AutoComplete(input, 0) : ""; ln += new string(' ', consoleWidth - (ln.Length % consoleWidth)); @@ -190,9 +191,9 @@ namespace Barotrauma Console.Write(ln); Console.ForegroundColor = ConsoleColor.White; Console.CursorLeft = 0; - Console.CursorTop--; + Console.CursorTop -= cursorLine; Console.Write(input); - Console.CursorLeft = input.Length; + Console.CursorLeft = input.Length % Console.WindowWidth; } private static void AssignOnClientRequestExecute(string names, Action onClientRequestExecute) @@ -1554,9 +1555,9 @@ namespace Barotrauma })); #if DEBUG - commands.Add(new Command("spamevents", "A debug command that immediately creates entity events for all items, characters and structures.", (string[] args) => + commands.Add(new Command("spamevents", "A debug command that creates a ton of entity events.", (string[] args) => { - foreach (Item item in Item.ItemList) + /*foreach (Item item in Item.ItemList) { foreach (ItemComponent component in item.Components) { @@ -1573,16 +1574,18 @@ namespace Barotrauma GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.Status }); } } - foreach (Character c in Character.CharacterList) { GameMain.Server.CreateEntityEvent(c, new object[] { NetEntityEvent.Type.Status }); + }*/ + foreach (Hull hull in Hull.hullList) + { + GameMain.Server.CreateEntityEvent(hull); } - foreach (Structure wall in Structure.WallList) { GameMain.Server.CreateEntityEvent(wall); - } + } })); #endif } diff --git a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 0656ea051..0e1667db4 100644 --- a/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -11,11 +11,11 @@ namespace Barotrauma { private List characterData = new List(); - public static void StartNewCampaign(string savePath, string subName, string seed) + public static void StartNewCampaign(string savePath, string subPath, string seed) { if (string.IsNullOrWhiteSpace(savePath)) return; - GameMain.GameSession = new GameSession(new Submarine(subName, ""), savePath, + GameMain.GameSession = new GameSession(new Submarine(subPath, ""), savePath, GameModePreset.List.Find(g => g.Identifier == "multiplayercampaign")); var campaign = ((MultiPlayerCampaign)GameMain.GameSession.GameMode); campaign.GenerateMap(seed); @@ -72,6 +72,27 @@ namespace Barotrauma }); } + public bool AllowedToEndRound(Character interactor) + { + if (interactor == null || Level.Loaded?.StartOutpost == null || Level.Loaded?.EndOutpost == null) + { + return false; + } + + if (interactor.Submarine == Level.Loaded.StartOutpost && + interactor.CanInteractWith(startWatchman)) + { + return true; + } + if (interactor.Submarine == Level.Loaded.EndOutpost && + interactor.CanInteractWith(endWatchman)) + { + return true; + } + + return false; + } + protected override void WatchmanInteract(Character watchman, Character interactor) { if ((watchman.Submarine == Level.Loaded.StartOutpost && !Submarine.MainSub.AtStartPosition) || @@ -85,8 +106,7 @@ namespace Barotrauma if (GameMain.Server != null) { var client = GameMain.Server.ConnectedClients.Find(c => c.Character == interactor); - hasPermissions = client != null && - (client.HasPermission(ClientPermissions.ManageRound) || client.HasPermission(ClientPermissions.ManageCampaign)); + hasPermissions = client != null; CreateDialog(new List { watchman }, hasPermissions ? "WatchmanInteract" : "WatchmanInteractNotAllowed", 1.0f); } } @@ -231,6 +251,7 @@ namespace Barotrauma } lastSaveID++; + DebugConsole.Log("Campaign saved, save ID " + lastSaveID); } } } diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs index 04ca33c0d..7bf36f0f8 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Repairable.cs @@ -1,9 +1,5 @@ using Barotrauma.Networking; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Lidgren.Network; namespace Barotrauma.Items.Components { @@ -14,5 +10,16 @@ namespace Barotrauma.Items.Components //let the clients know the initial deterioration delay item.CreateServerEvent(this); } + + public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) + { + if (c.Character == null) return; + StartRepairing(c.Character); + } + + public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) + { + msg.Write(deteriorationTimer); + } } } diff --git a/Barotrauma/BarotraumaServer/Source/Items/Item.cs b/Barotrauma/BarotraumaServer/Source/Items/Item.cs index 5f88e71eb..beb831abf 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Item.cs @@ -335,6 +335,14 @@ namespace Barotrauma { if (GameMain.Server == null) return; + if (!ItemList.Contains(this)) + { + string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } + int index = components.IndexOf(ic); if (index == -1) return; diff --git a/Barotrauma/BarotraumaServer/Source/Map/Hull.cs b/Barotrauma/BarotraumaServer/Source/Map/Hull.cs index 907ba6f45..3b10a7a24 100644 --- a/Barotrauma/BarotraumaServer/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/Source/Map/Hull.cs @@ -20,6 +20,16 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam) { if (IdFreed) { return; } + + //don't create updates if all clients are very far from the hull + float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance; + if (!GameMain.Server.ConnectedClients.Any(c => + c.Character != null && + Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr)) + { + return; + } + //update client hulls if the amount of water has changed by >10% //or if oxygen percentage has changed by 5% if (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f || @@ -31,8 +41,8 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(this); lastSentVolume = waterVolume; lastSentOxygen = OxygenPercentage; - sendUpdateTimer = NetworkUpdateInterval; - } + sendUpdateTimer = NetConfig.HullUpdateInterval; + } } } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs index 78ff44d7a..28e291e39 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs @@ -46,7 +46,7 @@ namespace Barotrauma.Networking //when was a specific entity event last sent to the client // key = event id, value = NetTime.Now when sending - public readonly Dictionary EntityEventLastSent = new Dictionary(); + public readonly Dictionary EntityEventLastSent = new Dictionary(); //when was a position update for a given entity last sent to the client // key = entity id, value = NetTime.Now when sending diff --git a/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs index 2303577e9..88ad64ba2 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/FileTransfer/FileSender.cs @@ -187,7 +187,7 @@ namespace Barotrauma.Networking transfer.WaitTimer -= deltaTime; if (transfer.WaitTimer > 0.0f) continue; - if (!transfer.Connection.CanSendImmediately(NetDeliveryMethod.ReliableOrdered, 1)) continue; + if (!transfer.Connection.CanSendImmediately(NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel)) continue; transfer.WaitTimer = 0.05f;// transfer.Connection.AverageRoundtripTime; @@ -202,15 +202,6 @@ namespace Barotrauma.Networking { message = peer.CreateMessage(); message.Write((byte)ServerPacketHeader.FILE_TRANSFER); - message.Write((byte)FileTransferMessageType.Initiate); - message.Write((byte)transfer.FileType); - message.Write((ushort)chunkLen); - message.Write((ulong)transfer.Data.Length); - message.Write(transfer.FileName); - GameMain.Server.CompressOutgoingMessage(message); - transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel); - - transfer.Status = FileTransferStatus.Sending; //if the recipient is the owner of the server (= a client running the server from the main exe) //we don't need to send anything, the client can just read the file directly diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs index 6ae2167ec..e9d5f2991 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs @@ -43,6 +43,7 @@ namespace Barotrauma.Networking private IRestResponse masterServerResponse; private bool autoRestartTimerRunning; + private float endRoundTimer; public VoipServer VoipServer { @@ -405,20 +406,38 @@ namespace Barotrauma.Networking } } + if (isCrewDead && respawnManager == null) + { + if (endRoundTimer <= 0.0f) + { + SendChatMessage(TextManager.Get("CrewDeadNoRespawns").Replace("[time]", "60"), ChatMessageType.Server); + } + endRoundTimer += deltaTime; + } + else + { + endRoundTimer = 0.0f; + } + //restart if all characters are dead or submarine is at the end of the level if ((serverSettings.AutoRestart && isCrewDead) || - (serverSettings.EndRoundAtLevelEnd && subAtLevelEnd)) + (serverSettings.EndRoundAtLevelEnd && subAtLevelEnd) + || + (isCrewDead && respawnManager == null && endRoundTimer >= 60.0f)) { if (serverSettings.AutoRestart && isCrewDead) { Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage); } - else + else if (serverSettings.EndRoundAtLevelEnd && subAtLevelEnd) { Log("Ending round (submarine reached the end of the level)", ServerLog.MessageType.ServerMessage); } - + else + { + Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage); + } EndGame(); return; } @@ -697,13 +716,24 @@ namespace Barotrauma.Networking string savePath = inc.ReadString(); string seed = inc.ReadString(); string subName = inc.ReadString(); + string subHash = inc.ReadString(); - if (connectedClient.HasPermission(ClientPermissions.SelectMode)) MultiPlayerCampaign.StartNewCampaign(savePath, subName, seed); - } + var matchingSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); + + if (matchingSub == null) + { + SendDirectChatMessage( + TextManager.Get("CampaignStartFailedSubNotFound").Replace("[subname]", subName), + connectedClient, ChatMessageType.MessageBox); + } + else + { + if (connectedClient.HasPermission(ClientPermissions.SelectMode)) MultiPlayerCampaign.StartNewCampaign(savePath, matchingSub.FilePath, seed); + } + } else { string saveName = inc.ReadString(); - if (connectedClient.HasPermission(ClientPermissions.SelectMode)) MultiPlayerCampaign.LoadCampaign(saveName); } break; @@ -734,6 +764,64 @@ namespace Barotrauma.Networking fileSender.ReadFileRequest(inc, connectedClient); } break; + case ClientPacketHeader.ERROR: + HandleClientError(inc, connectedClient); + break; + } + } + + private void HandleClientError(NetIncomingMessage inc, Client c) + { + string errorStr = "Unhandled error report"; + + ClientNetError error = (ClientNetError)inc.ReadByte(); + int levelEqualityCheckVal = inc.ReadInt32(); + switch (error) + { + case ClientNetError.MISSING_EVENT: + UInt16 expectedID = inc.ReadUInt16(); + UInt16 receivedID = inc.ReadUInt16(); + errorStr = "Expecting event id " + expectedID.ToString() + ", received " + receivedID.ToString(); + break; + case ClientNetError.MISSING_ENTITY: + UInt16 eventID = inc.ReadUInt16(); + UInt16 entityID = inc.ReadUInt16(); + Entity entity = Entity.FindEntityByID(entityID); + if (entity == null) + { + errorStr = "Received an update for an entity that doesn't exist (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + } + else if (entity is Character character) + { + errorStr = "Missing character " + character.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + } + else if (entity is Item item) + { + errorStr = "Missing item " + item.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + } + else + { + errorStr = "Missing entity " + entity.ToString() + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + } + break; + } + + if (Level.Loaded != null && levelEqualityCheckVal != Level.Loaded.EqualityCheckVal) + { + errorStr += " Level equality check failed. The level generated at your end doesn't match the level generated by the server (seed " + Level.Loaded.Seed + ")."; + } + + Log(c.Name + " has reported an error: " + errorStr, ServerLog.MessageType.Error); + GameAnalyticsManager.AddErrorEventOnce("GameServer.HandleClientError:LevelsDontMatch" + error, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorStr); + + if (c.Connection == OwnerConnection) + { + SendDirectChatMessage(errorStr, c, ChatMessageType.MessageBox); + EndGame(); + } + else + { + KickClient(c, errorStr); } } @@ -791,8 +879,8 @@ namespace Barotrauma.Networking //(the server started a new campaign and the client isn't aware of it yet?) if (campaign.CampaignID != campaignID) { - c.LastRecvCampaignSave = 0; - c.LastRecvCampaignUpdate = 0; + c.LastRecvCampaignSave = (ushort)(campaign.LastSaveID - 1); + c.LastRecvCampaignUpdate = (ushort)(campaign.LastUpdateID - 1); } } } @@ -946,7 +1034,17 @@ namespace Barotrauma.Networking return; } - if (!sender.HasPermission(command)) + //clients are allowed to end the round by talking with the watchman in multiplayer + //campaign even if they don't have the special permission + if (command == ClientPermissions.ManageRound && inc.PeekBoolean() && + GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign) + { + if (!mpCampaign.AllowedToEndRound(sender.Character)) + { + return; + } + } + else if (!sender.HasPermission(command)) { Log("Client \"" + sender.Name + "\" sent a server command \"" + command + "\". Permission denied.", ServerLog.MessageType.ServerMessage); return; @@ -1248,10 +1346,6 @@ namespace Barotrauma.Networking WriteClientList(c, outmsg); clientListBytes = outmsg.LengthBytes - clientListBytes; - int eventManagerBytes = outmsg.LengthBytes; - entityEventManager.Write(c, outmsg, out List sentEvents); - eventManagerBytes = outmsg.LengthBytes - eventManagerBytes; - int chatMessageBytes = outmsg.LengthBytes; WriteChatMessages(outmsg, c); chatMessageBytes = outmsg.LengthBytes - chatMessageBytes; @@ -1300,25 +1394,55 @@ namespace Barotrauma.Networking errorMsg += " Client list size: " + clientListBytes + " bytes\n" + " Chat message size: " + chatMessageBytes + " bytes\n" + - " Event size: " + eventManagerBytes + " bytes\n" + " Position update size: " + positionUpdateBytes + " bytes\n\n"; - - if (sentEvents != null && sentEvents.Count > 0) - { - errorMsg += "Sent events: \n"; - foreach (var entityEvent in sentEvents) - { - errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n"; - } - } - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } CompressOutgoingMessage(outmsg); - server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.Unreliable); + + //--------------------------------------------------------------------------- + + for (int i = 0; i < NetConfig.MaxEventPacketsPerUpdate; i++) + { + outmsg = server.CreateMessage(); + outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME); + outmsg.Write((float)NetTime.Now); + + int eventManagerBytes = outmsg.LengthBytes; + entityEventManager.Write(c, outmsg, out List sentEvents); + eventManagerBytes = outmsg.LengthBytes - eventManagerBytes; + + if (sentEvents.Count == 0) + { + break; + } + + outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE); + + if (outmsg.LengthBytes > NetPeerConfiguration.MaximumTransmissionUnit) + { + string errorMsg = "Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + NetPeerConfiguration.MaximumTransmissionUnit + ")\n"; + errorMsg += + " Event size: " + eventManagerBytes + " bytes\n"; + + if (sentEvents != null && sentEvents.Count > 0) + { + errorMsg += "Sent events: \n"; + foreach (var entityEvent in sentEvents) + { + errorMsg += " - " + (entityEvent.Entity?.ToString() ?? "null") + "\n"; + } + } + + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame2:PacketSizeExceeded" + outmsg.LengthBytes, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + } + + CompressOutgoingMessage(outmsg); + server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.Unreliable); + } } private void WriteClientList(Client c, NetOutgoingMessage outmsg) @@ -1562,9 +1686,23 @@ namespace Barotrauma.Networking Rand.SetSyncedSeed(roundStartSeed); int teamCount = 1; - MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? + MultiPlayerCampaign campaign = selectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; + if (campaign != null && campaign.Map == null) + { + initiatedStartGame = false; + startGameCoroutine = null; + string errorMsg = "Starting the round failed. Campaign was still active, but the map has been disposed. Try selecting another game mode."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("GameServer.StartGame:InvalidCampaignState", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + if (OwnerConnection != null) + { + SendDirectChatMessage(errorMsg, connectedClients.Find(c => c.Connection == OwnerConnection), ChatMessageType.Error); + } + yield return CoroutineStatus.Failure; + } + //don't instantiate a new gamesession if we're playing a campaign if (campaign == null || GameMain.GameSession == null) { @@ -1589,15 +1727,17 @@ namespace Barotrauma.Networking mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); campaign.AssignClientCharacterInfos(connectedClients); + Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); + Log("Submarine: " + GameMain.GameSession.Submarine.Name, ServerLog.MessageType.ServerMessage); + Log("Level seed: " + campaign.Map.SelectedConnection.Level.Seed, ServerLog.MessageType.ServerMessage); } else { GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty, teamCount > 1); - } - - Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); - Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); - Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); + Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); + Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); + Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); + } bool missionAllowRespawn = campaign == null && (!(GameMain.GameSession.GameMode is MissionMode) || @@ -1759,6 +1899,7 @@ namespace Barotrauma.Networking msg.Write(seed); msg.Write(GameMain.GameSession.Level.Seed); + msg.Write(GameMain.GameSession.Level.EqualityCheckVal); msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write((byte)GameMain.Config.LosMode); @@ -1820,6 +1961,8 @@ namespace Barotrauma.Networking Mission mission = GameMain.GameSession.Mission; GameMain.GameSession.GameMode.End(endMessage); + + endRoundTimer = 0.0f; if (serverSettings.AutoRestart) { @@ -1897,14 +2040,7 @@ namespace Barotrauma.Networking public override void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } - if (message.Sender != null) - { - Log($"{message.Sender}: {message.Text}", ServerLog.MessageType.Chat); - } - else - { - Log($"{message.Text}", ServerLog.MessageType.Chat); - } + Log(message.TextWithSender, ServerLog.MessageType.Chat); base.AddChatMessage(message); } @@ -2367,6 +2503,8 @@ namespace Barotrauma.Networking public void SendVoteStatus(List recipients) { + if (!recipients.Any()) { return; } + NetOutgoingMessage msg = server.CreateMessage(); msg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); msg.Write((byte)ServerNetObject.VOTE); @@ -2419,7 +2557,10 @@ namespace Barotrauma.Networking recipients.Add(otherClient.Connection); } } - server.SendMessage(msg, recipients, NetDeliveryMethod.ReliableUnordered, 0); + if (recipients.Any()) + { + server.SendMessage(msg, recipients, NetDeliveryMethod.ReliableUnordered, 0); + } serverSettings.SaveClientPermissions(); } @@ -2453,13 +2594,15 @@ namespace Barotrauma.Networking public void UpdateCheatsEnabled() { + if (!connectedClients.Any()) { return; } + var msg = server.CreateMessage(); msg.Write((byte)ServerPacketHeader.CHEATS_ENABLED); msg.Write(DebugConsole.CheatsEnabled); msg.WritePadBits(); CompressOutgoingMessage(msg); - + server.SendMessage(msg, connectedClients.Select(c => c.Connection).ToList(), NetDeliveryMethod.ReliableUnordered, 0); } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs index fe0b66db2..4bd7a0077 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/GameServerLogin.cs @@ -158,15 +158,15 @@ namespace Barotrauma.Networking } //kick connected client if status becomes invalid (e.g. VAC banned, not connected to steam) - if (status != Facepunch.Steamworks.ServerAuth.Status.OK && GameMain.Config.RequireSteamAuthentication) + /*if (status != Facepunch.Steamworks.ServerAuth.Status.OK && GameMain.Config.RequireSteamAuthentication) { var connectedClient = connectedClients.Find(c => c.SteamID == ownerID); if (connectedClient != null) { Log("Disconnecting client " + connectedClient.Name + " (Steam ID: " + steamID + "). Steam authentication no longer valid (" + status + ").", ServerLog.MessageType.ServerMessage); - KickClient(connectedClient, $"DisconnectMessage.SteamAuthNoLongerValid_[status]={status.ToString()}"); + KickClient(connectedClient, $"DisconnectMessage.SteamAuthNoLongerValid~[status]={status.ToString()}"); } - } + }*/ } private bool IsServerOwner(NetIncomingMessage inc, NetConnection senderConnection) @@ -342,7 +342,7 @@ namespace Barotrauma.Networking if (clVersion != GameMain.Version.ToString()) { DisconnectUnauthClient(inc, unauthClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion_[version]={GameMain.Version.ToString()}_[clientversion]={clVersion}"); + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={clVersion}"); Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong game version)", ServerLog.MessageType.Error); DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong game version)", Color.Red); @@ -368,7 +368,7 @@ namespace Barotrauma.Networking if (missingPackages.Count == 1) { - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackage_[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); + DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); return; } @@ -376,7 +376,7 @@ namespace Barotrauma.Networking { List packageStrs = new List(); missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackages_[missingcontentpackages]={string.Join(", ", packageStrs)}"); + DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); return; } @@ -399,7 +399,7 @@ namespace Barotrauma.Networking if (incompatiblePackages.Count == 1) { DisconnectUnauthClient(inc, unauthClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackage_[incompatiblecontentpackage]={GetPackageStr2(incompatiblePackages[0])}"); + $"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr2(incompatiblePackages[0])}"); Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible content package " + GetPackageStr2(incompatiblePackages[0]) + ")", ServerLog.MessageType.Error); return; } @@ -408,7 +408,7 @@ namespace Barotrauma.Networking List packageStrs = new List(); incompatiblePackages.ForEach(cp => packageStrs.Add(GetPackageStr2(cp))); DisconnectUnauthClient(inc, unauthClient, DisconnectReason.IncompatibleContentPackage, - $"DisconnectMessage.IncompatibleContentPackages_[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"); + $"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"); Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); return; } @@ -500,7 +500,7 @@ namespace Barotrauma.Networking private void DisconnectUnauthClient(NetIncomingMessage inc, UnauthenticatedClient unauthClient, DisconnectReason reason, string message) { - inc.SenderConnection.Disconnect(reason.ToString() + "/ " + message); + inc.SenderConnection.Disconnect(reason.ToString() + "/ " + TextManager.GetServerMessage(message)); if (unauthClient.SteamID > 0) { Steam.SteamManager.StopAuthSession(unauthClient.SteamID); } if (unauthClient != null) { diff --git a/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs index b710434c8..d7c3f4ea0 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -1,4 +1,5 @@ -using Lidgren.Network; +using Barotrauma.Extensions; +using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -208,7 +209,6 @@ namespace Barotrauma.Networking lastSentToAll = owner.LastRecvEntityEventID; } } - inGameClients.ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) lastSentToAll = c.LastRecvEntityEventID; }); clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); }); @@ -228,8 +228,8 @@ namespace Barotrauma.Networking GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago)" + - " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.ServerMessage); - server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesync"); + " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error); + server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesyncOldEvent"); } ); } @@ -242,8 +242,8 @@ namespace Barotrauma.Networking toKick.ForEach(c => { DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red); - GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected removed event" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.ServerMessage); - server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesync"); + GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error); + server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesyncRemovedEvent"); }); } } @@ -251,7 +251,7 @@ namespace Barotrauma.Networking var timedOutClients = clients.FindAll(c => c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut); foreach (Client timedOutClient in timedOutClients) { - GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.ServerMessage); + GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); GameMain.Server.DisconnectClient(timedOutClient, "", "ServerMessage.SyncTimeout"); } @@ -305,19 +305,7 @@ namespace Barotrauma.Networking } //too many events for one packet - if (eventsToSync.Count > MaxEventsPerWrite) - { - if (eventsToSync.Count > MaxEventsPerWrite * 3 && !client.NeedsMidRoundSync) - { - Color color = eventsToSync.Count > MaxEventsPerWrite * 20 ? Color.Red : Color.Orange; - if (eventsToSync.Count < MaxEventsPerWrite * 5) { color = Color.Yellow; } - DebugConsole.NewMessage("WARNING: event count very high: " + eventsToSync.Count + "/" + MaxEventsPerWrite, color); - } - - eventsToSync.RemoveRange(MaxEventsPerWrite, eventsToSync.Count - MaxEventsPerWrite); - } - - foreach (NetEntityEvent entityEvent in eventsToSync) + if (eventsToSync.Count > 200) { if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync) { @@ -346,25 +334,23 @@ namespace Barotrauma.Networking if (client.NeedsMidRoundSync) { - msg.Write((byte)ServerNetObject.ENTITY_EVENT_INITIAL); - - //how many (unique) events the clients had missed before joining - client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count; - //ID of the first event sent after the client joined - //(after the client has been synced they'll switch their lastReceivedID - //to the one before this, and the eventmanagers will start to function "normally") - client.FirstNewEventID = events.Count == 0 ? (UInt16)0 : events[events.Count - 1].ID; - + msg.Write((byte)ServerNetObject.ENTITY_EVENT_INITIAL); msg.Write(client.UnreceivedEntityEventCount); msg.Write(client.FirstNewEventID); - Write(msg, eventsToSync, client); + Write(msg, eventsToSync, out sentEvents, client); } else { msg.Write((byte)ServerNetObject.ENTITY_EVENT); Write(msg, eventsToSync, out sentEvents, client); } + + foreach (NetEntityEvent entityEvent in sentEvents) + { + (entityEvent as ServerEntityEvent).Sent = true; + client.EntityEventLastSent[entityEvent.ID] = NetTime.Now; + } sentEvents = eventsToSync; } @@ -389,10 +375,10 @@ namespace Barotrauma.Networking for (int i = startIndex; i < eventList.Count; i++) { - //find the first event that hasn't been sent in 1.5 * roundtriptime or at all - client.EntityEventLastSent.TryGetValue(eventList[i].ID, out float lastSent); + //find the first event that hasn't been sent in roundtriptime or at all + client.EntityEventLastSent.TryGetValue(eventList[i].ID, out double lastSent); - float minInterval = Math.Max(client.Connection.AverageRoundtripTime * 1.5f, (float)server.UpdateInterval.TotalSeconds * 2); + float minInterval = Math.Max(client.Connection.AverageRoundtripTime, (float)server.UpdateInterval.TotalSeconds * 2); if (lastSent > NetTime.Now - Math.Min(minInterval, 0.5f)) { @@ -418,7 +404,7 @@ namespace Barotrauma.Networking } else { - double midRoundSyncTimeOut = uniqueEvents.Count / MaxEventsPerWrite * server.UpdateInterval.TotalSeconds; + double midRoundSyncTimeOut = uniqueEvents.Count / 100 * server.UpdateInterval.TotalSeconds; midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 10.0f); client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs index 7432cf88f..b282901ce 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Voip/VoipServer.cs @@ -78,6 +78,7 @@ namespace Barotrauma.Networking bool senderSpectating = sender.Character == null || sender.Character.IsDead; //spectators cannot speak with in-game players or vice versa + //TODO: allow spectators to hear the voice chat if close enough to the speaker? if (recipientSpectating != senderSpectating) { return false; } //both spectating, no need to do radio/distance checks diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs b/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs index 6aa489dc4..67f5012ba 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Voting.cs @@ -116,8 +116,8 @@ namespace Barotrauma msg.Write(AllowEndVoting); if (AllowEndVoting) { - msg.Write((byte)GameMain.Server.ConnectedClients.Count(v => v.GetVote(VoteType.EndRound))); - msg.Write((byte)GameMain.Server.ConnectedClients.Count); + msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound))); + msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned)); } msg.Write(AllowVoteKick); diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index c65e80969..4b478cc34 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -73,6 +73,9 @@ + + + diff --git a/Barotrauma/BarotraumaShared/SharedContent.projitems b/Barotrauma/BarotraumaShared/SharedContent.projitems index 29ff67737..264aee20c 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.projitems +++ b/Barotrauma/BarotraumaShared/SharedContent.projitems @@ -1670,6 +1670,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -3150,8 +3156,10 @@ PreserveNewest + PreserveNewest + PreserveNewest PreserveNewest diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs index 4ce4ecd37..5384ef408 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/AITarget.cs @@ -110,7 +110,7 @@ namespace Barotrauma SonarLabel = element.GetAttributeString("sonarlabel", ""); } - public AITarget(Entity e) + public AITarget(Entity e, float sightRange = 3000, float soundRange = 0) { Entity = e; SightRange = sightRange; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs index 9852a296a..25829fc34 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs @@ -26,7 +26,6 @@ namespace Barotrauma } } - public class TargetingPriority { public string TargetTag; @@ -100,6 +99,9 @@ namespace Barotrauma private readonly float aggressiongreed; private readonly float aggressionhurt; + // TODO: expose? + private readonly float priorityFearIncreasement = 2; + private readonly float memoryFadeTime = 0.5f; public bool AttackHumans { @@ -249,8 +251,7 @@ namespace Barotrauma public override void SelectTarget(AITarget target) { SelectedAiTarget = target; - selectedTargetMemory = FindTargetMemory(target); - + selectedTargetMemory = GetTargetMemory(target); targetValue = 100.0f; } @@ -287,8 +288,7 @@ namespace Barotrauma } else { - TargetingPriority targetingPriority = null; - UpdateTargets(Character, out targetingPriority); + var targetingPriority = UpdateTargets(Character); updateTargetsTimer = UpdateTargetsInterval; if (SelectedAiTarget == null) @@ -395,13 +395,44 @@ namespace Barotrauma State = AIState.Idle; return; } - - Vector2 escapeDir = Vector2.Normalize(SimPosition - SelectedAiTarget.SimPosition); - if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; - SteeringManager.SteeringManual(deltaTime, escapeDir); - SteeringManager.SteeringWander(); - if (Character.CurrentHull == null) + else if (selectedTargetMemory != null) { + selectedTargetMemory.Priority += deltaTime * priorityFearIncreasement; + } + if (Character.CurrentHull != null) + { + // Seek exit, if inside + if (SteeringManager is IndoorsSteeringManager indoorSteering && escapePoint == Vector2.Zero) + { + foreach (Gap gap in Gap.GapList) + { + if (gap.Submarine != Character.Submarine) { continue; } + if (gap.Open < 1 || gap.IsRoomToRoom) { continue; } + var path = indoorSteering.PathFinder.FindPath(Character.SimPosition, gap.SimPosition); + if (!path.Unreachable) + { + if (escapePoint != Vector2.Zero) + { + // Ignore the gap if it's further away than the previously assigned escape point + if (Vector2.DistanceSquared(Character.SimPosition, gap.SimPosition) > Vector2.DistanceSquared(Character.SimPosition, escapePoint)) { continue; } + } + escapePoint = gap.SimPosition; + } + } + } + } + if (escapePoint != Vector2.Zero && Vector2.DistanceSquared(Character.SimPosition, escapePoint) > 1) + { + SteeringManager.SteeringSeek(escapePoint); + } + else + { + // If outside or near enough the escapePoint, steer away + escapePoint = Vector2.Zero; + Vector2 escapeDir = Vector2.Normalize(WorldPosition - SelectedAiTarget.WorldPosition); + if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; + SteeringManager.SteeringManual(deltaTime, escapeDir); + SteeringManager.SteeringWander(); SteeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f); } } @@ -418,14 +449,8 @@ namespace Barotrauma return; } - selectedTargetMemory.Priority -= deltaTime * 0.1f; - - Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition; - - if (Character.Submarine != null && SelectedAiTarget.Entity.Submarine != null && Character.Submarine != SelectedAiTarget.Entity.Submarine) - { - attackSimPosition = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - Character.Submarine.Position); - } + Vector2 attackWorldPos = SelectedAiTarget.WorldPosition; + Vector2 attackSimPos = SelectedAiTarget.SimPosition; if (SelectedAiTarget.Entity is Item item) { @@ -441,22 +466,24 @@ namespace Barotrauma } } - if (wallTarget != null) + if (raycastTimer > 0.0) { - attackSimPosition = ConvertUnits.ToSimUnits(wallTarget.Position); - if (Character.Submarine == null && SelectedAiTarget.Entity?.Submarine != null) - { - attackSimPosition += ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position); - } + raycastTimer -= deltaTime; } - else if (SelectedAiTarget.Entity is Character c) + else + { + UpdateWallTarget(); + raycastTimer = RaycastInterval; + } + + if (SelectedAiTarget.Entity is Character c) { //target the closest limb if the target is a character - float closestDist = Vector2.DistanceSquared(SelectedAiTarget.SimPosition, SimPosition) * 10.0f; - foreach (Limb limb in ((Character)SelectedAiTarget.Entity).AnimController.Limbs) + float closestDist = Vector2.DistanceSquared(SelectedAiTarget.WorldPosition, WorldPosition) * 10.0f; + foreach (Limb limb in c.AnimController.Limbs) { if (limb == null) continue; - float dist = Vector2.DistanceSquared(limb.SimPosition, SimPosition) / Math.Max(limb.AttackPriority, 0.1f); + float dist = Vector2.DistanceSquared(limb.WorldPosition, WorldPosition) / Math.Max(limb.AttackPriority, 0.1f); if (dist < closestDist) { closestDist = dist; @@ -466,12 +493,30 @@ namespace Barotrauma } } - if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater) + if (wallTarget != null) { - Character.AnimController.TargetDir = Character.SimPosition.X < attackSimPosition.X ? Direction.Right : Direction.Left; + attackWorldPos = wallTarget.Position; + attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos); + } + else + { + // Take the sub position into account in the sim pos + if (Character.Submarine == null && SelectedAiTarget.Entity.Submarine != null) + { + attackSimPos += SelectedAiTarget.Entity.Submarine.SimPosition; + } + else if (Character.Submarine != null && SelectedAiTarget.Entity.Submarine == null) + { + attackSimPos -= Character.Submarine.SimPosition; + } } - if (raycastTimer > 0.0) + if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater) + { + Character.AnimController.TargetDir = Character.WorldPosition.X < attackWorldPos.X ? Direction.Right : Direction.Left; + } + + if (aggressiveBoarding) { //targeting a wall section that can be passed through -> steer manually through the hole if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex)) @@ -529,72 +574,6 @@ namespace Barotrauma } } } - else - { - UpdateWallTarget(); - raycastTimer = RaycastInterval; - } - - if (aggressiveBoarding) - { - //targeting a wall section that can be passed through -> steer manually through the hole - if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex)) - { - WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex); - Hull targetHull = section.gap?.FlowTargetHull; - if (targetHull != null && !section.gap.IsRoomToRoom) - { - Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true); - if (wallTarget.Structure.IsHorizontal) - { - targetPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2; - } - else - { - targetPos.X = targetHull.WorldRect.Center.X; - } - - latchOntoAI?.DeattachFromBody(); - Character.AnimController.ReleaseStuckLimbs(); - if (steeringManager is IndoorsSteeringManager) - { - steeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetPos - Character.WorldPosition)); - } - else - { - steeringManager.SteeringSeek(ConvertUnits.ToSimUnits(targetPos)); - } - return; - } - } - else if (SelectedAiTarget.Entity is Item) - { - var door = ((Item)SelectedAiTarget.Entity).GetComponent(); - //steer through the door manually if it's open or broken - if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.Item.Condition <= 0.0f)) - { - var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition); - if (door.LinkedGap.IsHorizontal) - { - if (Character.WorldPosition.Y < door.Item.WorldRect.Y && Character.WorldPosition.Y > door.Item.WorldRect.Y - door.Item.Rect.Height) - { - velocity.Y = 0; - steeringManager.SteeringManual(deltaTime, velocity); - return; - } - } - else - { - if (Character.WorldPosition.X < door.Item.WorldRect.X && Character.WorldPosition.X > door.Item.WorldRect.Right) - { - velocity.X = 0; - steeringManager.SteeringManual(deltaTime, velocity); - return; - } - } - } - } - } bool canAttack = true; if (IsCoolDownRunning) @@ -612,7 +591,7 @@ namespace Barotrauma } else { - UpdateFallBack(attackSimPosition, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime); return; } } @@ -620,14 +599,14 @@ namespace Barotrauma { if (attackingLimb.attack.SecondaryCoolDownTimer <= 0) { - // Don't allow attacking when the attack target has changed. + // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { canAttack = false; if (attackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. - UpdateFallBack(attackSimPosition, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime); return; } attackingLimb = null; @@ -636,7 +615,7 @@ namespace Barotrauma { // If the secondary cooldown is defined and expired, check if we can switch the attack var previousLimb = attackingLimb; - var newLimb = GetAttackLimb(attackSimPosition, previousLimb); + var newLimb = GetAttackLimb(attackWorldPos, previousLimb); if (newLimb != null) { attackingLimb = newLimb; @@ -650,7 +629,7 @@ namespace Barotrauma } else { - UpdateFallBack(attackSimPosition, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime); return; } } @@ -665,15 +644,15 @@ namespace Barotrauma break; case AIBehaviorAfterAttack.FallBack: default: - UpdateFallBack(attackSimPosition, deltaTime); + UpdateFallBack(attackWorldPos, deltaTime); return; } } - if (attackingLimb == null) + if (attackingLimb == null || _previousAiTarget != SelectedAiTarget) { - attackingLimb = GetAttackLimb(attackSimPosition); + attackingLimb = GetAttackLimb(attackWorldPos); } if (canAttack) { @@ -683,24 +662,39 @@ namespace Barotrauma if (canAttack) { // Check that we can reach the target - distance = ConvertUnits.ToDisplayUnits(Vector2.Distance(attackingLimb.SimPosition, attackSimPosition)); + distance = Vector2.Distance(attackingLimb.WorldPosition, attackWorldPos); canAttack = distance < attackingLimb.attack.Range; } - Limb steeringLimb = Character.AnimController.MainLimb; + // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. For example the Hammerhead steering with the claws when it should use the torso. + // If we always use the main limb, this causes the character to seek the target with it's torso/head, when it should not. For example Mudraptor steering with it's belly, when it should use it's head. + // So let's use the one that's closer to the attacking limb. + Limb steeringLimb; + var torso = Character.AnimController.GetLimb(LimbType.Torso); + var head = Character.AnimController.GetLimb(LimbType.Head); + if (attackingLimb == null) + { + steeringLimb = head ?? torso; + } + else + { + if (head != null && torso != null) + { + steeringLimb = Vector2.DistanceSquared(attackingLimb.SimPosition, head.SimPosition) < Vector2.DistanceSquared(attackingLimb.SimPosition, torso.SimPosition) ? head : torso; + } + else + { + steeringLimb = head ?? torso; + } + } if (steeringLimb != null) { - Vector2 steeringVector = attackSimPosition - steeringLimb.SimPosition; - Vector2 targetingVector = Vector2.Normalize(steeringVector) * attackingLimb.attack.Range; - // Offset the position a bit so that we don't overshoot the movement. - Vector2 steerPos = attackSimPosition + targetingVector; - steeringManager.SteeringSeek(steerPos, 10); - if (Character.CurrentHull == null) - { - SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f); - } + Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; + // Offset so that we don't overshoot the movement + Vector2 steerPos = attackSimPos + offset; + SteeringManager.SteeringSeek(steerPos, 10); - if (steeringManager is IndoorsSteeringManager indoorsSteering) + if (SteeringManager is IndoorsSteeringManager indoorsSteering) { if (indoorsSteering.CurrentPath != null && !indoorsSteering.IsPathDirty) { @@ -715,7 +709,7 @@ namespace Barotrauma } else if (indoorsSteering.CurrentPath.Finished) { - steeringManager.SteeringManual(deltaTime, Vector2.Normalize(steeringVector)); + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); } else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null) { @@ -729,40 +723,42 @@ namespace Barotrauma } } } + else if (Character.CurrentHull == null) + { + SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f); + } } if (canAttack) { - UpdateLimbAttack(deltaTime, attackingLimb, attackSimPosition, distance); + UpdateLimbAttack(deltaTime, attackingLimb, attackSimPos, distance); } } - private Limb GetAttackLimb(Vector2 attackSimPosition, Limb ignoredLimb = null) + private bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { - AttackContext currentContext = Character.GetAttackContext(); - var target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity; - var limbs = Character.AnimController.Limbs - .Where(l => - l != ignoredLimb && - l.attack != null && - !l.IsSevered && - !l.IsStuck && - l.attack.IsValidContext(currentContext) && - l.attack.IsValidTarget(target) && - l.attack.Conditionals.All(c => (target is ISerializableEntity se && c.Matches(se)) || !(target is ISerializableEntity) || !(target is Character))) - .OrderByDescending(l => l.attack.Priority) - .ThenBy(l => ConvertUnits.ToDisplayUnits(Vector2.Distance(l.SimPosition, attackSimPosition))); - // TODO: priority should probably not override the distance -> use values instead of booleans - return limbs.FirstOrDefault(); - } - - private void UpdateWallTarget() - { - wallTarget = null; - - if (Character.AnimController.CurrentHull != null) - { - return; + Hull targetHull = section.gap?.FlowTargetHull; + if (targetHull != null) + { + if (wall.IsHorizontal) + { + targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2; + } + else + { + targetWorldPos.X = targetHull.WorldRect.Center.X; + } + latchOntoAI?.DeattachFromBody(); + Character.AnimController.ReleaseStuckLimbs(); + if (steeringManager is IndoorsSteeringManager) + { + steeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition)); + } + else + { + steeringManager.SteeringSeek(ConvertUnits.ToSimUnits(targetWorldPos)); + } + return true; } return false; } @@ -791,32 +787,23 @@ namespace Barotrauma wallTarget = null; //check if there's a wall between the target and the Character - Vector2 rayStart = Character.SimPosition; + Vector2 rayStart = SimPosition; Vector2 rayEnd = SelectedAiTarget.SimPosition; + bool offset = SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null; - if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) + if (offset) { - rayStart -= ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position); + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; } - Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd); + Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); + if (Submarine.LastPickedFraction == 1.0f || closestBody == null) { return; } - Structure wall = closestBody.UserData as Structure; - if (wall?.Submarine == null) - { - return; - /*if (selectedAiTarget.Entity.Submarine != null) - { - wallTarget = new WallTarget(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition), selectedAiTarget.Entity.Submarine); - latchOntoAI?.SetAttachTarget(closestBody, selectedAiTarget.Entity.Submarine, Submarine.LastPickedPosition); - }*/ - //if (selectedAiTarget.Entity.Submarine != null && Character.Submarine == null) wallAttackPos += ConvertUnits.ToSimUnits(selectedAiTarget.Entity.Submarine.Position); - } - else + if (closestBody.UserData is Structure wall && wall.Submarine != null) { int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); int passableHoleCount = GetMinimumPassableHoleCount(); @@ -826,49 +813,46 @@ namespace Barotrauma { if (wall.SectionBodyDisabled(i)) { - if (aggressiveBoarding && CanPassThroughHole(wall, i)) //aggressive boarders always target holes they can pass through + if (aggressiveBoarding && CanPassThroughHole(wall, i)) { + //aggressive boarders always target holes they can pass through sectionIndex = i; break; } - else //otherwise ignore and keep breaking other sections + else { + //otherwise ignore and keep breaking other sections continue; } } if (wall.SectionDamage(i) > sectionDamage) sectionIndex = i; } - Vector2 sectionPos = ConvertUnits.ToSimUnits(wall.SectionPosition(sectionIndex)); + Vector2 sectionPos = wall.SectionPosition(sectionIndex); Vector2 attachTargetNormal; if (wall.IsHorizontal) { - attachTargetNormal = new Vector2(0.0f, Math.Sign(Character.WorldPosition.Y - wall.WorldPosition.Y)); - sectionPos.Y += ConvertUnits.ToSimUnits((wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2) * attachTargetNormal.Y; + attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y)); + sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y; } else { - attachTargetNormal = new Vector2(Math.Sign(Character.WorldPosition.X - wall.WorldPosition.X), 0.0f); - sectionPos.X += ConvertUnits.ToSimUnits((wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2) * attachTargetNormal.X; + attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f); + sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X; } - wallTarget = new WallTarget(ConvertUnits.ToDisplayUnits(sectionPos), wall, sectionIndex); + latchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, sectionPos, attachTargetNormal); + if (wall.Submarine != null) + { + sectionPos += wall.Submarine.Position; + } + wallTarget = new WallTarget(sectionPos, wall, sectionIndex); } } public override void OnAttacked(Character attacker, AttackResult attackResult) { updateTargetsTimer = Math.Min(updateTargetsTimer, 0.1f); - - // Reduce the cooldown so that the character can react - foreach (var limb in Character.AnimController.Limbs) - { - if (limb.attack != null) - { - limb.attack.CoolDownTimer *= 0.1f; - // secondary cooldown? - } - } if (attackResult.Damage > 0.0f && attackWhenProvoked) { @@ -883,37 +867,60 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); if (attacker == null || attacker.AiTarget == null) return; - AITargetMemory targetMemory = FindTargetMemory(attacker.AiTarget); + AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget); targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * aggressionhurt; + + // Reduce the cooldown so that the character can react + // Only allow to react once. Otherwise would attack the target with only a fraction of cooldown + if (SelectedAiTarget != attacker.AiTarget) + { + foreach (var limb in Character.AnimController.Limbs) + { + if (limb.attack != null) + { + limb.attack.CoolDownTimer *= 0.1f; + } + } + } } // 10 dmg, 100 health -> 0.1 private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); - private void UpdateLimbAttack(float deltaTime, Limb limb, Vector2 attackPosition, float distance = -1) + private void UpdateLimbAttack(float deltaTime, Limb limb, Vector2 attackSimPos, float distance = -1) { - var damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; - if (damageTarget == null) return; - - float prevHealth = damageTarget.Health; - if (limb.UpdateAttack(deltaTime, attackPosition, damageTarget, out AttackResult attackResult, distance)) + if (SelectedAiTarget == null) { return; } + if (wallTarget != null) { - if (damageTarget.Health > 0) + // If the selected target is not the wall target, make the wall target the selected target. + var aiTarget = wallTarget.Structure.AiTarget; + if (aiTarget != null && SelectedAiTarget != aiTarget) { - // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * aggressiongreed; + SelectTarget(aiTarget); } } - - if (!limb.attack.IsRunning) + if (SelectedAiTarget.Entity is IDamageable damageTarget) { - wallTarget = null; + float prevHealth = damageTarget.Health; + if (limb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance)) + { + if (damageTarget.Health > 0) + { + // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * aggressiongreed; + } + else + { + selectedTargetMemory.Priority = 0; + } + } } } - private void UpdateFallBack(Vector2 attackPosition, float deltaTime) + private void UpdateFallBack(Vector2 attackWorldPos, float deltaTime) { - float dist = Vector2.Distance(attackPosition, Character.SimPosition); + Vector2 attackVector = attackWorldPos - WorldPosition; + float dist = attackVector.Length(); float desiredDist = colliderSize * 2.0f; if (dist < desiredDist) { @@ -921,7 +928,6 @@ namespace Barotrauma if (!MathUtils.IsValid(attackDir)) attackDir = Vector2.UnitY; steeringManager.SteeringManual(deltaTime, attackDir * (1.0f - (dist / 500.0f))); } - steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f); } @@ -972,15 +978,13 @@ namespace Barotrauma //goes through all the AItargets, evaluates how preferable it is to attack the target, //whether the Character can see/hear the target and chooses the most preferable target within //sight/hearing range - public void UpdateTargets(Character character, out TargetingPriority targetingPriority) + public TargetingPriority UpdateTargets(Character character) { - targetingPriority = null; - SelectedAiTarget = null; + AITarget newTarget = null; + TargetingPriority targetingPriority = null; selectedTargetMemory = null; targetValue = 0.0f; - UpdateTargetMemories(); - foreach (AITarget target in AITarget.List) { if (!target.Enabled) continue; @@ -989,48 +993,67 @@ namespace Barotrauma continue; } - float valueModifier = 1.0f; - float dist = 0.0f; - Character targetCharacter = target.Entity as Character; //ignore the aitarget if it is the Character itself if (targetCharacter == character) continue; + float valueModifier = 1; string targetingTag = null; if (targetCharacter != null) { if (targetCharacter.Submarine != null && Character.Submarine == null) + { + targetingTag = "dead"; + if (targetCharacter.Submarine != Character.Submarine) + { + // In a different sub or the target is outside when we are inside or vice versa -> Ignore the target + continue; + } + else if (targetCharacter.CurrentHull != Character.CurrentHull) + { + // In the same sub, halve the priority, if not in the same hull. + valueModifier = 0.5f; + } + } + else if (targetCharacter.AIController is EnemyAIController enemy) + { + if (enemy.combatStrength > combatStrength) + { + targetingTag = "stronger"; + } + else if (enemy.combatStrength < combatStrength) + { + targetingTag = "weaker"; + } + if (State == AIState.Escape && targetingTag == "stronger") + { + // Frightened + valueModifier = 2; + } + else + { + if (targetCharacter.Submarine != Character.Submarine) + { + // In a different sub or the target is outside when we are inside or vice versa -> Ignore the target + continue; + } + else if (targetCharacter.CurrentHull != Character.CurrentHull) + { + // In the same sub, halve the priority, if not in the same hull. + valueModifier = 0.5f; + } + } + } + else if (targetCharacter.Submarine != null && Character.Submarine == null) { //target inside, AI outside -> we'll be attacking a wall between the characters so use the priority for attacking rooms targetingTag = "room"; } - else if (targetCharacter.IsDead) - { - targetingTag = "dead"; - } else if (targetingPriorities.ContainsKey(targetCharacter.SpeciesName.ToLowerInvariant())) { targetingTag = targetCharacter.SpeciesName.ToLowerInvariant(); } - else if (targetingPriorities.ContainsKey(targetCharacter.SpeciesName.ToLowerInvariant())) - { - targetingTag = targetCharacter.SpeciesName.ToLowerInvariant(); - } - else if (targetingPriorities.ContainsKey(targetCharacter.SpeciesName.ToLowerInvariant())) - { - if (targetCharacter.AIController is EnemyAIController enemy) - { - if (enemy.combatStrength > combatStrength) - { - targetingTag = "stronger"; - } - else if (enemy.combatStrength < combatStrength) - { - targetingTag = "weaker"; - } - } - } } else if (target.Entity != null) { @@ -1056,19 +1079,63 @@ namespace Barotrauma } } } + else if (target.Entity is Structure s) + { + targetingTag = "wall"; + if (aggressiveBoarding) + { + // Ignore walls when inside. + valueModifier = character.CurrentHull == null ? 2 : 0; + if (valueModifier > 0) + { + // Ignore structures that doesn't have a body (not walls) + valueModifier *= s.HasBody ? 1 : 0; + } + for (int i = 0; i < s.Sections.Length; i++) + { + var section = s.Sections[i]; + if (CanPassThroughHole(s, i)) + { + // Ignore walls that can be passed through + valueModifier = 0; + break; + } + else if (section.gap != null) + { + // up to 100% priority increase for every gap in the wall + valueModifier *= 1 + section.gap.Open; + } + } + } + } else { targetingTag = "room"; } - if (door != null) { - //increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside - if (character.CurrentHull == null && aggressiveBoarding && !door.LinkedGap.IsRoomToRoom) + // If there's not a more specific tag for the door + if (string.IsNullOrEmpty(targetingTag) || targetingTag == "room") { - valueModifier = door.IsOpen ? 10 : 5; + targetingTag = "door"; } - else if (door.IsOpen || door.Item.Condition <= 0.0f) //ignore broken and open doors + bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; + bool isOpen = door.IsOpen || door.Item.Condition <= 0.0f; + //increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside + if (aggressiveBoarding) + { + if (character.CurrentHull == null) + { + valueModifier = isOutdoor ? 1 : 0; + valueModifier *= isOpen ? 5 : 1; + } + else + { + valueModifier = isOutdoor ? 0 : 1; + valueModifier *= isOpen ? 0 : 1; + } + } + else if (isOpen) //ignore broken and open doors { continue; } @@ -1084,10 +1151,15 @@ namespace Barotrauma valueModifier *= targetingPriorities[targetingTag].Priority; + if (targetingTag == null) continue; + if (!targetingPriorities.ContainsKey(targetingTag)) continue; + + valueModifier *= targetingPriorities[targetingTag].Priority; + if (valueModifier == 0.0f) continue; Vector2 toTarget = target.WorldPosition - character.WorldPosition; - dist = toTarget.Length(); + float dist = toTarget.Length(); //if the target has been within range earlier, the character will notice it more easily //(i.e. remember where the target was) @@ -1101,28 +1173,29 @@ namespace Barotrauma // -> just ignore the distance and attack whatever has the highest priority dist = Math.Max(dist, 100.0f); - AITargetMemory targetMemory = FindTargetMemory(target); + AITargetMemory targetMemory = GetTargetMemory(target); if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y) { // Inside the sub, treat objects that are up or down, as they were farther away. dist *= 3; } - valueModifier = valueModifier * targetMemory.Priority / (float)Math.Sqrt(dist); + valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist); if (valueModifier > targetValue) { - SelectedAiTarget = target; + newTarget = target; selectedTargetMemory = targetMemory; targetingPriority = targetingPriorities[targetingTag]; targetValue = valueModifier; } } + SelectedAiTarget = newTarget; if (SelectedAiTarget != _previousAiTarget) { wallTarget = null; } - _previousAiTarget = SelectedAiTarget; + return targetingPriority; } private AITargetMemory GetTargetMemory(AITarget target) @@ -1132,69 +1205,39 @@ namespace Barotrauma memory = new AITargetMemory(10); targetMemories.Add(target, memory); } - - memory = new AITargetMemory(10.0f); - targetMemories.Add(target, memory); - return memory; } private List removals = new List(); private void UpdateTargetMemories(float deltaTime) { - List toBeRemoved = null; - foreach (KeyValuePair memory in targetMemories) + removals.Clear(); + foreach (var memory in targetMemories) { - memory.Value.Priority += 0.1f; - if (Math.Abs(memory.Value.Priority) < 1.0f || !AITarget.List.Contains(memory.Key)) + // Slowly decrease all memories + memory.Value.Priority -= memoryFadeTime * deltaTime; + // Remove targets that have no priority or have been removed + if (memory.Value.Priority <= 1 || !AITarget.List.Contains(memory.Key)) { - if (toBeRemoved == null) toBeRemoved = new List(); - toBeRemoved.Add(memory.Key); + removals.Add(memory.Key); } } removals.ForEach(r => targetMemories.Remove(r)); } - if (toBeRemoved != null) - { - foreach (AITarget target in toBeRemoved) - { - targetMemories.Remove(target); - } - } - - #endregion - - protected override void OnStateChanged(AIState from, AIState to) - { - latchOntoAI?.DeattachFromBody(); - Character.AnimController.ReleaseStuckLimbs(); - } - #endregion protected override void OnStateChanged(AIState from, AIState to) { latchOntoAI?.DeattachFromBody(); Character.AnimController.ReleaseStuckLimbs(); + escapePoint = Vector2.Zero; + wallTarget = null; } private int GetMinimumPassableHoleCount() { - return (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderSize) / Structure.WallSectionSize); - } - - #endregion - - protected override void OnStateChanged(AIState from, AIState to) - { - latchOntoAI?.DeattachFromBody(); - Character.AnimController.ReleaseStuckLimbs(); - } - - private int GetMinimumPassableHoleCount() - { - return (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderSize) / Structure.WallSectionSize); + return (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderSize) / Structure.WallSectionSize); } private bool CanPassThroughHole(Structure wall, int sectionIndex) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs index 76a151069..a232f65d1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs @@ -245,7 +245,7 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { if (c.CurrentHull == Character.CurrentHull && !c.IsDead && - (c.AIController is EnemyAIController || c.TeamID != Character.TeamID)) + (c.AIController is EnemyAIController || (c.TeamID != Character.TeamID && Character.TeamID != Character.TeamType.FriendlyNPC && c.TeamID != Character.TeamType.FriendlyNPC))) { var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders"); newOrder = new Order(orderPrefab, Character.CurrentHull, null); @@ -290,7 +290,7 @@ namespace Barotrauma public override void OnAttacked(Character attacker, AttackResult attackResult) { float damage = attackResult.Damage; - if (damage < 0) { return; } + if (damage <= 0) { return; } if (attacker == null || attacker.IsDead || attacker.Removed) { if (objectiveManager.CurrentOrder == null) @@ -466,7 +466,9 @@ namespace Barotrauma // Even the smallest fire reduces the safety by 50% float fire = hull.FireSources.Count * 0.5f + hull.FireSources.Sum(fs => fs.DamageRange) / hull.Size.X; float fireFactor = ignoreFire ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1)); - int enemyCount = Character.CharacterList.Count(e => e.CurrentHull == hull && !e.IsDead && !e.IsUnconscious && (e.AIController is EnemyAIController || e.TeamID != character.TeamID)); + int enemyCount = Character.CharacterList.Count(e => + e.CurrentHull == hull && !e.IsDead && !e.IsUnconscious && + (e.AIController is EnemyAIController || (e.TeamID != character.TeamID && character.TeamID != Character.TeamType.FriendlyNPC && e.TeamID != Character.TeamType.FriendlyNPC))); // The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages) float enemyFactor = ignoreEnemies ? 1 : MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs index 398b37f6b..7fccbf418 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -95,7 +95,7 @@ namespace Barotrauma getItemObjective = new AIObjectiveGetItem(character, itemIdentifiers) { GetItemPriority = GetItemPriority, - IgnoreContainedItems = IgnoreAlreadyContainedItems + ignoredContainerIdentifiers = ignoredContainerIdentifiers }; AddSubObjective(getItemObjective); return; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs index a7b1f8993..f3afac468 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -115,6 +115,7 @@ namespace Barotrauma unreachable.Add(goToObjective.Target as Hull); } goToObjective = null; + SteeringManager.SteeringWander(); } } else if (currentHull != null) @@ -131,6 +132,11 @@ namespace Barotrauma foreach (Character enemy in Character.CharacterList) { + //don't run from friendly NPCs + if (enemy.TeamID == Character.TeamType.FriendlyNPC) { continue; } + //friendly NPCs don't run away from anything but characters controlled by EnemyAIController (= monsters) + if (character.TeamID == Character.TeamType.FriendlyNPC && !(enemy.AIController is EnemyAIController)) { continue; } + if (enemy.CurrentHull == currentHull && !enemy.IsDead && !enemy.IsUnconscious && (enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID)) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs index 1a2ce2e76..7f20924c1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -17,7 +17,7 @@ namespace Barotrauma private string[] itemIdentifiers; private Item targetItem, moveToTarget; private int currSearchIndex; - public bool IgnoreContainedItems; + public string[] ignoredContainerIdentifiers; private AIObjectiveGoTo goToObjective; private float currItemPriority; private bool equip; @@ -99,11 +99,12 @@ namespace Barotrauma FindTargetItem(); if (targetItem == null || moveToTarget == null) { - SteeringManager.Reset(); + SteeringManager.SteeringWander(); return; } - if (Vector2.DistanceSquared(character.Position, moveToTarget.Position) < MathUtils.Pow(targetItem.InteractDistance * 2, 2)) + if (moveToTarget.CurrentHull == character.CurrentHull && + Vector2.DistanceSquared(character.Position, moveToTarget.Position) < MathUtils.Pow(targetItem.InteractDistance * 2, 2)) { int targetSlot = -1; if (equip) @@ -196,8 +197,12 @@ namespace Barotrauma else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } if (item.CurrentHull == null || item.Condition <= 0.0f) { continue; } - if (IgnoreContainedItems && item.Container != null) { continue; } - if (!itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; } + if (itemIdentifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; } + + if (ignoredContainerIdentifiers != null && item.Container != null) + { + if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } + } //if the item is inside a character's inventory, don't steal it unless the character is dead if (item.ParentInventory is CharacterInventory) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs index ae20818b2..c5dd9b58c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs @@ -1097,6 +1097,8 @@ namespace Barotrauma CheckValidity(); + CheckValidity(); + UpdateNetPlayerPosition(deltaTime); CheckDistFromCollider(); UpdateCollisionCategories(); @@ -1389,6 +1391,19 @@ namespace Barotrauma } if (errorMsg != null) { + if (character.IsRemotePlayer) + { + errorMsg += " Ragdoll controlled remotely."; + } + if (SimplePhysicsEnabled) + { + errorMsg += " Simple physics enabled."; + } + if (GameMain.NetworkMember != null) + { + errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; + } + #if DEBUG DebugConsole.ThrowError(errorMsg); #else diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs index b2dd6e674..7a25c82cf 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs @@ -1110,13 +1110,15 @@ namespace Barotrauma ViewTarget = null; if (!AllowInput) return; - Vector2 smoothedCursorDiff = cursorPosition - SmoothedCursorPosition; - if (Controlled == this) + if (Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)) { SmoothedCursorPosition = cursorPosition; } else { + //apply some smoothing to the cursor positions of remote players when playing as a client + //to make aiming look a little less choppy + Vector2 smoothedCursorDiff = cursorPosition - SmoothedCursorPosition; smoothedCursorDiff = NetConfig.InterpolateCursorPositionError(smoothedCursorDiff); SmoothedCursorPosition = cursorPosition - smoothedCursorDiff; } @@ -1664,10 +1666,12 @@ namespace Barotrauma focusedItem = null; } findFocusedTimer -= deltaTime; - } + } #endif //climb ladders automatically when pressing up/down inside their trigger area - if (SelectedConstruction == null && !AnimController.InWater && Screen.Selected != GameMain.SubEditorScreen) + Ladder currentLadder = SelectedConstruction?.GetComponent(); + if ((SelectedConstruction == null || currentLadder != null) && + !AnimController.InWater && Screen.Selected != GameMain.SubEditorScreen) { bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down); bool isControlled = Controlled == this; @@ -1678,6 +1682,19 @@ namespace Barotrauma float minDist = float.PositiveInfinity; foreach (Ladder ladder in Ladder.List) { + if (ladder == currentLadder) + { + continue; + } + else if (currentLadder != null) + { + //only switch from ladder to another if the ladders are above the current ladders and pressing up, or vice versa + if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y != IsKeyDown(InputType.Up)) + { + continue; + } + } + if (CanInteractWith(ladder.Item, out float dist, checkLinked: false) && dist < minDist) { minDist = dist; @@ -1934,9 +1951,23 @@ namespace Barotrauma //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! bool allowRagdoll = GameMain.NetworkMember != null ? GameMain.NetworkMember.ServerSettings.AllowRagdollButton : true; if (IsForceRagdolled) + { IsRagdolled = IsForceRagdolled; - else if (allowRagdoll && (!IsRagdolled || AnimController.Collider.LinearVelocity.LengthSquared() < 1f)) //Keep us ragdolled if we were forced or we're too speedy to unragdoll - IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + } + //Keep us ragdolled if we were forced or we're too speedy to unragdoll + else if (allowRagdoll && (!IsRagdolled || AnimController.Collider.LinearVelocity.LengthSquared() < 1f)) + { + if (ragdollingLockTimer > 0.0f) + { + ragdollingLockTimer -= deltaTime; + } + else + { + bool wasRagdolled = IsRagdolled; + IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.25f; } + } + } UpdateSightRange(); UpdateSoundRange(); @@ -2518,7 +2549,14 @@ namespace Barotrauma { item.Submarine = inventory.Owner.Submarine; var itemElement = item.Save(parentElement); - itemElement.Add(new XAttribute("i", Array.IndexOf(inventory.Items, item))); + + List slotIndices = new List(); + for (int i = 0; i < inventory.Capacity; i++) + { + if (inventory.Items[i] == item) { slotIndices.Add(i); } + } + + itemElement.Add(new XAttribute("i", string.Join(",", slotIndices))); foreach (ItemContainer container in item.GetComponents()) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs index 574e99435..c5aee6f70 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs @@ -801,13 +801,32 @@ namespace Barotrauma { foreach (XElement itemElement in element.Elements()) { - var newItem = Item.Load(itemElement, inventory.Owner.Submarine); - int slotIndex = itemElement.GetAttributeInt("i", 0); - if (newItem == null) continue; + var newItem = Item.Load(itemElement, inventory.Owner.Submarine, createNetworkEvent: true); + if (newItem == null) { continue; } - SpawnInventoryItemProjSpecific(newItem); + int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 }); + if (!slotIndices.Any()) + { + DebugConsole.ThrowError("Invalid inventory data in character \"" + Name + "\" - no slot indices found"); + continue; + } + + inventory.TryPutItem(newItem, slotIndices[0], false, false, null); - inventory.TryPutItem(newItem, slotIndex, false, false, null); + //force the item to the correct slots + // e.g. putting the item in a hand slot will also put it in the first available Any-slot, + // which may not be where it actually was + for (int i = 0; i < inventory.Capacity; i++) + { + if (slotIndices.Contains(i)) + { + inventory.Items[i] = newItem; + } + else if (inventory.Items[i] == newItem) + { + inventory.Items[i] = null; + } + } int itemContainerIndex = 0; var itemContainers = newItem.GetComponents().ToList(); @@ -821,8 +840,6 @@ namespace Barotrauma } } - partial void SpawnInventoryItemProjSpecific(Item item); - public void ReloadHeadAttachments() { ResetLoadedAttachments(); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs index f0b5af9d2..500b69c5e 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs @@ -13,17 +13,17 @@ namespace Barotrauma public readonly AnimController.Animation Animation; - public CharacterStateInfo(Vector2 pos, float rotation, Vector2 velocity, float angularVelocity, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None) + public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None) : this(pos, rotation, velocity, angularVelocity, 0, time, dir, interact, animation) { } - public CharacterStateInfo(Vector2 pos, float rotation, UInt16 ID, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None) + public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None) : this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, interact, animation) { } - protected CharacterStateInfo(Vector2 pos, float rotation, Vector2 velocity, float angularVelocity, UInt16 ID, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None) + protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None) : base(pos, rotation, velocity, angularVelocity, ID, time) { Direction = dir; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs index ad1261170..708e62bee 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -151,9 +151,12 @@ namespace Barotrauma //how high the strength has to be for the affliction to take affect public readonly float ActivationThreshold = 0.0f; //how high the strength has to be for the affliction icon to be shown in the UI - public readonly float ShowIconThreshold = 0.0f; + public readonly float ShowIconThreshold = 0.05f; public readonly float MaxStrength = 100.0f; - + + //how high the strength has to be for the affliction icon to be shown with a health scanner + public readonly float ShowInHealthScannerThreshold = 0.05f; + public float BurnOverlayAlpha; public float DamageOverlayAlpha; @@ -254,9 +257,11 @@ namespace Barotrauma } ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f); - ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", ActivationThreshold); + ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); + ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f)); + DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs index dff106cfb..5d6f70a83 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs @@ -460,6 +460,7 @@ namespace Barotrauma { affliction.Strength = 0.0f; } + CalculateVitality(); } private void AddLimbAffliction(Limb limb, Affliction newAffliction) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs index bb595803c..2d07409d1 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs @@ -491,10 +491,10 @@ namespace Barotrauma /// /// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated. /// - public bool UpdateAttack(float deltaTime, Vector2 attackPosition, IDamageable damageTarget, out AttackResult attackResult, float distance = -1) + public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1) { attackResult = default(AttackResult); - float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackPosition)); + float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime); @@ -511,7 +511,7 @@ namespace Barotrauma ignoredBodies.Add(character.AnimController.Collider.FarseerBody); structureBody = Submarine.PickBody( - SimPosition, attackPosition, + SimPosition, attackSimPos, ignoredBodies, Physics.CollisionWall); if (damageTarget is Item) @@ -520,9 +520,10 @@ namespace Barotrauma // Ignore blocking on items, because it causes cases where a Mudraptor cannot hit the hatch, for example. wasHit = true; } - else if (damageTarget is Structure && structureBody?.UserData is Structure) + else if (damageTarget is Structure wall && structureBody != null && + (structureBody.UserData is Structure || (structureBody.UserData is Submarine sub && sub == wall.Submarine))) { - // If the attack is aimed to a structure and hits a structure, it's successful + // If the attack is aimed to a structure (wall) and hits a structure or the sub, it's successful wasHit = true; } else @@ -607,7 +608,7 @@ namespace Barotrauma attack.SetCoolDown(); } - Vector2 diff = attackPosition - SimPosition; + Vector2 diff = attackSimPos - SimPosition; bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f; if (applyForces) { @@ -620,13 +621,13 @@ namespace Barotrauma Limb limb = character.AnimController.Limbs[limbIndex]; Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA; - limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos); + limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos); } } else { Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA; - body.ApplyLinearImpulse(Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos); + body.ApplyLinearImpulse(Mass * attack.Force * Vector2.Normalize(attackSimPos - SimPosition), forcePos); } } return wasHit; diff --git a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs index 17347cacc..3cdabed81 100644 --- a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs @@ -912,7 +912,7 @@ namespace Barotrauma ThrowError(args[0] + " is not a valid latency value."); return; } - if (!float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float randomLatency)) + if (!float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float randomLatency)) { ThrowError(args[1] + " is not a valid latency value."); return; diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs index adc0b0d8d..d82b66631 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs @@ -103,12 +103,6 @@ namespace Barotrauma if (missionType == MissionType.Random) { allowedMissions.AddRange(MissionPrefab.List); -#if SERVER - if (GameMain.Server != null) - { - allowedMissions.RemoveAll(mission => !GameMain.Server.ServerSettings.AllowedRandomMissionTypes.Contains(mission.type)); - } -#endif } else if (missionType == MissionType.None) { @@ -125,7 +119,7 @@ namespace Barotrauma allowedMissions.RemoveAll(m => !m.IsAllowed(locations[0], locations[1])); } - int probabilitySum = allowedMissions.Sum(m => m.Commonness); + int probabilitySum = allowedMissions.Sum(m => m.Commonness); int randomNumber = rand.NextInt32() % probabilitySum; foreach (MissionPrefab missionPrefab in allowedMissions) { diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs index d5778c9cc..ffe7cf9d3 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameSession.cs @@ -208,7 +208,7 @@ namespace Barotrauma DockingPort myPort = null, outPostPort = null; foreach (DockingPort port in DockingPort.List) { - if (port.IsHorizontal) { continue; } + if (port.IsHorizontal || port.Docked) { continue; } if (port.Item.Submarine == level.StartOutpost) { outPostPort = port; diff --git a/Barotrauma/BarotraumaShared/Source/GameSettings.cs b/Barotrauma/BarotraumaShared/Source/GameSettings.cs index 400a45443..2fd21c4aa 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSettings.cs @@ -168,7 +168,7 @@ namespace Barotrauma } } - private float soundVolume = 0.5f, musicVolume = 0.3f, voiceChatVolume = 0.5f; + private float soundVolume = 0.5f, musicVolume = 0.3f, voiceChatVolume = 0.5f, microphoneVolume = 1.0f; public float SoundVolume { @@ -211,6 +211,14 @@ namespace Barotrauma } } + public float MicrophoneVolume + { + get { return microphoneVolume; } + set + { + microphoneVolume = MathHelper.Clamp(value, 0.1f, 5.0f); + } + } public string Language { get { return TextManager.Language; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs index 7058ea85c..91d669c94 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs @@ -51,6 +51,11 @@ namespace Barotrauma.Items.Components public PhysicsBody Body { get; private set; } + private float RepairThreshold + { + get { return item.GetComponent()?.ShowRepairUIThreshold ?? 0.0f; } + } + private float stuck; [Serialize(0.0f, false)] public float Stuck diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs index fc74c2b70..7fb161dfa 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/Throwable.cs @@ -105,7 +105,7 @@ namespace Barotrauma.Items.Components GameServer.Log(picker.LogName + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); #endif - item.Drop(picker); + item.Drop(picker, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f); ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector*10.0f); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs index ea482bfa5..9b9b78066 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs @@ -208,8 +208,9 @@ namespace Barotrauma.Items.Components public ItemComponent(Item item, XElement element) { this.item = item; + originalElement = element; name = element.Name.ToString(); - properties = SerializableProperty.GetProperties(this); + SerializableProperties = SerializableProperty.GetProperties(this); requiredItems = new Dictionary>(); requiredSkills = new List(); @@ -243,18 +244,9 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Invalid pick key in " + element + "!", e); } - properties = SerializableProperty.DeserializeProperties(this, element); -#if CLIENT - string msg = TextManager.Get(Msg, true); - if (msg != null) - { - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBind(inputType).ToString()); - } - Msg = msg; - } -#endif + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + ParseMsg(); + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -631,53 +623,11 @@ namespace Barotrauma.Items.Components public virtual void Load(XElement componentElement) { if (componentElement == null) return; - foreach (XAttribute attribute in componentElement.Attributes()) { - if (!properties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) continue; + if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) continue; property.TrySetValue(this, attribute.Value); } -#if CLIENT - string msg = TextManager.Get(Msg, true); - if (msg != null) - { - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBind(inputType).ToString()); - } - Msg = msg; - } -#endif - var prevRequiredItems = new Dictionary>(requiredItems); - bool overrideRequiredItems = false; - - foreach (XElement subElement in componentElement.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "requireditem": - if (!overrideRequiredItems) requiredItems.Clear(); - overrideRequiredItems = true; - - RelatedItem newRequiredItem = RelatedItem.Load(subElement, item.Name); - if (newRequiredItem == null) continue; - - var prevRequiredItem = prevRequiredItems.ContainsKey(newRequiredItem.Type) ? - prevRequiredItems[newRequiredItem.Type].Find(ri => ri.JoinedIdentifiers == newRequiredItem.JoinedIdentifiers) : null; - if (prevRequiredItem != null) - { - newRequiredItem.statusEffects = prevRequiredItem.statusEffects; - newRequiredItem.Msg = prevRequiredItem.Msg; - } - - if (!requiredItems.ContainsKey(newRequiredItem.Type)) - { - requiredItems[newRequiredItem.Type] = new List(); - } - requiredItems[newRequiredItem.Type].Add(newRequiredItem); - break; - } - } ParseMsg(); OverrideRequiredItems(componentElement); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs index 660bc5dd8..0f790a004 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs @@ -201,19 +201,19 @@ namespace Barotrauma.Items.Components tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess); allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); - float temperatureTolerance = MathHelper.Lerp(10.0f, 20.0f, degreeOfSuccess); optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess); allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess); - - float fissionRateTolerance = MathHelper.Lerp(10.0f, 20.0f, degreeOfSuccess); - optimalFissionRate = Vector2.Lerp(new Vector2(40.0f, 70.0f), new Vector2(30.0f, 85.0f), degreeOfSuccess); - allowedFissionRate = Vector2.Lerp(new Vector2(30.0f, 85.0f), new Vector2(20.0f, 98.0f), degreeOfSuccess); + + optimalFissionRate = Vector2.Lerp(new Vector2(30, AvailableFuel - 20), new Vector2(20, AvailableFuel - 10), degreeOfSuccess); + optimalFissionRate.X = Math.Min(optimalFissionRate.X, optimalFissionRate.Y - 10); + allowedFissionRate = Vector2.Lerp(new Vector2(20, AvailableFuel), new Vector2(10, AvailableFuel), degreeOfSuccess); + allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10); float heatAmount = fissionRate * (AvailableFuel / 100.0f) * 2.0f; float temperatureDiff = (heatAmount - turbineOutput) - Temperature; Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff)); if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime; - + FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime); TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime); @@ -364,7 +364,7 @@ namespace Barotrauma.Items.Components } else if (-currPowerConsumption < load) { - targetFissionRate = Math.Min(targetFissionRate + speed * 2 * deltaTime, allowedFissionRate.Y); + targetFissionRate = Math.Min(targetFissionRate + speed * 2 * deltaTime, 100.0f); } targetFissionRate = MathHelper.Clamp(targetFissionRate, 0.0f, 100.0f); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs index 3f33c4df2..8d3baec86 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Repairable.cs @@ -12,8 +12,6 @@ namespace Barotrauma.Items.Components public static float SkillIncreaseMultiplier = 0.4f; private string header; - - private float fixDurationLowSkill, fixDurationHighSkill; private float deteriorationTimer; @@ -52,17 +50,20 @@ namespace Barotrauma.Items.Components set; } - /*private float repairProgress; - public float RepairProgress + [Serialize(100.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The amount of time it takes to fix the item with insufficient skill levels.")] + public float FixDurationLowSkill { - get { return repairProgress; } - set - { - repairProgress = MathHelper.Clamp(value, 0.0f, 1.0f); - if (repairProgress >= 1.0f && currentFixer != null) currentFixer.AnimController.Anim = AnimController.Animation.None; - } - }*/ - + get; + set; + } + + [Serialize(10.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, ToolTip = "The amount of time it takes to fix the item with sufficient skill levels.")] + public float FixDurationHighSkill + { + get; + set; + } + private Character currentFixer; public Character CurrentFixer { @@ -83,8 +84,6 @@ namespace Barotrauma.Items.Components this.item = item; header = element.GetAttributeString("name", ""); - fixDurationLowSkill = element.GetAttributeFloat("fixdurationlowskill", 100.0f); - fixDurationHighSkill = element.GetAttributeFloat("fixdurationhighskill", 5.0f); InitProjSpecific(element); } @@ -160,7 +159,7 @@ namespace Barotrauma.Items.Components } bool wasBroken = !item.IsFullCondition; - float fixDuration = MathHelper.Lerp(fixDurationLowSkill, fixDurationHighSkill, successFactor); + float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); if (fixDuration <= 0.0f) { item.Condition = item.MaxCondition; @@ -186,26 +185,5 @@ namespace Barotrauma.Items.Components { character.AnimController.UpdateUseItem(false, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((item.Condition / item.MaxCondition) % 0.1f)); } - - public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null) - { - msg.Write(deteriorationTimer); - } - - public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime) - { - deteriorationTimer = msg.ReadSingle(); - } - - public void ClientWrite(NetBuffer msg, object[] extraData = null) - { - //no need to write anything, just letting the server know we started repairing - } - - public void ServerRead(ClientNetObject type, NetBuffer msg, Client c) - { - if (c.Character == null) return; - StartRepairing(c.Character); - } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs index 021ad6197..9a93d357f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components IsActive = value; #if SERVER - if (GameMain.Server != null) item.CreateServerEvent(this); + if (GameMain.Server != null && GameMain.Server.GameStarted) { item.CreateServerEvent(this); } #endif } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs index 248838cf1..8b89d237a 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs @@ -177,26 +177,21 @@ namespace Barotrauma.Items.Components Vector2 nodePos = refSub == null ? newConnection.Item.Position : newConnection.Item.Position - refSub.HiddenSubPosition; - - - if (nodes.Count > 0 && nodes[0] == nodePos) break; - if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) break; - - if (nodes.Count > 0 && nodes[0] == nodePos) break; - if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) break; - - if (nodes.Count > 0 && nodes[0] == nodePos) break; - if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) break; - - if (nodes.Count > 0 && nodes[0] == nodePos) break; - if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) break; - + if (nodes.Count > 0 && nodes[0] == nodePos) break; if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) break; //make sure we place the node at the correct end of the wire (the end that's closest to the new node pos) int newNodeIndex = 0; if (nodes.Count > 1) + { + if (Vector2.DistanceSquared(nodes[nodes.Count-1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) + { + newNodeIndex = nodes.Count; + } + } + + if (newNodeIndex == 0) { nodes.Insert(0, nodePos); } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs index a74c4f4e2..3dfbf16d9 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs @@ -433,9 +433,10 @@ namespace Barotrauma.Items.Components if (usableProjectileCount == 0 || (usableProjectileCount < maxProjectileCount && objective.Option.ToLowerInvariant() != "fireatwill")) { ItemContainer container = null; + Item containerItem = null; foreach (MapEntity e in item.linkedTo) { - var containerItem = e as Item; + containerItem = e as Item; if (containerItem == null) continue; container = containerItem.GetComponent(); @@ -453,7 +454,7 @@ namespace Barotrauma.Items.Components var containShellObjective = new AIObjectiveContainItem(character, container.ContainableItems[0].Identifiers[0], container); character?.Speak(TextManager.Get("DialogLoadTurret").Replace("[itemname]", item.Name), null, 0.0f, "loadturret", 30.0f); containShellObjective.MinContainedAmount = usableProjectileCount + 1; - containShellObjective.IgnoreAlreadyContainedItems = true; + containShellObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; objective.AddSubObjective(containShellObjective); return false; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Item.cs b/Barotrauma/BarotraumaShared/Source/Items/Item.cs index 5244c3978..ad34272f1 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Item.cs @@ -59,8 +59,7 @@ namespace Barotrauma public PhysicsBody body; public readonly XElement StaticBodyConfig; - - private bool needsPositionUpdate; + private float lastSentCondition; private float sendConditionUpdateTimer; private bool conditionUpdatePending; @@ -89,7 +88,7 @@ namespace Barotrauma if (hasInGameEditableProperties == null) { hasInGameEditableProperties = false; - if (properties.Values.Any(p => p.Attributes.OfType().Any())) + if (SerializableProperties.Values.Any(p => p.Attributes.OfType().Any())) { hasInGameEditableProperties = true; } @@ -98,7 +97,7 @@ namespace Barotrauma foreach (ItemComponent component in components) { if (!component.AllowInGameEditing) { continue; } - if (component.properties.Values.Any(p => p.Attributes.OfType().Any())) + if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType().Any())) { hasInGameEditableProperties = true; break; @@ -212,14 +211,14 @@ namespace Barotrauma set { spriteColor = value; } } - [Serialize("1.0,1.0,1.0,1.0", false), Editable] + [Serialize("1.0,1.0,1.0,1.0", true), Editable] public Color InventoryIconColor { get; protected set; } - [Serialize("1.0,1.0,1.0,1.0", false), Editable(ToolTip = "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] + [Serialize("1.0,1.0,1.0,1.0", true), Editable(ToolTip = "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] public Color ContainerColor { get; @@ -275,12 +274,11 @@ namespace Barotrauma SetActiveSprite(); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && lastSentCondition != condition) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(lastSentCondition, condition)) { if (Math.Abs(lastSentCondition - condition) > 1.0f || condition == 0.0f || condition == Prefab.Health) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); - lastSentCondition = condition; + conditionUpdatePending = true; } } } @@ -585,10 +583,10 @@ namespace Barotrauma public override MapEntity Clone() { Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false); - foreach (KeyValuePair property in properties) + foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; - clone.properties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); + clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); } if (components.Count != clone.components.Count) @@ -605,7 +603,7 @@ namespace Barotrauma foreach (KeyValuePair property in components[i].SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; - clone.components[i].properties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i])); + clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i])); } //clone requireditem identifiers @@ -997,6 +995,21 @@ namespace Barotrauma aiTarget.SightRange -= deltaTime * 1000.0f; aiTarget.SoundRange -= deltaTime * 1000.0f; } + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + sendConditionUpdateTimer -= deltaTime; + if (conditionUpdatePending) + { + if (sendConditionUpdateTimer <= 0.0f) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); + lastSentCondition = condition; + sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; + conditionUpdatePending = false; + } + } + } ApplyStatusEffects(ActionType.Always, deltaTime, null); @@ -1273,7 +1286,11 @@ namespace Barotrauma LastSentSignalRecipients.Clear(); if (connections == null) return; - stepsTaken++; + public List GetConnectedComponentsRecursive(Connection c) where T : ItemComponent + { + List connectedComponents = new List(); + List alreadySearched = new List() { this }; + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents); if (!connections.TryGetValue(connectionName, out Connection c)) return; @@ -1561,12 +1578,15 @@ namespace Barotrauma return isCombined; } - public void Drop(Character dropper) + public void Drop(Character dropper, bool createNetworkEvent = true) { - if (parentInventory != null && !parentInventory.Owner.Removed && !Removed && - GameMain.NetworkMember != null && (GameMain.NetworkMember.IsServer || Character.Controlled == dropper)) + if (createNetworkEvent) { - parentInventory.CreateNetworkEvent(); + if (parentInventory != null && !parentInventory.Owner.Removed && !Removed && + GameMain.NetworkMember != null && (GameMain.NetworkMember.IsServer || Character.Controlled == dropper)) + { + parentInventory.CreateNetworkEvent(); + } } foreach (ItemComponent ic in components) { ic.Drop(dropper); } @@ -1809,8 +1829,20 @@ namespace Barotrauma } partial void UpdateNetPosition(float deltaTime); - + public static Item Load(XElement element, Submarine submarine) + { + return Load(element, submarine, createNetworkEvent: false); + } + + /// + /// Instantiate a new item and load its data from the XML element. + /// + /// The element containing the data of the item + /// The submarine to spawn the item in (can be null) + /// Should an EntitySpawner event be created to notify clients about the item being created. + /// + public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent) { string name = element.Attribute("name").Value; string identifier = element.GetAttributeString("identifier", ""); @@ -1856,9 +1888,16 @@ namespace Barotrauma linkedToID = new List() }; +#if SERVER + if (createNetworkEvent) + { + Spawner.CreateNetworkEvent(item, remove: false); + } +#endif + foreach (XAttribute attribute in element.Attributes()) { - if (!item.properties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) continue; + if (!item.SerializableProperties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) continue; bool shouldBeLoaded = false; foreach (var propertyAttribute in property.Attributes.OfType()) { @@ -1902,7 +1941,7 @@ namespace Barotrauma { component.OnItemLoaded(); } - + return item; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs index 6e177f7a3..76866dfdf 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/ItemInventory.cs @@ -91,8 +91,7 @@ namespace Barotrauma public override void CreateNetworkEvent() { - int componentIndex = container.Item.GetComponentIndex(container); - if (componentIndex == -1) + if (!Item.ItemList.Contains(container.Item)) { string errorMsg = "Attempted to create a network event for an item (" + container.Item.Name + ") that hasn't been fully initialized yet."; DebugConsole.ThrowError(errorMsg); @@ -101,6 +100,13 @@ namespace Barotrauma GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } + + int componentIndex = container.Item.GetComponentIndex(container); + if (componentIndex == -1) + { + DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components"); + return; + } if (GameMain.NetworkMember != null) { diff --git a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs index 2335c73c0..f0e9b8957 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs @@ -276,6 +276,9 @@ namespace Barotrauma Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false); bool underWater = hull == null || explosionPos.Y < hull.Surface; + Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false); + bool underWater = hull == null || explosionPos.Y < hull.Surface; + explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary distFactors = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs b/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs index 83ea002c0..c252c399a 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs @@ -175,12 +175,11 @@ namespace Barotrauma LimitSize(); UpdateProjSpecific(growModifier); - -#if CLIENT - if (GameMain.Client != null) return; -#endif - - if (size.X < 1.0f) Remove(); + + if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + { + Remove(); + } } partial void UpdateProjSpecific(float growModifier); @@ -293,10 +292,6 @@ namespace Barotrauma //evaporate some of the water hull.WaterVolume -= extinguishAmount; -#if CLIENT - if (GameMain.Client != null) return; -#endif - if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { Remove(); @@ -325,12 +320,11 @@ namespace Barotrauma size.X -= extinguishAmount; hull.WaterVolume -= extinguishAmount; - -#if CLIENT - if (GameMain.Client != null) return; -#endif - - if (size.X < 1.0f) Remove(); + + if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) + { + Remove(); + } } public void Extinguish(float deltaTime, float amount, Vector2 worldPosition) diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs index 8ffc5630e..0bb9df3e9 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs @@ -231,7 +231,7 @@ namespace Barotrauma public static Level CreateRandom(LocationConnection locationConnection) { - string seed = locationConnection.Locations[0].Name + locationConnection.Locations[1].Name; + string seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; float sizeFactor = MathUtils.InverseLerp( MapGenerationParams.Instance.SmallLevelConnectionLength, @@ -1522,14 +1522,40 @@ namespace Barotrauma outpost.MakeOutpost(); Point? minSize = null; + DockingPort subPort = null; if (Submarine.MainSub != null) { Point subSize = Submarine.MainSub.GetDockedBorders().Size; Point outpostSize = outpost.GetDockedBorders().Size; minSize = new Point(Math.Max(subSize.X, outpostSize.X), subSize.Y + outpostSize.Y); + + float closestDistance = float.MaxValue; + foreach (DockingPort port in DockingPort.List) + { + if (port.IsHorizontal || port.Docked) { continue; } + if (port.Item.Submarine != Submarine.MainSub) { continue; } + //the submarine port has to be at the top of the sub + if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; } + float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X); + if (dist < closestDistance) + { + subPort = port; + closestDistance = dist; + } + } } - outpost.SetPosition(outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize)); + float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; + //don't try to compensate if the port is very far from the sub's center of mass + if (Math.Abs(subDockingPortOffset) > 2000.0f) + { + subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -2000.0f, 2000.0f); + string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + DebugConsole.NewMessage(warningMsg, Color.Orange); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); + } + + outpost.SetPosition(outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, subDockingPortOffset)); if ((i == 0) == !Mirrored) { StartOutpost = outpost; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs index d96da098c..3874415b6 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObject.cs @@ -15,7 +15,6 @@ namespace Barotrauma public Vector3 Position; public float NetworkUpdateTimer; - public const float NetworkUpdateInterval = 0.2f; public float Scale; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs index 40469cea9..b75965a40 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -343,7 +343,7 @@ namespace Barotrauma { GameMain.NetworkMember.CreateEntityEvent(this, new object[] { obj }); obj.NeedsNetworkSyncing = false; - obj.NetworkUpdateTimer = LevelObject.NetworkUpdateInterval; + obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs index 83033e604..1c3ec2339 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/LevelObjects/LevelTrigger.cs @@ -432,12 +432,16 @@ namespace Barotrauma { if (ForceFluctuationStrength > 0.0f) { - forceFluctuationTimer += deltaTime; - if (forceFluctuationTimer > ForceFluctuationInterval) + //no need for force fluctuation (or network updates) if the trigger limits velocity and there are no triggerers + if (forceMode != TriggerForceMode.LimitVelocity || triggerers.Any()) { - NeedsNetworkSyncing = true; - currentForceFluctuation = Rand.Range(1.0f - ForceFluctuationStrength, 1.0f); - forceFluctuationTimer = 0.0f; + forceFluctuationTimer += deltaTime; + if (forceFluctuationTimer > ForceFluctuationInterval) + { + NeedsNetworkSyncing = true; + currentForceFluctuation = Rand.Range(1.0f - ForceFluctuationStrength, 1.0f); + forceFluctuationTimer = 0.0f; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs index c2e449bc8..5c2a35a8c 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs @@ -15,6 +15,8 @@ namespace Barotrauma public int TypeChangeTimer; + public string BaseName { get => baseName; } + public string Name { get; private set; } public Vector2 MapPosition { get; private set; } @@ -31,10 +33,10 @@ namespace Barotrauma get { CheckMissionCompleted(); - + for (int i = availableMissions.Count; i < Connections.Count * 2; i++) { - int seed = (ToolBox.StringToInt(Name) + MissionsCompleted * 10 + i) % int.MaxValue; + int seed = (ToolBox.StringToInt(BaseName) + MissionsCompleted * 10 + i) % int.MaxValue; MTRandom rand = new MTRandom(seed); LocationConnection connection = Connections[(MissionsCompleted + i) % Connections.Count]; @@ -45,7 +47,7 @@ namespace Barotrauma if (availableMissions.Any(m => m.Prefab == mission.Prefab)) { continue; } if (GameSettings.VerboseLogging && mission != null) { - DebugConsole.NewMessage("Generated a new mission for a location connection (seed: " + seed.ToString("X") + ", type: " + mission.Name + ")", Color.White); + DebugConsole.NewMessage("Generated a new mission for a location (location: " + Name + ", seed: " + seed.ToString("X") + ", missions completed: " + MissionsCompleted + ", type: " + mission.Name + ")", Color.White); } availableMissions.Add(mission); } @@ -98,7 +100,16 @@ namespace Barotrauma public void ChangeType(LocationType newType) { - if (newType == Type) return; + if (newType == Type) { return; } + + //clear missions from this and adjacent locations (they may be invalid now) + availableMissions.Clear(); + foreach (LocationConnection connection in Connections) + { + connection.OtherLocation(this)?.availableMissions.Clear(); + } + + DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); Type = newType; Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); @@ -110,6 +121,7 @@ namespace Barotrauma { if (mission.Completed) { + DebugConsole.Log("Mission \"" + mission.Name + "\" completed in \"" + Name + "\"."); MissionsCompleted++; } } @@ -117,7 +129,7 @@ namespace Barotrauma availableMissions.RemoveAll(m => m.Completed); } - private string RandomName(LocationType type) + public void Remove() { baseName = type.GetRandomName(); nameFormatIndex = Rand.Int(type.NameFormats.Count, Rand.RandSync.Server); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs index ba0ba04d5..5776ce80b 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Map/Map.cs @@ -578,10 +578,12 @@ namespace Barotrauma location.MissionsCompleted = missionsCompleted; if (showNotifications && prevLocationType != location.Type) { - ChangeLocationType( - location, - prevLocationName, - prevLocationType.CanChangeTo.Find(c => c.ChangeToType.ToLowerInvariant() == location.Type.Identifier.ToLowerInvariant())); + var change = prevLocationType.CanChangeTo.Find(c => + c.ChangeToType.ToLowerInvariant() == location.Type.Identifier.ToLowerInvariant()); + if (change != null) + { + ChangeLocationType(location, prevLocationName, change); + } } break; case "connection": diff --git a/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs index b5a5bc508..569b5ccff 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs @@ -407,7 +407,7 @@ namespace Barotrauma try { - MethodInfo loadMethod = t.GetMethod("Load"); + MethodInfo loadMethod = t.GetMethod("Load", new [] { typeof(XElement), typeof(Submarine) }); if (loadMethod == null) { DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + "."); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs index 6e4fe089b..82d0dad3e 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs @@ -297,7 +297,13 @@ namespace Barotrauma CreateStairBodies(); } } - + + // Only add ai targets automatically to walls + if (aiTarget == null && HasBody && Tags.Contains("wall")) + { + aiTarget = new AITarget(this); + } + InsertToList(); } diff --git a/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs index 75ec3391b..037364597 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/StructurePrefab.cs @@ -8,6 +8,8 @@ namespace Barotrauma { partial class StructurePrefab : MapEntityPrefab { + public XElement ConfigElement { get; private set; } + private bool canSpriteFlipX, canSpriteFlipY; private float health; @@ -150,6 +152,7 @@ namespace Barotrauma { name = element.GetAttributeString("name", "") }; + sp.ConfigElement = element; if (string.IsNullOrEmpty(sp.name)) sp.name = element.Name.ToString(); sp.identifier = element.GetAttributeString("identifier", ""); @@ -214,6 +217,10 @@ namespace Barotrauma } SerializableProperty.DeserializeProperties(sp, element); + if (sp.Body) + { + sp.Tags.Add("wall"); + } string translatedDescription = TextManager.Get("EntityDescription." + sp.identifier, true); if (!string.IsNullOrEmpty(translatedDescription)) sp.Description = translatedDescription; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs index f185a891f..66456de83 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; +using System.Threading; using System.Xml.Linq; using Voronoi2; @@ -322,7 +323,7 @@ namespace Barotrauma for (int i = 0; i <= maxLoadRetries; i++) { doc = OpenFile(filePath); - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } + if (doc != null || i == maxLoadRetries) { break; } DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); Thread.Sleep(250); } @@ -493,7 +494,7 @@ namespace Barotrauma } } - public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null) + public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f) { Rectangle dockedBorders = GetDockedBorders(); Vector2 diffFromDockedBorders = @@ -541,17 +542,17 @@ namespace Barotrauma else if (minX < 0) { //no wall found at the left side, spawn to the left from the right-side wall - spawnPos.X = maxX - minWidth - 100.0f; + spawnPos.X = maxX - minWidth - 100.0f + subDockingPortOffset; } else if (maxX > Level.Loaded.Size.X) { //no wall found at right side, spawn to the right from the left-side wall - spawnPos.X = minX + minWidth + 100.0f; + spawnPos.X = minX + minWidth + 100.0f + subDockingPortOffset; } else { //walls found at both sides, use their midpoint - spawnPos.X = (minX + maxX) / 2; + spawnPos.X = (minX + maxX) / 2 + subDockingPortOffset; } spawnPos.Y = Math.Min(spawnPos.Y, Level.Loaded.Size.Y - dockedBorders.Height / 2 - 10); @@ -1191,7 +1192,7 @@ namespace Barotrauma for (int i = 0; i <= maxLoadRetries; i++) { doc = OpenFile(filePath); - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } + if (doc != null || i == maxLoadRetries) { break; } DebugConsole.NewMessage("Loading the submarine \"" + Name + "\" failed, retrying in 250 ms..."); Thread.Sleep(250); } diff --git a/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs index 0a1b5fa21..a0d500ade 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/SubmarineBody.cs @@ -700,6 +700,23 @@ namespace Barotrauma Vector2 impulse = direction * impact * 0.5f; impulse = impulse.ClampLength(5.0f); + if (!MathUtils.IsValid(impulse)) + { + string errorMsg = + "Invalid impulse in SubmarineBody.ApplyImpact: " + impulse + + ". Direction: " + direction + ", body position: " + Body.SimPosition + ", impact: " + impact + "."; + if (GameMain.NetworkMember != null) + { + errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; + } + if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "SubmarineBody.ApplyImpact:InvalidImpulse", + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + errorMsg); + return; + } + #if CLIENT if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs index 2876896aa..e40d6c714 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs @@ -129,10 +129,10 @@ namespace Barotrauma.Networking if (listener.WorldPosition == sender.WorldPosition) { return 0.0f; } float dist = Vector2.Distance(listener.WorldPosition, sender.WorldPosition); - if (dist > range) { return 0.0f; } + if (dist > range) { return 1.0f; } if (Submarine.CheckVisibility(listener.SimPosition, sender.SimPosition) != null) dist = (dist + 100f) * obstructionmult; - if (dist > range) { return 0.0f; } + if (dist > range) { return 1.0f; } return dist / range; } @@ -152,7 +152,7 @@ namespace Barotrauma.Networking public static string ApplyDistanceEffect(string text, float garbleAmount) { if (garbleAmount < 0.3f) return text; - if (garbleAmount > 1.0f) return ""; + if (garbleAmount >= 1.0f) return ""; int startIndex = Math.Max(text.IndexOf(':') + 1, 1); diff --git a/Barotrauma/BarotraumaShared/Source/Networking/FileTransfer/FileTransfer.cs b/Barotrauma/BarotraumaShared/Source/Networking/FileTransfer/FileTransfer.cs index 51a489698..a5d48f5eb 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/FileTransfer/FileTransfer.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/FileTransfer/FileTransfer.cs @@ -7,7 +7,7 @@ enum FileTransferMessageType { - Unknown, Initiate, Data, Cancel + Unknown, Initiate, Data, TransferOnSameMachine, Cancel } enum FileTransferType diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs index d7a36e28c..74b6afcb2 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs @@ -32,11 +32,14 @@ namespace Barotrauma.Networking public const float HighPrioCharacterPositionUpdateInterval = 0.0f; public const float LowPrioCharacterPositionUpdateInterval = 1.0f; - //how much the physics body of an item has to move until the server - //send a position update to clients (in sim units) - public const float ItemPosUpdateDistance = 2.0f; - - public const float DeleteDisconnectedTime = 10.0f; + public const float DeleteDisconnectedTime = 20.0f; + + public const float ItemConditionUpdateInterval = 0.15f; + public const float LevelObjectUpdateInterval = 0.5f; + public const float HullUpdateInterval = 0.5f; + public const float HullUpdateDistance = 20000.0f; + + public const int MaxEventPacketsPerUpdate = 4; /// /// Interpolates the positional error of a physics body towards zero. diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs index 03e28f3d4..20cb9b39a 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -58,15 +58,8 @@ namespace Barotrauma.Networking eventCount++; continue; } - //the ID has been taken by another entity (the original entity has been removed) -> write an empty event - /*else if (Entity.FindEntityByID(e.Entity.ID) != e.Entity || e.Entity.IdFreed) - { - //technically the clients don't have any use for these, but removing events and shifting the IDs of all - //consecutive ones is so error-prone that I think this is a safer option - tempBuffer.Write(Entity.NullEntityID); - tempBuffer.WritePadBits(); - }*/ - else + + if (msg.LengthBytes + tempBuffer.LengthBytes + tempEventBuffer.LengthBytes > MaxEventBufferLength) { //no more room in this packet break; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs index 5732a9955..3a4f36a23 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs @@ -513,11 +513,29 @@ namespace Barotrauma.Networking set; } + private SelectionMode subSelectionMode; [Serialize(SelectionMode.Manual, true)] - public SelectionMode SubSelectionMode { get; private set; } + public SelectionMode SubSelectionMode + { + get { return subSelectionMode; } + set + { + subSelectionMode = value; + Voting.AllowSubVoting = subSelectionMode == SelectionMode.Vote; + } + } + private SelectionMode modeSelectionMode; [Serialize(SelectionMode.Manual, true)] - public SelectionMode ModeSelectionMode { get; private set; } + public SelectionMode ModeSelectionMode + { + get { return modeSelectionMode; } + set + { + modeSelectionMode = value; + Voting.AllowModeVoting = modeSelectionMode == SelectionMode.Vote; + } + } public BanList BanList { get; private set; } diff --git a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs index c8d7f480c..d87c6ab3b 100644 --- a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs @@ -36,392 +36,20 @@ namespace Barotrauma private set; } - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - - public Vector2 LinearVelocity - { - get; - private set; - } - - public float AngularVelocity - { - get; - private set; - } - public readonly float Timestamp; public readonly UInt16 ID; - public PosInfo(Vector2 pos, float rotation, Vector2 linearVelocity, float angularVelocity, float time) + public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, float time) : this(pos, rotation, linearVelocity, angularVelocity, 0, time) { } - public PosInfo(Vector2 pos, float rotation, Vector2 linearVelocity, float angularVelocity, UInt16 ID) + public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID) : this(pos, rotation, linearVelocity, angularVelocity, ID, 0.0f) { } - protected PosInfo(Vector2 pos, float rotation, Vector2 linearVelocity, float angularVelocity, UInt16 ID, float time) + protected PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID, float time) { Position = pos; Rotation = rotation; @@ -1147,8 +775,8 @@ namespace Barotrauma newVelocity = positionBuffer[0].LinearVelocity; newPosition = positionBuffer[0].Position; - newRotation = positionBuffer[0].Rotation; - newAngularVelocity = positionBuffer[0].AngularVelocity; + newRotation = positionBuffer[0].Rotation ?? Rotation; + newAngularVelocity = positionBuffer[0].AngularVelocity ?? AngularVelocity; positionBuffer.RemoveAt(0); } diff --git a/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs index 55d8c62fe..072c260ab 100644 --- a/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs @@ -107,7 +107,7 @@ namespace Barotrauma public string[] propertyNames; private object[] propertyEffects; - private PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.And; + private PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; private List propertyConditionals; private bool setValue; @@ -465,6 +465,13 @@ namespace Barotrauma if (target == null || target.SerializableProperties == null) { continue; } foreach (PropertyConditional pc in propertyConditionals) { + if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) + { + if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + { + continue; + } + } if (pc.Matches(target)) { return true; } } } @@ -475,6 +482,13 @@ namespace Barotrauma if (target == null || target.SerializableProperties == null) { continue; } foreach (PropertyConditional pc in propertyConditionals) { + if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) + { + if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + { + continue; + } + } if (!pc.Matches(target)) { return false; } } } @@ -696,6 +710,8 @@ namespace Barotrauma foreach (Limb limb in character.AnimController.Limbs) { limb.character.DamageLimb(entity.WorldPosition, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f); + //only apply non-limb-specific afflictions to the first limb + if (!affliction.Prefab.LimbSpecific) { break; } } } else if (target is Limb limb) diff --git a/Barotrauma/BarotraumaShared/Source/TextManager.cs b/Barotrauma/BarotraumaShared/Source/TextManager.cs index a4b006c84..1d5d074c8 100644 --- a/Barotrauma/BarotraumaShared/Source/TextManager.cs +++ b/Barotrauma/BarotraumaShared/Source/TextManager.cs @@ -153,49 +153,59 @@ namespace Barotrauma string[] messages = serverMessage.Split('/'); - for (int i = 0; i < messages.Length; i++) + try { - if (!IsServerMessageWithVariables(messages[i])) // No variables, try to translate + for (int i = 0; i < messages.Length; i++) { - if (messages[i].Contains(" ")) continue; // Spaces found, do not translate - - string msg = Get(messages[i], true); - - if (msg != null) // If a translation was found, otherwise use the original + if (!IsServerMessageWithVariables(messages[i])) // No variables, try to translate { - messages[i] = msg; - } - } - else - { - string[] messageWithVariables = messages[i].Split('~'); - string msg = Get(messageWithVariables[0], true); - - if (msg != null) // If a translation was found, otherwise use the original - { - messages[i] = msg; + if (messages[i].Contains(" ")) continue; // Spaces found, do not translate + string msg = Get(messages[i], true); + if (msg != null) // If a translation was found, otherwise use the original + { + messages[i] = msg; + } } else { - continue; // No translation found, probably caused by player input -> skip variable handling - } + string[] messageWithVariables = messages[i].Split('~'); + string msg = Get(messageWithVariables[0], true); - // First index is always the message identifier -> start at 1 - for (int j = 1; j < messageWithVariables.Length; j++) - { - string[] variableAndValue = messageWithVariables[j].Split('='); - messages[i] = messages[i].Replace(variableAndValue[0], variableAndValue[1]); + if (msg != null) // If a translation was found, otherwise use the original + { + messages[i] = msg; + } + else + { + continue; // No translation found, probably caused by player input -> skip variable handling + } + + // First index is always the message identifier -> start at 1 + for (int j = 1; j < messageWithVariables.Length; j++) + { + string[] variableAndValue = messageWithVariables[j].Split('='); + messages[i] = messages[i].Replace(variableAndValue[0], variableAndValue[1]); + } } } + + string translatedServerMessage = string.Empty; + for (int i = 0; i < messages.Length; i++) + { + translatedServerMessage += messages[i]; + } + return translatedServerMessage; } - string translatedServerMessage = string.Empty; - for (int i = 0; i < messages.Length; i++) + catch (IndexOutOfRangeException exception) { - translatedServerMessage += messages[i]; + string errorMsg = "Failed to translate server message \"" + serverMessage + "\"."; +#if DEBUG + DebugConsole.ThrowError(errorMsg, exception); +#endif + GameAnalyticsManager.AddErrorEventOnce("TextManager.GetServerMessage:" + serverMessage, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return errorMsg; } - - return translatedServerMessage; } public static bool IsServerMessageWithVariables(string message) diff --git a/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs index 31760c6d5..bb12617b7 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs @@ -753,6 +753,15 @@ namespace Barotrauma } } + /// + /// Float comparison. Note that may still fail in some cases. + /// + public static bool NearlyEqual(Vector2 a, Vector2 b, float epsilon = 0.0001f) + { + return NearlyEqual(a.X, b.X, epsilon) && NearlyEqual(a.Y, b.Y, epsilon); + } + + /// /// Returns a position in a curve. /// public static Vector2 Bezier(Vector2 start, Vector2 control, Vector2 end, float t) diff --git a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs index 5ad818e21..8f7e07c60 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs @@ -358,8 +358,8 @@ namespace Barotrauma } catch (IOException e) { - if (i >= maxRetries || !File.Exists(sCompressedFile)) { throw; } - DebugConsole.NewMessage("Failed decompress file \"" + sCompressedFile + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); + if (i >= maxRetries) { throw; } + DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}, retrying in 250 ms...", Color.Red); Thread.Sleep(250); } } @@ -403,18 +403,18 @@ namespace Barotrauma } } - public static void ClearFolder(string FolderName, string[] ignoredFiles = null) + public static void ClearFolder(string FolderName, string[] ignoredFileNames = null) { DirectoryInfo dir = new DirectoryInfo(FolderName); foreach (FileInfo fi in dir.GetFiles()) { - if (ignoredFiles != null) + if (ignoredFileNames != null) { bool ignore = false; - foreach (string ignoredFile in ignoredFiles) + foreach (string ignoredFile in ignoredFileNames) { - if (Path.GetFullPath(fi.FullName).Equals(Path.GetFullPath(ignoredFile))) + if (Path.GetFileName(fi.FullName).Equals(Path.GetFileName(ignoredFile))) { ignore = true; break; diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index dd01ea22b..bfcc4904b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index aacc41a31..0e37eeee6 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Muikku.sub b/Barotrauma/BarotraumaShared/Submarines/Muikku.sub index 759d33ff4..386de54c9 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Muikku.sub and b/Barotrauma/BarotraumaShared/Submarines/Muikku.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 6f8e91fd6..3d9685698 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/PAX.sub b/Barotrauma/BarotraumaShared/Submarines/PAX.sub index 1ce31fef0..58e7cc9a0 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/PAX.sub and b/Barotrauma/BarotraumaShared/Submarines/PAX.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 4207a60d2..74c05a493 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index 1c829b2d6..57f8fc56f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 40dd2231f..421c3d7df 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,48 @@ +--------------------------------------------------------------------------------------------------------- +v0.8.9.7 +--------------------------------------------------------------------------------------------------------- + +Additions and changes: +- The fission rate gauge on the reactor now visualizes the amount of fuel available. +- Doors and hatches can now also be repaired with wrenches. +- Changed default StatusEffect conditional comparison type back to OR. +- Clients communicate syncing errors to the server, and the server logs a more descriptive error about +what went wrong. Should make it easier to diagnose disconnection issues from now on. +- Ending a multiplayer campaign round by talking to watchman doesn't require any special permissions. +- Server automatically ends rounds if there have been no players alive in 60 seconds and respawning +is not allowed during the round. +- Added a button for resetting an entity's properties to the default values to the sub editor. +- Updated handheld sonar UI graphics. + +Bugfixes: +- Fixed non-radio chat/VOIP having an unlimited distance. +- Fixed a networking bug that caused the server to send item state changes to the clients before sending +a message about the item being spawned. For example, spawning any item with a LightComponent would always +cause clients to get disconnected. +- Changes to the way the clients are put in sync with the server when joining mid-round. Should make it +less likely for clients to get disconnected immediately after starting a round. +- StatusEffects only apply non-limb-specific afflictions to one limb even if targeting the whole character. +Fixes drugs like fentanyl and morphine being way too harmful due to the oxygen loss affliction being +applied once per every limb. +- Fixed TargetItemComponentName not working in StatusEffect conditionals (making it impossible to create +conditionals that target a specific component of an item). +- Made all of the new medical items combinable and usable in a syringe gun (assuming the drug is in a syringe). +- Fixed inability to throw anything in the multiplayer. +- Fixed multiplayer campaign setup UI showing the client's subs instead of the server's. +- Fixed campaign view button overlapping with the "ready to start" tickbox in the server lobby. +- Fixed sub/mode voting not being enabled when changing the setting after the server has been started. +- Fixed character inventories occasionally being saved incorrectly in the multiplayer campaign when an +- Fixed flares not activating by left clicking. +- Fixed affliction icons flickering rapidly in the health interface and above the health bar if their +strength is fluctuating around the threshold where the icon becomes visible. +- Fixed dedicated server crashing when typing in more text than can fit on one line. +- Fixed enemies "fleeing" after they have been shot. There was a steering issue when they targeted characters +inside the sub while being outside. +- Fixed Hammerhead attack causing warping. +- Fixed incorrect submarine and level seed in server logs when playing campaign mode. +- Hide the start button from the campaign UI if the client doesn't have the permission to manage +the campaign or rounds. + --------------------------------------------------------------------------------------------------------- v0.8.9.6 --------------------------------------------------------------------------------------------------------- @@ -13,6 +58,7 @@ themselves alive and less likely to get stuck. - New husk sprites. - Improved item/character highlight effect. - New signal items (divide, multiply, subtract, memory, equals, greater than, color, xor). +- Option to adjust microphone volume in multiplayer. - Added a console commands for changing the gender and race of the character. - More intuitive BrokenSprite condition logic: a BrokenSprite with a MaxCondition of 50 will start fading in at 50 (and be fully visible when the condition drops to 0 or down to the MaxCondition of @@ -43,6 +89,8 @@ suitable capture device. - Fixed crashing when starting a round if a submarine name contains underscores. - Fixed clients console errors when attempting to modify the properties of an ItemComponent in-game (for example, the color of a light component). +- Added some more information to the console messages and error popups when a client gets disconnected. +- Fixed enablecheats command crashing the server if it's used when there are no clients present. Misc bugfixes: - Audio fixes (less snap, crackle and pop). @@ -55,12 +103,18 @@ identifiers. - Fixed item interfaces getting repositioned every frame when the editing HUD is open. - Fixed held items clipping with the sleeves of the character (e.g. when holding a revolver while an uniform is equipped). +- Fixed being able to levitate by spamming the ragdoll button. - Fixed dead characters draining oxygen tanks inside diving suits/masks. +- Fixed reactor gauges getting messed up if the optimal fission rate is more than 100% (which may happen +if the power consumption is larger than what the reactor can generate). - Fixed mud raptors not having an inventory (nor lootable items). - Fixed inability to interact with any items when aim assist is set to 0%. - Fixed info panel flickering out and Tab getting "inverted" (= info panel shown when tab is not being held) when selecting crew members in the panel. - Fixed characters arms occasionally getting stuck above their shoulders. +- Fixed wire nodes occasionally being created at the wrong end of a wire (e.g. when moving a wire between +connections in a connection panel, the wire stretched from the device at the other end of the wire to +the device that's being rewired). Misc: - Changed the way arguments are given to the "setclientcharacter" command (no semicolon to separate the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25fbbf2f3..652dc4fb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,8 @@ The client project includes the code only required by the client executable: gra ### Building the game Before building, you should choose the build configuration. The available configurations are `DebugWindows`, `DebugMac`, `DebugLinux`, `ReleaseWindows`, `ReleaseMac` and `ReleaseLinux`. Additionally, unless you want to work on the dedicated server, you may want to make sure `WindowsClient`, `MacClient` or `LinuxClient` are selected as the startup project. +The `BarotraumaShared\Content` folder, which contains Barotrauma's art, item XMLs, sounds, and other assets, is not included in the GitHub repository. If you have a legal copy of the game, you can copy the `Content` folder from the game's files to `BarotraumaShared\Content`. + The debug build configurations include some features that make debugging and testing a little easier: things such as additional console commands, being able to move the submarine with the IJKL keys and allowing clients to use any console command in multiplayer. The debug builds don't create crash reports when an unhandled exception occurs - the intention behind this is to allow exceptions to be caught by the debugger instead of having the game close and write a report. To build through the command line (recommended if you're on Linux), use the following command: diff --git a/git-strip-merge b/git-strip-merge index 318a93d46..3bae2789e 100644 --- a/git-strip-merge +++ b/git-strip-merge @@ -120,8 +120,21 @@ gitbranch() { original=$(gitbranch HEAD) branchsha=$(gitsha "$branch") +branchshortsha="${branchsha:0:7}" -logmsg="$(git log .."$branchsha")" +#get the hash of the last merged remote commit from the latest commit msg in this repo +#assumes that the commit message starts with hash1...hash2 +prevcommitmsg="$(git log -1 --pretty=%B)" +prevcommitsha="${prevcommitmsg:10:7}" + +#create a commit message for out new "merge commit" +# hash1...hash2 + logs of the merged commits +logmsg="$prevcommitsha" +logmsg+="..." +logmsg+="$branchshortsha" +logmsg+=" + +$(git log "$prevcommitsha"..."$branchsha")" trap 'git checkout --quiet "$original"' EXIT @@ -130,11 +143,10 @@ git rm -rf -r "${files[@]}" "${quiet[@]}" && git commit -m "$msgcommit" "${quiet[@]}" && newsha=$(gitsha HEAD) && git checkout "$original" "${quiet[@]}" && -#original -#git merge -m "$msgmerge" "${mergeopts[@]}" "$newsha" -#baro additions -git merge -m "$msgmerge" "--squash" "$newsha" +git merge -m "$msgmerge" -X theirs "--squash" "$newsha" +git checkout HEAD .gitignore git commit -m "$logmsg" -#./git-strip-merge --no-commit --no-ff barotrauma-development/dev -f Barotrauma/BarotraumaShared/Content/* +#example usage +#./git-strip-merge barotrauma-development/dev -f Barotrauma/BarotraumaShared/Content/*